Skip to content

Commit 862b36c

Browse files
authored
fix: revive dropped mempool rebroadcasts (#1823)
* fix: revive mempool txs that get re-broadcasted after being dropped from the mempool * chore: separate query for reviving mempool txs * test: add test for reviving mempool txs * chore: fix test * chore: fix test * fix: update mempool tx receipt info on reviving
1 parent f974dfd commit 862b36c

File tree

5 files changed

+250
-2
lines changed

5 files changed

+250
-2
lines changed

src/datastore/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,6 +1548,7 @@ export interface DbChainTip {
15481548
microblock_count: number;
15491549
tx_count: number;
15501550
tx_count_unanchored: number;
1551+
mempool_tx_count: number;
15511552
}
15521553

15531554
export enum IndexesState {

src/datastore/pg-store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ export class PgStore extends BasePgStore {
218218
microblock_count: tip?.microblock_count ?? 0,
219219
tx_count: tip?.tx_count ?? 0,
220220
tx_count_unanchored: tip?.tx_count_unanchored ?? 0,
221+
mempool_tx_count: tip?.mempool_tx_count ?? 0,
221222
};
222223
}
223224

src/datastore/pg-write-store.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,6 +1644,27 @@ export class PgWriteStore extends PgStore {
16441644
tenure_change_signature: tx.tenure_change_signature ?? null,
16451645
tenure_change_signers: tx.tenure_change_signers ?? null,
16461646
}));
1647+
1648+
// Revive mempool txs that were previously dropped
1649+
const revivedTxs = await sql<{ tx_id: string }[]>`
1650+
UPDATE mempool_txs
1651+
SET pruned = false,
1652+
status = ${DbTxStatus.Pending},
1653+
receipt_block_height = ${values[0].receipt_block_height},
1654+
receipt_time = ${values[0].receipt_time}
1655+
WHERE tx_id IN ${sql(values.map(v => v.tx_id))}
1656+
AND pruned = true
1657+
AND NOT EXISTS (
1658+
SELECT 1
1659+
FROM txs
1660+
WHERE txs.tx_id = mempool_txs.tx_id
1661+
AND txs.canonical = true
1662+
AND txs.microblock_canonical = true
1663+
)
1664+
RETURNING tx_id
1665+
`;
1666+
txIds.push(...revivedTxs.map(r => r.tx_id));
1667+
16471668
const result = await sql<{ tx_id: string }[]>`
16481669
WITH inserted AS (
16491670
INSERT INTO mempool_txs ${sql(values)}
@@ -1652,7 +1673,9 @@ export class PgWriteStore extends PgStore {
16521673
),
16531674
count_update AS (
16541675
UPDATE chain_tip SET
1655-
mempool_tx_count = mempool_tx_count + (SELECT COUNT(*) FROM inserted),
1676+
mempool_tx_count = mempool_tx_count
1677+
+ (SELECT COUNT(*) FROM inserted)
1678+
+ ${revivedTxs.count},
16561679
mempool_updated_at = NOW()
16571680
)
16581681
SELECT tx_id FROM inserted
@@ -2329,7 +2352,7 @@ export class PgWriteStore extends PgStore {
23292352
const updatedRows = await sql<{ tx_id: string }[]>`
23302353
WITH restored AS (
23312354
UPDATE mempool_txs
2332-
SET pruned = FALSE
2355+
SET pruned = FALSE, status = ${DbTxStatus.Pending}
23332356
WHERE tx_id IN ${sql(txIds)} AND pruned = TRUE
23342357
RETURNING tx_id
23352358
),

src/tests/datastore-tests.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4108,6 +4108,7 @@ describe('postgres datastore', () => {
41084108
index_block_hash: '0xcc',
41094109
burn_block_height: 123,
41104110
block_count: 3,
4111+
mempool_tx_count: 0,
41114112
microblock_count: 0,
41124113
microblock_hash: undefined,
41134114
microblock_sequence: undefined,
@@ -4180,6 +4181,7 @@ describe('postgres datastore', () => {
41804181
microblock_sequence: undefined,
41814182
tx_count: 2,
41824183
tx_count_unanchored: 2,
4184+
mempool_tx_count: 0,
41834185
});
41844186

41854187
const block4b: DbBlock = {
@@ -4230,6 +4232,7 @@ describe('postgres datastore', () => {
42304232
microblock_sequence: undefined,
42314233
tx_count: 2, // Tx from block 2b now counts, but compensates with tx from block 2
42324234
tx_count_unanchored: 2,
4235+
mempool_tx_count: 1,
42334236
});
42344237

42354238
const b1 = await db.getBlock({ hash: block1.block_hash });

src/tests/mempool-tests.ts

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,6 +1726,226 @@ describe('mempool tests', () => {
17261726
expect(txResult2.body.tx_status).toBe('success');
17271727
});
17281728

1729+
test('Revive dropped and rebroadcasted mempool tx', async () => {
1730+
const senderAddress = 'SP25YGP221F01S9SSCGN114MKDAK9VRK8P3KXGEMB';
1731+
const txId = '0x521234';
1732+
const dbBlock1: DbBlock = {
1733+
block_hash: '0x0123',
1734+
index_block_hash: '0x1234',
1735+
parent_index_block_hash: '0x5678',
1736+
parent_block_hash: '0x5678',
1737+
parent_microblock_hash: '0x00',
1738+
parent_microblock_sequence: 0,
1739+
block_height: 1,
1740+
burn_block_time: 39486,
1741+
burn_block_hash: '0x1234',
1742+
burn_block_height: 123,
1743+
miner_txid: '0x4321',
1744+
canonical: true,
1745+
execution_cost_read_count: 0,
1746+
execution_cost_read_length: 0,
1747+
execution_cost_runtime: 0,
1748+
execution_cost_write_count: 0,
1749+
execution_cost_write_length: 0,
1750+
tx_count: 1,
1751+
};
1752+
const dbBlock1b: DbBlock = {
1753+
block_hash: '0x0123bb',
1754+
index_block_hash: '0x1234bb',
1755+
parent_index_block_hash: '0x5678bb',
1756+
parent_block_hash: '0x5678bb',
1757+
parent_microblock_hash: '0x00',
1758+
parent_microblock_sequence: 0,
1759+
block_height: 1,
1760+
burn_block_time: 39486,
1761+
burn_block_hash: '0x1234bb',
1762+
burn_block_height: 123,
1763+
miner_txid: '0x4321bb',
1764+
canonical: true,
1765+
execution_cost_read_count: 0,
1766+
execution_cost_read_length: 0,
1767+
execution_cost_runtime: 0,
1768+
execution_cost_write_count: 0,
1769+
execution_cost_write_length: 0,
1770+
tx_count: 1,
1771+
};
1772+
const dbBlock2b: DbBlock = {
1773+
block_hash: '0x2123',
1774+
index_block_hash: '0x2234',
1775+
parent_index_block_hash: dbBlock1b.index_block_hash,
1776+
parent_block_hash: dbBlock1b.block_hash,
1777+
parent_microblock_hash: '0x00',
1778+
parent_microblock_sequence: 0,
1779+
block_height: 2,
1780+
burn_block_time: 39486,
1781+
burn_block_hash: '0x1234',
1782+
burn_block_height: 123,
1783+
miner_txid: '0x4321',
1784+
canonical: true,
1785+
execution_cost_read_count: 0,
1786+
execution_cost_read_length: 0,
1787+
execution_cost_runtime: 0,
1788+
execution_cost_write_count: 0,
1789+
execution_cost_write_length: 0,
1790+
tx_count: 1,
1791+
};
1792+
const mempoolTx: DbMempoolTxRaw = {
1793+
tx_id: txId,
1794+
anchor_mode: 3,
1795+
nonce: 0,
1796+
raw_tx: bufferToHex(Buffer.from('test-raw-mempool-tx')),
1797+
type_id: DbTxTypeId.Coinbase,
1798+
status: 1,
1799+
post_conditions: '0x01f5',
1800+
fee_rate: 1234n,
1801+
sponsored: false,
1802+
sponsor_address: undefined,
1803+
sender_address: senderAddress,
1804+
origin_hash_mode: 1,
1805+
coinbase_payload: bufferToHex(Buffer.from('hi')),
1806+
pruned: false,
1807+
receipt_time: 1616063078,
1808+
};
1809+
const dbTx1: DbTxRaw = {
1810+
...mempoolTx,
1811+
...dbBlock1,
1812+
parent_burn_block_time: 1626122935,
1813+
tx_index: 4,
1814+
status: DbTxStatus.Success,
1815+
raw_result: '0x0100000000000000000000000000000001', // u1
1816+
canonical: true,
1817+
microblock_canonical: true,
1818+
microblock_sequence: I32_MAX,
1819+
microblock_hash: '',
1820+
parent_index_block_hash: '',
1821+
event_count: 0,
1822+
execution_cost_read_count: 0,
1823+
execution_cost_read_length: 0,
1824+
execution_cost_runtime: 0,
1825+
execution_cost_write_count: 0,
1826+
execution_cost_write_length: 0,
1827+
};
1828+
1829+
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
1830+
1831+
let chainTip = await db.getChainTip();
1832+
expect(chainTip.mempool_tx_count).toBe(1);
1833+
1834+
// Verify tx shows up in mempool (non-pruned)
1835+
const mempoolResult1 = await supertest(api.server).get(
1836+
`/extended/v1/address/${mempoolTx.sender_address}/mempool`
1837+
);
1838+
expect(mempoolResult1.body.results[0].tx_id).toBe(txId);
1839+
const mempoolCount1 = await supertest(api.server).get(`/extended/v1/tx/mempool`);
1840+
expect(mempoolCount1.body.total).toBe(1);
1841+
1842+
// Drop mempool tx
1843+
await db.dropMempoolTxs({
1844+
status: DbTxStatus.DroppedStaleGarbageCollect,
1845+
txIds: [mempoolTx.tx_id],
1846+
});
1847+
1848+
// Verify tx is pruned from mempool
1849+
const mempoolResult2 = await supertest(api.server).get(
1850+
`/extended/v1/address/${mempoolTx.sender_address}/mempool`
1851+
);
1852+
expect(mempoolResult2.body.results).toHaveLength(0);
1853+
const mempoolCount2 = await supertest(api.server).get(`/extended/v1/tx/mempool`);
1854+
expect(mempoolCount2.body.total).toBe(0);
1855+
chainTip = await db.getChainTip();
1856+
expect(chainTip.mempool_tx_count).toBe(0);
1857+
1858+
// Re-broadcast mempool tx
1859+
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
1860+
1861+
// Verify tx shows up in mempool again (revived)
1862+
const mempoolResult3 = await supertest(api.server).get(
1863+
`/extended/v1/address/${mempoolTx.sender_address}/mempool`
1864+
);
1865+
expect(mempoolResult3.body.results[0].tx_id).toBe(txId);
1866+
const mempoolCount3 = await supertest(api.server).get(`/extended/v1/tx/mempool`);
1867+
expect(mempoolCount3.body.total).toBe(1);
1868+
chainTip = await db.getChainTip();
1869+
expect(chainTip.mempool_tx_count).toBe(1);
1870+
1871+
// Mine tx in block to prune from mempool
1872+
await db.update({
1873+
block: dbBlock1,
1874+
microblocks: [],
1875+
minerRewards: [],
1876+
txs: [
1877+
{
1878+
tx: dbTx1,
1879+
stxEvents: [],
1880+
stxLockEvents: [],
1881+
ftEvents: [],
1882+
nftEvents: [],
1883+
contractLogEvents: [],
1884+
smartContracts: [],
1885+
names: [],
1886+
namespaces: [],
1887+
pox2Events: [],
1888+
pox3Events: [],
1889+
pox4Events: [],
1890+
},
1891+
],
1892+
});
1893+
1894+
// Verify tx is pruned from mempool
1895+
const mempoolResult4 = await supertest(api.server).get(
1896+
`/extended/v1/address/${mempoolTx.sender_address}/mempool`
1897+
);
1898+
expect(mempoolResult4.body.results).toHaveLength(0);
1899+
const mempoolCount4 = await supertest(api.server).get(`/extended/v1/tx/mempool`);
1900+
expect(mempoolCount4.body.total).toBe(0);
1901+
chainTip = await db.getChainTip();
1902+
expect(chainTip.mempool_tx_count).toBe(0);
1903+
1904+
// Verify tx is mined
1905+
const txResult1 = await supertest(api.server).get(`/extended/v1/tx/${txId}`);
1906+
expect(txResult1.body.tx_status).toBe('success');
1907+
expect(txResult1.body.canonical).toBe(true);
1908+
1909+
// Orphan the block to get the tx orphaned and placed back in the pool
1910+
await db.update({
1911+
block: dbBlock1b,
1912+
microblocks: [],
1913+
minerRewards: [],
1914+
txs: [],
1915+
});
1916+
await db.update({
1917+
block: dbBlock2b,
1918+
microblocks: [],
1919+
minerRewards: [],
1920+
txs: [],
1921+
});
1922+
1923+
// Verify tx is orphaned and back in mempool
1924+
const txResult2 = await supertest(api.server).get(`/extended/v1/tx/${txId}`);
1925+
expect(txResult2.body.canonical).toBeFalsy();
1926+
1927+
// Verify tx has been revived and is back in the mempool
1928+
const mempoolResult5 = await supertest(api.server).get(
1929+
`/extended/v1/address/${mempoolTx.sender_address}/mempool`
1930+
);
1931+
expect(mempoolResult5.body.results[0].tx_id).toBe(txId);
1932+
const mempoolCount5 = await supertest(api.server).get(`/extended/v1/tx/mempool`);
1933+
expect(mempoolCount5.body.total).toBe(1);
1934+
chainTip = await db.getChainTip();
1935+
expect(chainTip.mempool_tx_count).toBe(1);
1936+
1937+
// Re-broadcast mempool tx
1938+
await db.updateMempoolTxs({ mempoolTxs: [mempoolTx] });
1939+
1940+
// Verify tx has been revived and is back in the mempool
1941+
const mempoolResult6 = await supertest(api.server).get(
1942+
`/extended/v1/address/${mempoolTx.sender_address}/mempool`
1943+
);
1944+
expect(mempoolResult6.body.results[0].tx_id).toBe(txId);
1945+
const mempoolCount6 = await supertest(api.server).get(`/extended/v1/tx/mempool`);
1946+
expect(mempoolCount6.body.total).toBe(1);
1947+
});
1948+
17291949
test('returns fee priorities for mempool transactions', async () => {
17301950
const mempoolTxs: DbMempoolTxRaw[] = [];
17311951
for (let i = 0; i < 10; i++) {

0 commit comments

Comments
 (0)