Skip to content

Commit 6f7e457

Browse files
committed
Local nonce manager
Nonce manager is used by the generated contracts to track the current nonce and update it after each successfully submitted transaction. Tracking the nonce locally is required when transactions are submitted from multiple goroutines for when Ethereum clients are deployed behind a load balancer and their mempools are not always in sync.
1 parent fc13282 commit 6f7e457

8 files changed

Lines changed: 225 additions & 2 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package ethutil
2+
3+
import (
4+
"context"
5+
6+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
7+
"github.com/ethereum/go-ethereum/common"
8+
)
9+
10+
// NonceManager tracks the nonce for the account and allows to update it after
11+
// each successfully submitted transaction. Tracking the nonce locall is
12+
// required when transactions are submitted from multiple goroutines or when
13+
// multiple Ethereum clients are deployed behind a load balancer, there are no
14+
// sticky sessions and mempool synchronization between them takes some time.
15+
//
16+
// NonceManager provides no synchronization and is NOT safe for concurrent use.
17+
// It is up to the client code to implement the required synchronization.
18+
//
19+
// An example execution might work as follows:
20+
// 1. Obtain transaction lock,
21+
// 2. Calculate CurrentNonce(),
22+
// 3. Submit transaction with the calculated nonce,
23+
// 4. Call IncrementNonce(),
24+
// 5. Release transaction lock.
25+
type NonceManager struct {
26+
account common.Address
27+
transactor bind.ContractTransactor
28+
localNonce uint64
29+
}
30+
31+
// NewNonceManager creates NonceManager instance for the provided account using
32+
// the provided contract transactor. Contract transactor is used for every
33+
// CurrentNonce execution to check the pending nonce value as seen by the
34+
// Ethereum client.
35+
func NewNonceManager(
36+
account common.Address,
37+
transactor bind.ContractTransactor,
38+
) *NonceManager {
39+
return &NonceManager{
40+
account: account,
41+
transactor: transactor,
42+
localNonce: 0,
43+
}
44+
}
45+
46+
// CurrentNonce returns the nonce value that should be used for the next
47+
// transaction. The nonce is evaluated as the higher value from the local
48+
// nonce and pending nonce fetched from the Ethereum client.
49+
//
50+
// CurrentNonce is NOT safe for concurrent use. It is up to the code using this
51+
// function to provide the required synchronization, optionally including
52+
// IncrementNonce call as well.
53+
func (nm *NonceManager) CurrentNonce() (uint64, error) {
54+
pendingNonce, err := nm.transactor.PendingNonceAt(
55+
context.TODO(),
56+
nm.account,
57+
)
58+
if err != nil {
59+
return 0, err
60+
}
61+
62+
if pendingNonce < nm.localNonce {
63+
logger.Infof(
64+
"local nonce [%v] is higher than pending [%v]; using the local one",
65+
nm.localNonce,
66+
pendingNonce,
67+
)
68+
}
69+
70+
if pendingNonce > nm.localNonce {
71+
logger.Infof(
72+
"local nonce [%v] is lower than pending [%v]; updating",
73+
nm.localNonce,
74+
pendingNonce,
75+
)
76+
77+
nm.localNonce = pendingNonce
78+
}
79+
80+
return nm.localNonce, nil
81+
}
82+
83+
// IncrementNonce increments the value of the nonce kept locally by one.
84+
// This function is NOT safe for concurrent use. It is up to the client code
85+
// using this function to provide the required synchronization.
86+
func (nm *NonceManager) IncrementNonce() uint64 {
87+
nm.localNonce++
88+
return nm.localNonce
89+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package ethutil
2+
3+
import (
4+
"context"
5+
"math/big"
6+
"testing"
7+
8+
"github.com/ethereum/go-ethereum"
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/ethereum/go-ethereum/core/types"
11+
)
12+
13+
func TestResolveAndIncrement(t *testing.T) {
14+
tests := map[string]struct {
15+
pendingNonce uint64
16+
localNonce uint64
17+
expectedNonce uint64
18+
expectedNextNonce uint64
19+
}{
20+
"pending and local the same": {
21+
pendingNonce: 10,
22+
localNonce: 10,
23+
expectedNonce: 10,
24+
expectedNextNonce: 11,
25+
},
26+
"pending nonce higher": {
27+
pendingNonce: 121,
28+
localNonce: 120,
29+
expectedNonce: 121,
30+
expectedNextNonce: 122,
31+
},
32+
"pending nonce lower": {
33+
pendingNonce: 110,
34+
localNonce: 111,
35+
expectedNonce: 111,
36+
expectedNextNonce: 112,
37+
},
38+
}
39+
40+
for testName, test := range tests {
41+
t.Run(testName, func(t *testing.T) {
42+
transactor := &mockTransactor{test.pendingNonce}
43+
manager := &NonceManager{
44+
transactor: transactor,
45+
localNonce: test.localNonce,
46+
}
47+
48+
nonce, err := manager.CurrentNonce()
49+
if err != nil {
50+
t.Fatal(err)
51+
}
52+
53+
if nonce != test.expectedNonce {
54+
t.Errorf(
55+
"unexpected nonce\nexpected: [%v]\nactual: [%v]",
56+
test.expectedNonce,
57+
nonce,
58+
)
59+
}
60+
61+
nextNonce := manager.IncrementNonce()
62+
63+
if nextNonce != test.expectedNextNonce {
64+
t.Errorf(
65+
"unexpected nonce\nexpected: [%v]\nactual: [%v]",
66+
test.expectedNextNonce,
67+
nextNonce,
68+
)
69+
}
70+
})
71+
}
72+
}
73+
74+
type mockTransactor struct {
75+
nextNonce uint64
76+
}
77+
78+
func (mt *mockTransactor) PendingCodeAt(
79+
ctx context.Context,
80+
account common.Address,
81+
) ([]byte, error) {
82+
panic("not implemented")
83+
}
84+
85+
func (mt *mockTransactor) PendingNonceAt(
86+
ctx context.Context,
87+
account common.Address,
88+
) (uint64, error) {
89+
return mt.nextNonce, nil
90+
}
91+
92+
func (mt *mockTransactor) SuggestGasPrice(
93+
ctx context.Context,
94+
) (*big.Int, error) {
95+
panic("not implemented")
96+
}
97+
98+
func (mt *mockTransactor) EstimateGas(
99+
ctx context.Context,
100+
call ethereum.CallMsg,
101+
) (gas uint64, err error) {
102+
panic("not implemented")
103+
}
104+
105+
func (mt *mockTransactor) SendTransaction(
106+
ctx context.Context,
107+
tx *types.Transaction,
108+
) error {
109+
panic("not implemented")
110+
}

tools/generators/ethereum/command.go.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) {
221221
address,
222222
key,
223223
client,
224+
ethutil.NewNonceManager(config.Account.Address, client),
224225
&sync.Mutex{},
225226
)
226227
}

tools/generators/ethereum/command_template_content.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ func initialize{{.Class}}(c *cli.Context) (*contract.{{.Class}}, error) {
224224
address,
225225
key,
226226
client,
227+
ethutil.NewNonceManager(config.Account.Address, client),
227228
&sync.Mutex{},
228229
)
229230
}

tools/generators/ethereum/contract.go.tmpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type {{.Class}} struct {
3434
callerOptions *bind.CallOpts
3535
transactorOptions *bind.TransactOpts
3636
errorResolver *ethutil.ErrorResolver
37+
nonceManager *ethutil.NonceManager
3738

3839
transactionMutex *sync.Mutex
3940
}
@@ -42,6 +43,7 @@ func New{{.Class}}(
4243
contractAddress common.Address,
4344
accountKey *keystore.Key,
4445
backend bind.ContractBackend,
46+
nonceManager *ethutil.NonceManager,
4547
transactionMutex *sync.Mutex,
4648
) (*{{.Class}}, error) {
4749
callerOptions := &bind.CallOpts{
@@ -78,6 +80,7 @@ func New{{.Class}}(
7880
callerOptions: callerOptions,
7981
transactorOptions: transactorOptions,
8082
errorResolver: ethutil.NewErrorResolver(backend, &contractABI, &contractAddress),
83+
nonceManager: nonceManager,
8184
transactionMutex: transactionMutex,
8285
}, nil
8386
}

tools/generators/ethereum/contract_non_const_methods.go.tmpl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,17 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}(
4242
transactionOptions[0].Apply(transactorOptions)
4343
}
4444

45+
nonce, err := {{$contract.ShortVar}}.nonceManager.CurrentNonce()
46+
if err != nil {
47+
return nil, fmt.Errorf("failed to retrieve account nonce: %v", err)
48+
}
49+
50+
transactorOptions.Nonce = new(big.Int).SetUint64(nonce)
51+
4552
transaction, err := {{$contract.ShortVar}}.contract.{{$method.CapsName}}(
4653
transactorOptions,
4754
{{$method.Params}}
4855
)
49-
5056
if err != nil {
5157
return transaction, {{$contract.ShortVar}}.errorResolver.ResolveError(
5258
err,
@@ -66,6 +72,8 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}(
6672
transaction.Hash().Hex(),
6773
)
6874

75+
{{$contract.ShortVar}}.nonceManager.IncrementNonce()
76+
6977
return transaction, err
7078
}
7179

tools/generators/ethereum/contract_non_const_methods_template_content.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,17 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}(
4545
transactionOptions[0].Apply(transactorOptions)
4646
}
4747
48+
nonce, err := {{$contract.ShortVar}}.nonceManager.CurrentNonce()
49+
if err != nil {
50+
return nil, fmt.Errorf("failed to retrieve account nonce: %v", err)
51+
}
52+
53+
transactorOptions.Nonce = new(big.Int).SetUint64(nonce)
54+
4855
transaction, err := {{$contract.ShortVar}}.contract.{{$method.CapsName}}(
4956
transactorOptions,
5057
{{$method.Params}}
5158
)
52-
5359
if err != nil {
5460
return transaction, {{$contract.ShortVar}}.errorResolver.ResolveError(
5561
err,
@@ -69,6 +75,8 @@ func ({{$contract.ShortVar}} *{{$contract.Class}}) {{$method.CapsName}}(
6975
transaction.Hash().Hex(),
7076
)
7177
78+
{{$contract.ShortVar}}.nonceManager.IncrementNonce()
79+
7280
return transaction, err
7381
}
7482

tools/generators/ethereum/contract_template_content.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type {{.Class}} struct {
3737
callerOptions *bind.CallOpts
3838
transactorOptions *bind.TransactOpts
3939
errorResolver *ethutil.ErrorResolver
40+
nonceManager *ethutil.NonceManager
4041
4142
transactionMutex *sync.Mutex
4243
}
@@ -45,6 +46,7 @@ func New{{.Class}}(
4546
contractAddress common.Address,
4647
accountKey *keystore.Key,
4748
backend bind.ContractBackend,
49+
nonceManager *ethutil.NonceManager,
4850
transactionMutex *sync.Mutex,
4951
) (*{{.Class}}, error) {
5052
callerOptions := &bind.CallOpts{
@@ -81,6 +83,7 @@ func New{{.Class}}(
8183
callerOptions: callerOptions,
8284
transactorOptions: transactorOptions,
8385
errorResolver: ethutil.NewErrorResolver(backend, &contractABI, &contractAddress),
86+
nonceManager: nonceManager,
8487
transactionMutex: transactionMutex,
8588
}, nil
8689
}

0 commit comments

Comments
 (0)