Skip to content

Commit 8b10b69

Browse files
authored
fix: update mempool garbage collection logic for 3.0 (#2117)
* fix: update mempool garbage collection logic for 3.0 * fix: undo vscode * fix: mempool test
1 parent d6ab738 commit 8b10b69

File tree

5 files changed

+90
-63
lines changed

5 files changed

+90
-63
lines changed

.env

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,6 @@ PG_APPLICATION_NAME=stacks-blockchain-api
6060
# (with both Event Server and API endpoints).
6161
# STACKS_API_MODE=
6262

63-
# Stacks nodes automatically perform garbage-collection by dropping transactions from the mempool if they
64-
# are pending for more than 256 blocks. This variable controls the block age threshold at which the API will do
65-
# the same.
66-
# STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD=256
67-
6863
# To avoid running unnecessary mempool stats during transaction influx, we use a debounce mechanism for the process.
6964
# This variable controls the duration it waits until there are no further mempool updates
7065
# MEMPOOL_STATS_DEBOUNCE_INTERVAL=1000

src/datastore/pg-write-store.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2768,21 +2768,35 @@ export class PgWriteStore extends PgStore {
27682768
}
27692769

27702770
/**
2771-
* Deletes mempool txs older than `STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD` blocks (default 256).
2771+
* Deletes mempool txs that should be dropped by block age or time age depending on which Stacks
2772+
* epoch we're on.
27722773
* @param sql - DB client
27732774
* @returns List of deleted `tx_id`s
27742775
*/
27752776
async deleteGarbageCollectedMempoolTxs(sql: PgSqlClient): Promise<{ deletedTxs: string[] }> {
2776-
const blockThreshold = parseInt(
2777-
process.env['STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD'] ?? '256'
2778-
);
2779-
// TODO: Use DELETE instead of UPDATE once we implement a non-archival API replay mode.
2777+
// Is 3.0 active? Check if the latest block was signed by signers.
2778+
const nakamotoActive =
2779+
(
2780+
await sql<{ index_block_hash: string }[]>`
2781+
SELECT b.index_block_hash
2782+
FROM blocks AS b
2783+
INNER JOIN chain_tip AS c ON c.index_block_hash = b.index_block_hash
2784+
WHERE b.signer_bitvec IS NOT NULL
2785+
LIMIT 1
2786+
`
2787+
).count > 0;
2788+
// If 3.0 is active, drop transactions older than 2560 minutes.
2789+
// If 2.5 or earlier is active, drop transactions older than 256 blocks.
27802790
const deletedTxResults = await sql<{ tx_id: string }[]>`
27812791
WITH pruned AS (
27822792
UPDATE mempool_txs
27832793
SET pruned = TRUE, status = ${DbTxStatus.DroppedApiGarbageCollect}
2784-
WHERE pruned = FALSE
2785-
AND receipt_block_height <= (SELECT block_height - ${blockThreshold} FROM chain_tip)
2794+
WHERE pruned = FALSE AND
2795+
${
2796+
nakamotoActive
2797+
? sql`receipt_time <= EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - INTERVAL '2560 minutes'))::int`
2798+
: sql`receipt_block_height <= (SELECT block_height - 256 FROM chain_tip)`
2799+
}
27862800
RETURNING tx_id
27872801
),
27882802
count_update AS (

tests/api/mempool.test.ts

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -42,41 +42,74 @@ describe('mempool tests', () => {
4242
await migrate('down');
4343
});
4444

45-
test('garbage collection', async () => {
46-
const garbageThresholdOrig = process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD;
47-
process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD = '2';
48-
try {
49-
// Insert 5 blocks with 1 mempool tx each.
50-
for (let block_height = 1; block_height <= 5; block_height++) {
51-
const block = new TestBlockBuilder({
52-
block_height: block_height,
53-
index_block_hash: `0x0${block_height}`,
54-
parent_index_block_hash: `0x0${block_height - 1}`,
55-
})
56-
.addTx({ tx_id: `0x111${block_height}`, nonce: block_height })
57-
.build();
58-
await db.update(block);
59-
const mempoolTx = testMempoolTx({ tx_id: `0x0${block_height}` });
60-
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
61-
}
45+
test('garbage collection pre 3.0', async () => {
46+
const hexFromHeight = (height: number) => {
47+
const hex = height.toString(16);
48+
return hex.length % 2 == 1 ? `0${hex}` : hex;
49+
};
50+
// Insert more than 256 blocks with 1 mempool tx each.
51+
for (let block_height = 1; block_height <= 259; block_height++) {
52+
const block = new TestBlockBuilder({
53+
block_height: block_height,
54+
index_block_hash: `0x${hexFromHeight(block_height)}`,
55+
parent_index_block_hash: `0x${hexFromHeight(block_height - 1)}`,
56+
})
57+
.addTx({ tx_id: `0x11${hexFromHeight(block_height)}`, nonce: block_height })
58+
.build();
59+
await db.update(block);
60+
const mempoolTx = testMempoolTx({ tx_id: `0x${hexFromHeight(block_height)}` });
61+
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
62+
}
63+
await db.update(
64+
new TestBlockBuilder({
65+
block_height: 260,
66+
index_block_hash: `0xff`,
67+
parent_index_block_hash: `0x0103`,
68+
}).build()
69+
);
6270

63-
// Make sure we only have mempool txs for block_height >= 3
64-
const mempoolTxResult = await db.getMempoolTxList({
65-
limit: 10,
66-
offset: 0,
67-
includeUnanchored: false,
68-
});
69-
const mempoolTxs = mempoolTxResult.results;
70-
expect(mempoolTxs.length).toEqual(3);
71-
const txIds = mempoolTxs.map(e => e.tx_id).sort();
72-
expect(txIds).toEqual(['0x03', '0x04', '0x05']);
73-
} finally {
74-
if (typeof garbageThresholdOrig === 'undefined') {
75-
delete process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD;
76-
} else {
77-
process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD = garbageThresholdOrig;
78-
}
71+
// Make sure we only have mempool txs for block_height >= 3
72+
const mempoolTxResult = await db.getMempoolTxList({
73+
limit: 10,
74+
offset: 0,
75+
includeUnanchored: false,
76+
});
77+
expect(mempoolTxResult.total).toEqual(257);
78+
});
79+
80+
test('garbage collection post 3.0', async () => {
81+
// Insert 3 txs spaced out so garbage collection kicks in.
82+
for (let block_height = 1; block_height <= 3; block_height++) {
83+
const block = new TestBlockBuilder({
84+
block_height: block_height,
85+
index_block_hash: `0x0${block_height}`,
86+
parent_index_block_hash: `0x0${block_height - 1}`,
87+
signer_bitvec: '1111',
88+
})
89+
.addTx({ tx_id: `0x111${block_height}`, nonce: block_height })
90+
.build();
91+
await db.update(block);
92+
const mempoolTx = testMempoolTx({ tx_id: `0x0${block_height}`, receipt_time: 1 });
93+
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
7994
}
95+
96+
const mempoolTx = testMempoolTx({ tx_id: `0x0fff` });
97+
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
98+
await db.update(
99+
new TestBlockBuilder({
100+
block_height: 4,
101+
index_block_hash: `0xff`,
102+
parent_index_block_hash: `0x03`,
103+
}).build()
104+
);
105+
106+
// Make sure we only have the latest mempool tx
107+
const mempoolTxResult = await db.getMempoolTxList({
108+
limit: 10,
109+
offset: 0,
110+
includeUnanchored: false,
111+
});
112+
expect(mempoolTxResult.total).toEqual(1);
80113
});
81114

82115
test('mempool stats', async () => {

tests/api/socket-io.test.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -230,19 +230,15 @@ describe('socket-io', () => {
230230
});
231231

232232
test('socket-io > mempool txs', async () => {
233-
process.env.STACKS_MEMPOOL_TX_GARBAGE_COLLECTION_THRESHOLD = '0';
234-
235233
const address = apiServer.address;
236234
const socket = io(`http://${address}`, {
237235
reconnection: false,
238236
query: { subscriptions: 'mempool' },
239237
});
240-
const txWaiters: Waiter<MempoolTransaction | Transaction>[] = [waiter(), waiter()];
238+
const txWaiters: Waiter<MempoolTransaction>[] = [waiter()];
241239
socket.on('mempool', tx => {
242240
if (tx.tx_status === 'pending') {
243241
txWaiters[0].finish(tx);
244-
} else {
245-
txWaiters[1].finish(tx);
246242
}
247243
});
248244

@@ -258,21 +254,9 @@ describe('socket-io', () => {
258254
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
259255
const pendingResult = await txWaiters[0];
260256

261-
const block2 = new TestBlockBuilder({
262-
block_height: 2,
263-
index_block_hash: '0x02',
264-
parent_index_block_hash: '0x01',
265-
})
266-
.addTx({ tx_id: '0x0201' })
267-
.build();
268-
await db.update(block2);
269-
const droppedResult = await txWaiters[1];
270-
271257
try {
272258
expect(pendingResult.tx_id).toEqual('0x01');
273259
expect(pendingResult.tx_status).toEqual('pending');
274-
expect(droppedResult.tx_id).toEqual('0x01');
275-
expect(droppedResult.tx_status).toEqual('dropped_stale_garbage_collect');
276260
} finally {
277261
socket.emit('unsubscribe', 'mempool');
278262
socket.close();

tests/utils/test-builders.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export interface TestBlockArgs {
9898
parent_microblock_hash?: string;
9999
parent_microblock_sequence?: number;
100100
canonical?: boolean;
101+
signer_bitvec?: string;
101102
}
102103

103104
/**
@@ -126,7 +127,7 @@ function testBlock(args?: TestBlockArgs): DbBlock {
126127
execution_cost_write_count: 0,
127128
execution_cost_write_length: 0,
128129
tx_count: 1,
129-
signer_bitvec: null,
130+
signer_bitvec: args?.signer_bitvec ?? null,
130131
};
131132
}
132133

0 commit comments

Comments
 (0)