-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #35 from keep-network/nonce-management
Local nonce manager
- Loading branch information
Showing
8 changed files
with
271 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters