Skip to content

Commit cc1b41b

Browse files
authored
fix: Sort recycled nonces in ascending order for reuse or cancellation (#634)
* fix: Sort recycled nonces in ascending order for reuse or cancellation * fix: Update cancelRecycledNoncesWorker to work with sorted set
1 parent 52981f0 commit cc1b41b

File tree

2 files changed

+28
-24
lines changed

2 files changed

+28
-24
lines changed

src/db/wallets/walletNonce.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export const lastUsedNonceKey = (chainId: number, walletAddress: Address) =>
1313
`nonce:${chainId}:${normalizeAddress(walletAddress)}`;
1414

1515
/**
16-
* The "recycled nonces" set stores unsorted nonces to be reused or cancelled.
17-
* Example: [ "25", "23", "24" ]
16+
* The "recycled nonces" sorted set stores nonces to be reused or cancelled, sorted by nonce value.
17+
* Example: [ "23", "24", "25" ]
1818
*/
1919
export const recycledNoncesKey = (chainId: number, walletAddress: Address) =>
2020
`nonce-recycled:${chainId}:${normalizeAddress(walletAddress)}`;
@@ -86,15 +86,15 @@ export const acquireNonce = async (
8686
chainId: number,
8787
walletAddress: Address,
8888
): Promise<{ nonce: number; isRecycledNonce: boolean }> => {
89-
// Acquire an recylced nonce, if any.
90-
let nonce = await _acquireRecycledNonce(chainId, walletAddress);
91-
if (nonce !== null) {
92-
return { nonce, isRecycledNonce: true };
89+
// Try to acquire the lowest recycled nonce first
90+
const recycledNonce = await _acquireRecycledNonce(chainId, walletAddress);
91+
if (recycledNonce !== null) {
92+
return { nonce: recycledNonce, isRecycledNonce: true };
9393
}
9494

9595
// Else increment the last used nonce.
9696
const key = lastUsedNonceKey(chainId, walletAddress);
97-
nonce = await redis.incr(key);
97+
let nonce = await redis.incr(key);
9898
if (nonce === 1) {
9999
// If INCR returned 1, the nonce was not set.
100100
// This may be a newly imported wallet.
@@ -125,22 +125,25 @@ export const recycleNonce = async (
125125
}
126126

127127
const key = recycledNoncesKey(chainId, walletAddress);
128-
await redis.sadd(key, nonce.toString());
128+
await redis.zadd(key, nonce, nonce.toString());
129129
};
130130

131131
/**
132-
* Acquires a recycled nonce that is unused.
132+
* Acquires the lowest recycled nonce that is unused.
133133
* @param chainId
134134
* @param walletAddress
135135
* @returns
136136
*/
137137
const _acquireRecycledNonce = async (
138138
chainId: number,
139139
walletAddress: Address,
140-
) => {
140+
): Promise<number | null> => {
141141
const key = recycledNoncesKey(chainId, walletAddress);
142-
const res = await redis.spop(key);
143-
return res ? parseInt(res) : null;
142+
const result = await redis.zpopmin(key);
143+
if (result.length === 0) {
144+
return null;
145+
}
146+
return parseInt(result[0]);
144147
};
145148

146149
const _syncNonce = async (
@@ -166,7 +169,7 @@ const _syncNonce = async (
166169
/**
167170
* Returns the last used nonce.
168171
* This function should be used to inspect nonce values only.
169-
* Use `incrWalletNonce` if using this nonce to send a transaction.
172+
* Use `acquireNonce` to fetch a nonce for sending a transaction.
170173
* @param chainId
171174
* @param walletAddress
172175
* @returns number
@@ -178,7 +181,7 @@ export const inspectNonce = async (chainId: number, walletAddress: Address) => {
178181
};
179182

180183
/**
181-
* Delete all wallet nonces. Useful when the get out of sync.
184+
* Delete all wallet nonces. Useful when they get out of sync.
182185
*/
183186
export const deleteAllNonces = async () => {
184187
const keys = [

src/worker/tasks/cancelRecycledNoncesWorker.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,16 @@ const handler: Processor<any, void, string> = async (job: Job<string>) => {
2929
const keys = await redis.keys("nonce-recycled:*");
3030

3131
for (const key of keys) {
32-
const { chainId, walletAddress } = fromUnusedNoncesKey(key);
32+
const { chainId, walletAddress } = fromRecycledNoncesKey(key);
3333

34-
const unusedNonces = await getAndDeleteUnusedNonces(key);
35-
job.log(`Found unused nonces: key=${key} nonces=${unusedNonces}`);
34+
const recycledNonces = await getAndDeleteRecycledNonces(key);
35+
job.log(`Found recycled nonces: key=${key} nonces=${recycledNonces}`);
3636

37-
if (unusedNonces.length > 0) {
37+
if (recycledNonces.length > 0) {
3838
const success: number[] = [];
3939
const fail: number[] = [];
4040
const ignore: number[] = [];
41-
for (const nonce of unusedNonces) {
41+
for (const nonce of recycledNonces) {
4242
try {
4343
await sendCancellationTransaction({
4444
chainId,
@@ -65,28 +65,29 @@ const handler: Processor<any, void, string> = async (job: Job<string>) => {
6565
}
6666
};
6767

68-
const fromUnusedNoncesKey = (key: string) => {
68+
const fromRecycledNoncesKey = (key: string) => {
6969
const [_, chainId, walletAddress] = key.split(":");
7070
return {
7171
chainId: parseInt(chainId),
7272
walletAddress: walletAddress as Address,
7373
};
7474
};
7575

76-
const getAndDeleteUnusedNonces = async (key: string) => {
77-
// Returns all unused nonces for this key and deletes the key.
76+
const getAndDeleteRecycledNonces = async (key: string) => {
77+
// Returns all recycled nonces for this key and deletes the key.
7878
// Example response:
7979
// [
8080
// [ null, [ '1', '2', '3', '4' ] ],
8181
// [ null, 1 ]
8282
// ]
83-
const multiResult = await redis.multi().smembers(key).del(key).exec();
83+
const multiResult = await redis.multi().zrange(key, 0, -1).del(key).exec();
8484
if (!multiResult) {
8585
throw new Error(`Error getting members of ${key}.`);
8686
}
8787
const [error, nonces] = multiResult[0];
8888
if (error) {
8989
throw new Error(`Error getting members of ${key}: ${error}`);
9090
}
91-
return (nonces as string[]).map((v) => parseInt(v)).sort();
91+
// No need to sort here as ZRANGE returns elements in ascending order
92+
return (nonces as string[]).map((v) => parseInt(v));
9293
};

0 commit comments

Comments
 (0)