Skip to content

Commit 8fbb5b3

Browse files
authored
Merge pull request #35 from keep-network/nonce-management
Local nonce manager
2 parents fc13282 + 4cefc7b commit 8fbb5b3

File tree

8 files changed

+271
-2
lines changed

8 files changed

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

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(key.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(key.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)