1
1
import * as assert from 'assert' ;
2
- import {
3
- getOrAdd ,
4
- I32_MAX ,
5
- getIbdBlockHeight ,
6
- getUintEnvOrDefault ,
7
- unwrapOptionalProp ,
8
- } from '../helpers' ;
2
+ import { getOrAdd , I32_MAX , getIbdBlockHeight , getUintEnvOrDefault } from '../helpers' ;
9
3
import {
10
4
DbBlock ,
11
5
DbTx ,
@@ -92,7 +86,6 @@ import { parseResolver, parseZoneFileTxt } from '../event-stream/bns/bns-helpers
92
86
import { SyntheticPoxEventName } from '../pox-helpers' ;
93
87
import { logger } from '../logger' ;
94
88
import {
95
- PgJsonb ,
96
89
PgSqlClient ,
97
90
batchIterate ,
98
91
connectPostgres ,
@@ -122,6 +115,8 @@ class MicroblockGapError extends Error {
122
115
}
123
116
}
124
117
118
+ type TransactionHeader = { txId : string ; sender : string ; nonce : number } ;
119
+
125
120
/**
126
121
* Extends `PgStore` to provide data insertion functions. These added features are usually called by
127
122
* the `EventServer` upon receiving blockchain events from a Stacks node. It also deals with chain data
@@ -208,8 +203,12 @@ export class PgWriteStore extends PgStore {
208
203
if ( ! isCanonical ) {
209
204
markBlockUpdateDataAsNonCanonical ( data ) ;
210
205
} else {
211
- const txIds = data . txs . map ( d => d . tx . tx_id ) ;
212
- await this . pruneMempoolTxs ( sql , txIds ) ;
206
+ const prunableTxs : TransactionHeader [ ] = data . txs . map ( d => ( {
207
+ txId : d . tx . tx_id ,
208
+ sender : d . tx . sender_address ,
209
+ nonce : d . tx . nonce ,
210
+ } ) ) ;
211
+ await this . pruneMempoolTxs ( sql , prunableTxs ) ;
213
212
}
214
213
setTotalBlockUpdateDataExecutionCost ( data ) ;
215
214
@@ -245,7 +244,11 @@ export class PgWriteStore extends PgStore {
245
244
) ;
246
245
const restoredMempoolTxs = await this . restoreMempoolTxs (
247
246
sql ,
248
- orphanedAndMissingTxs . map ( tx => tx . tx_id )
247
+ orphanedAndMissingTxs . map ( tx => ( {
248
+ txId : tx . tx_id ,
249
+ sender : tx . sender_address ,
250
+ nonce : tx . nonce ,
251
+ } ) )
249
252
) ;
250
253
restoredMempoolTxs . restoredTxs . forEach ( txId => {
251
254
logger . info ( `Restored micro-orphaned tx to mempool ${ txId } ` ) ;
@@ -688,15 +691,23 @@ export class PgWriteStore extends PgStore {
688
691
// Restore any micro-orphaned txs into the mempool
689
692
const restoredMempoolTxs = await this . restoreMempoolTxs (
690
693
sql ,
691
- microOrphanedTxs . map ( tx => tx . tx_id )
694
+ microOrphanedTxs . map ( tx => ( {
695
+ txId : tx . tx_id ,
696
+ sender : tx . sender_address ,
697
+ nonce : tx . nonce ,
698
+ } ) )
692
699
) ;
693
700
restoredMempoolTxs . restoredTxs . forEach ( txId => {
694
701
logger . info ( `Restored micro-orphaned tx to mempool ${ txId } ` ) ;
695
702
} ) ;
696
703
}
697
704
698
- const candidateTxIds = data . txs . map ( d => d . tx . tx_id ) ;
699
- const removedTxsResult = await this . pruneMempoolTxs ( sql , candidateTxIds ) ;
705
+ const prunableTxs : TransactionHeader [ ] = data . txs . map ( d => ( {
706
+ txId : d . tx . tx_id ,
707
+ sender : d . tx . sender_address ,
708
+ nonce : d . tx . nonce ,
709
+ } ) ) ;
710
+ const removedTxsResult = await this . pruneMempoolTxs ( sql , prunableTxs ) ;
700
711
if ( removedTxsResult . removedTxs . length > 0 ) {
701
712
logger . debug (
702
713
`Removed ${ removedTxsResult . removedTxs . length } microblock-txs from mempool table during microblock ingestion`
@@ -2471,9 +2482,9 @@ export class PgWriteStore extends PgStore {
2471
2482
` ;
2472
2483
// Any txs restored need to be pruned from the mempool
2473
2484
const updatedMbTxs = updatedMbTxsQuery . map ( r => parseTxQueryResult ( r ) ) ;
2474
- const txsToPrune = updatedMbTxs
2485
+ const txsToPrune : TransactionHeader [ ] = updatedMbTxs
2475
2486
. filter ( tx => tx . canonical && tx . microblock_canonical )
2476
- . map ( tx => tx . tx_id ) ;
2487
+ . map ( tx => ( { txId : tx . tx_id , sender : tx . sender_address , nonce : tx . nonce } ) ) ;
2477
2488
const removedTxsResult = await this . pruneMempoolTxs ( sql , txsToPrune ) ;
2478
2489
if ( removedTxsResult . removedTxs . length > 0 ) {
2479
2490
logger . debug (
@@ -2648,17 +2659,31 @@ export class PgWriteStore extends PgStore {
2648
2659
* marked from canonical to non-canonical.
2649
2660
* @param txIds - List of transactions to update in the mempool
2650
2661
*/
2651
- async restoreMempoolTxs ( sql : PgSqlClient , txIds : string [ ] ) : Promise < { restoredTxs : string [ ] } > {
2652
- if ( txIds . length === 0 ) return { restoredTxs : [ ] } ;
2653
- for ( const txId of txIds ) {
2654
- logger . debug ( `Restoring mempool tx: ${ txId } ` ) ;
2655
- }
2656
-
2662
+ async restoreMempoolTxs (
2663
+ sql : PgSqlClient ,
2664
+ transactions : TransactionHeader [ ]
2665
+ ) : Promise < { restoredTxs : string [ ] } > {
2666
+ if ( transactions . length === 0 ) return { restoredTxs : [ ] } ;
2667
+ if ( logger . isLevelEnabled ( 'debug' ) )
2668
+ for ( const tx of transactions )
2669
+ logger . debug ( `Restoring mempool tx: ${ tx . txId } sender: ${ tx . sender } nonce: ${ tx . nonce } ` ) ;
2670
+
2671
+ // Also restore transactions for the same `sender_address` with the same `nonce`.
2672
+ const inputData = transactions . map ( t => [ t . txId . replace ( '0x' , '\\x' ) , t . sender , t . nonce ] ) ;
2657
2673
const updatedRows = await sql < { tx_id : string } [ ] > `
2658
- WITH restored AS (
2674
+ WITH input_data (tx_id, sender_address, nonce) AS (VALUES ${ sql ( inputData ) } ),
2675
+ affected_mempool_tx_ids AS (
2676
+ SELECT m.tx_id
2677
+ FROM mempool_txs AS m
2678
+ INNER JOIN input_data AS i
2679
+ ON m.sender_address = i.sender_address AND m.nonce = i.nonce::int
2680
+ UNION
2681
+ SELECT tx_id::bytea FROM input_data
2682
+ ),
2683
+ restored AS (
2659
2684
UPDATE mempool_txs
2660
- SET pruned = FALSE , status = ${ DbTxStatus . Pending }
2661
- WHERE tx_id IN ${ sql ( txIds ) } AND pruned = TRUE
2685
+ SET pruned = false , status = ${ DbTxStatus . Pending }
2686
+ WHERE pruned = true AND tx_id IN (SELECT DISTINCT tx_id FROM affected_mempool_tx_ids)
2662
2687
RETURNING tx_id
2663
2688
),
2664
2689
count_update AS (
@@ -2668,60 +2693,67 @@ export class PgWriteStore extends PgStore {
2668
2693
)
2669
2694
SELECT tx_id FROM restored
2670
2695
` ;
2671
-
2672
- const updatedTxs = updatedRows . map ( r => r . tx_id ) ;
2673
- for ( const tx of updatedTxs ) {
2674
- logger . debug ( `Updated mempool tx: ${ tx } ` ) ;
2675
- }
2676
-
2677
- let restoredTxs = updatedRows . map ( r => r . tx_id ) ;
2678
-
2679
- // txs that didnt exist in the mempool need to be inserted into the mempool
2680
- if ( updatedRows . length < txIds . length ) {
2681
- const txsRequiringInsertion = txIds . filter ( txId => ! updatedTxs . includes ( txId ) ) ;
2682
-
2683
- logger . debug ( `To restore mempool txs, ${ txsRequiringInsertion . length } txs require insertion` ) ;
2684
-
2696
+ const restoredTxIds = updatedRows . map ( r => r . tx_id ) ;
2697
+ if ( logger . isLevelEnabled ( 'debug' ) )
2698
+ for ( const txId of restoredTxIds ) logger . debug ( `Restored mempool tx: ${ txId } ` ) ;
2699
+
2700
+ // Transactions that didn't exist in the mempool need to be inserted into the mempool
2701
+ const txIdsRequiringInsertion = transactions
2702
+ . filter ( tx => ! restoredTxIds . includes ( tx . txId ) )
2703
+ . map ( tx => tx . txId ) ;
2704
+ if ( txIdsRequiringInsertion . length ) {
2705
+ logger . debug (
2706
+ `To restore mempool txs, ${ txIdsRequiringInsertion . length } txs require insertion`
2707
+ ) ;
2685
2708
const txs : TxQueryResult [ ] = await sql `
2686
2709
SELECT DISTINCT ON(tx_id) ${ sql ( TX_COLUMNS ) }
2687
2710
FROM txs
2688
- WHERE tx_id IN ${ sql ( txsRequiringInsertion ) }
2711
+ WHERE tx_id IN ${ sql ( txIdsRequiringInsertion ) }
2689
2712
ORDER BY tx_id, block_height DESC, microblock_sequence DESC, tx_index DESC
2690
2713
` ;
2691
-
2692
- if ( txs . length !== txsRequiringInsertion . length ) {
2714
+ if ( txs . length !== txIdsRequiringInsertion . length ) {
2693
2715
logger . error ( `Not all txs requiring insertion were found` ) ;
2694
2716
}
2695
2717
2696
2718
const mempoolTxs = convertTxQueryResultToDbMempoolTx ( txs ) ;
2697
-
2698
2719
await this . updateMempoolTxs ( { mempoolTxs } ) ;
2699
-
2700
- restoredTxs = [ ...restoredTxs , ...txsRequiringInsertion ] ;
2701
-
2702
- for ( const tx of mempoolTxs ) {
2703
- logger . debug ( `Inserted mempool tx: ${ tx . tx_id } ` ) ;
2704
- }
2720
+ if ( logger . isLevelEnabled ( 'debug' ) )
2721
+ for ( const tx of mempoolTxs ) logger . debug ( `Inserted non-existing mempool tx: ${ tx . tx_id } ` ) ;
2705
2722
}
2706
2723
2707
- return { restoredTxs : restoredTxs } ;
2724
+ return { restoredTxs : [ ... restoredTxIds , ... txIdsRequiringInsertion ] } ;
2708
2725
}
2709
2726
2710
2727
/**
2711
2728
* Remove transactions in the mempool table. This should be called when transactions are
2712
2729
* mined into a block.
2713
2730
* @param txIds - List of transactions to update in the mempool
2714
2731
*/
2715
- async pruneMempoolTxs ( sql : PgSqlClient , txIds : string [ ] ) : Promise < { removedTxs : string [ ] } > {
2716
- if ( txIds . length === 0 ) return { removedTxs : [ ] } ;
2717
- for ( const txId of txIds ) {
2718
- logger . debug ( `Pruning mempool tx: ${ txId } ` ) ;
2719
- }
2732
+ async pruneMempoolTxs (
2733
+ sql : PgSqlClient ,
2734
+ transactions : TransactionHeader [ ]
2735
+ ) : Promise < { removedTxs : string [ ] } > {
2736
+ if ( transactions . length === 0 ) return { removedTxs : [ ] } ;
2737
+ if ( logger . isLevelEnabled ( 'debug' ) )
2738
+ for ( const tx of transactions )
2739
+ logger . debug ( `Pruning mempool tx: ${ tx . txId } sender: ${ tx . sender } nonce: ${ tx . nonce } ` ) ;
2740
+
2741
+ // Also prune transactions for the same `sender_address` with the same `nonce`.
2742
+ const inputData = transactions . map ( t => [ t . txId . replace ( '0x' , '\\x' ) , t . sender , t . nonce ] ) ;
2720
2743
const updateResults = await sql < { tx_id : string } [ ] > `
2721
- WITH pruned AS (
2744
+ WITH input_data (tx_id, sender_address, nonce) AS (VALUES ${ sql ( inputData ) } ),
2745
+ affected_mempool_tx_ids AS (
2746
+ SELECT m.tx_id
2747
+ FROM mempool_txs AS m
2748
+ INNER JOIN input_data AS i
2749
+ ON m.sender_address = i.sender_address AND m.nonce = i.nonce::int
2750
+ UNION
2751
+ SELECT tx_id::bytea FROM input_data
2752
+ ),
2753
+ pruned AS (
2722
2754
UPDATE mempool_txs
2723
2755
SET pruned = true
2724
- WHERE tx_id IN ${ sql ( txIds ) } AND pruned = FALSE
2756
+ WHERE pruned = false AND tx_id IN (SELECT DISTINCT tx_id FROM affected_mempool_tx_ids)
2725
2757
RETURNING tx_id
2726
2758
),
2727
2759
count_update AS (
@@ -2769,20 +2801,28 @@ export class PgWriteStore extends PgStore {
2769
2801
indexBlockHash : string ,
2770
2802
canonical : boolean ,
2771
2803
updatedEntities : ReOrgUpdatedEntities
2772
- ) : Promise < { txsMarkedCanonical : string [ ] ; txsMarkedNonCanonical : string [ ] } > {
2773
- const result : { txsMarkedCanonical : string [ ] ; txsMarkedNonCanonical : string [ ] } = {
2804
+ ) : Promise < {
2805
+ txsMarkedCanonical : TransactionHeader [ ] ;
2806
+ txsMarkedNonCanonical : TransactionHeader [ ] ;
2807
+ } > {
2808
+ const result : {
2809
+ txsMarkedCanonical : TransactionHeader [ ] ;
2810
+ txsMarkedNonCanonical : TransactionHeader [ ] ;
2811
+ } = {
2774
2812
txsMarkedCanonical : [ ] ,
2775
2813
txsMarkedNonCanonical : [ ] ,
2776
2814
} ;
2777
2815
2778
2816
const q = new PgWriteQueue ( ) ;
2779
2817
q . enqueue ( async ( ) => {
2780
- const txResult = await sql < { tx_id : string ; update_balances_count : number } [ ] > `
2818
+ const txResult = await sql <
2819
+ { tx_id : string ; sender_address : string ; nonce : number ; update_balances_count : number } [ ]
2820
+ > `
2781
2821
WITH updated_txs AS (
2782
2822
UPDATE txs
2783
2823
SET canonical = ${ canonical }
2784
2824
WHERE index_block_hash = ${ indexBlockHash } AND canonical != ${ canonical }
2785
- RETURNING tx_id, sender_address, sponsor_address, fee_rate, sponsored, canonical
2825
+ RETURNING tx_id, sender_address, nonce, sponsor_address, fee_rate, sponsored, canonical
2786
2826
),
2787
2827
affected_addresses AS (
2788
2828
SELECT
@@ -2817,22 +2857,26 @@ export class PgWriteStore extends PgStore {
2817
2857
SET balance = ft_balances.balance + EXCLUDED.balance
2818
2858
RETURNING ft_balances.address
2819
2859
)
2820
- SELECT tx_id, (SELECT COUNT(*)::int FROM update_ft_balances) AS update_balances_count
2860
+ SELECT tx_id, sender_address, nonce, (SELECT COUNT(*)::int FROM update_ft_balances) AS update_balances_count
2821
2861
FROM updated_txs
2822
2862
` ;
2823
- const txIds = txResult . map ( row => row . tx_id ) ;
2863
+ const txs = txResult . map ( row => ( {
2864
+ txId : row . tx_id ,
2865
+ sender : row . sender_address ,
2866
+ nonce : row . nonce ,
2867
+ } ) ) ;
2824
2868
if ( canonical ) {
2825
2869
updatedEntities . markedCanonical . txs += txResult . count ;
2826
- result . txsMarkedCanonical = txIds ;
2870
+ result . txsMarkedCanonical = txs ;
2827
2871
} else {
2828
2872
updatedEntities . markedNonCanonical . txs += txResult . count ;
2829
- result . txsMarkedNonCanonical = txIds ;
2873
+ result . txsMarkedNonCanonical = txs ;
2830
2874
}
2831
2875
if ( txResult . count ) {
2832
2876
await sql `
2833
2877
UPDATE principal_stx_txs
2834
2878
SET canonical = ${ canonical }
2835
- WHERE tx_id IN ${ sql ( txIds ) }
2879
+ WHERE tx_id IN ${ sql ( txs . map ( t => t . txId ) ) }
2836
2880
AND index_block_hash = ${ indexBlockHash } AND canonical != ${ canonical }
2837
2881
` ;
2838
2882
}
0 commit comments