1
1
import json
2
+ import multiprocessing
2
3
import random
3
4
import requests
4
5
from concurrent .futures import ThreadPoolExecutor
5
6
6
- from ..errors import NetworkException
7
- from ..generated import wallet_pb2
7
+
8
+ from zpywallet .errors import NetworkException
9
+ from zpywallet .generated import wallet_pb2
10
+
11
+ from zpywallet .mempool .cache import SQLTransactionStorage , DatabaseError
8
12
9
13
10
14
def transform_and_sort_transactions (data ):
@@ -17,11 +21,12 @@ def transform_and_sort_transactions(data):
17
21
return sorted_data
18
22
19
23
20
- class BitcoinMempool :
21
- """Holds the transactions inside the mempool. There should only be one
22
- mempool class for each coin running in the entire application.
24
+ class BTCLikeMempool :
25
+ """Holds the transactions of Bitcoin-like blockchains inside the mempool.
26
+ There should only be one mempool class for each coin running in the entire
27
+ application.
23
28
24
- Note: The performance of this class heavily depends on the network speed and CPU
29
+ The performance of this class heavily depends on the network speed and CPU
25
30
speed of the node as well as the number of threads available, the size of the RPC
26
31
batch work queue specified in the constructor, and the amount of transactions in
27
32
megabytes you are trying to fetch at once.
@@ -59,8 +64,11 @@ def _clean_tx(self, element):
59
64
# Of course this is an unconfirmed transaction.
60
65
# Hence it is also not a coinbase transaction.
61
66
new_element .confirmed = False
67
+ new_element .txid = element ["txid" ]
62
68
63
69
for vin in element ["vin" ]:
70
+ if "txid" not in vin .keys ():
71
+ continue
64
72
txinput = new_element .btclike_transaction .inputs .add ()
65
73
txinput .txid = vin ["txid" ]
66
74
txinput .index = vin ["vout" ]
@@ -82,23 +90,17 @@ def _clean_tx(self, element):
82
90
new_element .btclike_transaction .fee = element ["vsize" ]
83
91
return new_element
84
92
85
- def _post_clean_tx (self , new_element ):
86
-
93
+ def _post_clean_tx (self , new_element , sql_transaction_storage ):
87
94
for txinput in new_element .btclike_transaction .inputs :
88
- fine_rawtx = self . raw_txos . get (txinput .txid )
95
+ fine_rawtx = sql_transaction_storage . get_rawtx (txinput .txid )
89
96
if fine_rawtx is None :
90
97
return None # maybe processing error.
91
- txinput .amount = int (fine_rawtx ["vout" ][txinput .index ]["value" ] * 1e8 )
92
- if "address" in fine_rawtx ["vout" ][txinput .index ]["scriptPubKey" ].keys ():
93
- txinput .address = fine_rawtx ["vout" ][txinput .index ]["scriptPubKey" ][
94
- "address"
95
- ]
96
- elif (
97
- "addresses" in fine_rawtx ["vout" ][txinput .index ]["scriptPubKey" ].keys ()
98
- ):
99
- txinput .address = fine_rawtx ["vout" ][txinput .index ]["scriptPubKey" ][
100
- "addresses"
101
- ][0 ]
98
+ txinput .amount = fine_rawtx .btclike_transaction .outputs [
99
+ txinput .index
100
+ ].amount
101
+ txinput .address = fine_rawtx .btclike_transaction .outputs [
102
+ txinput .index
103
+ ].address
102
104
103
105
# Now we must calculate the total fee
104
106
total_inputs = sum ([a .amount for a in new_element .btclike_transaction .inputs ])
@@ -124,6 +126,7 @@ def __init__(self, **kwargs):
124
126
self .future_blocks_min = kwargs .get ("future_blocks_min" ) or 0
125
127
self .future_blocks_max = kwargs .get ("future_blocks_max" ) or 1
126
128
self .full_transactions = kwargs .get ("full_transactions" ) or True
129
+ self .db_connection_parameters = kwargs .get ("db_connection_parameters" )
127
130
self .transactions = []
128
131
self .in_mempool = []
129
132
self .txos = []
@@ -153,7 +156,7 @@ def _send_rpc_request(self, method, params=None):
153
156
# Certain nodes which are placed behind web servers or Cloudflare will
154
157
# configure rate limits and return some HTML error page if we go over that.
155
158
# Zpywallet is not designed to handle such content so we check for it first.
156
- # If you are using the full node facilities, ou are recommended to connect
159
+ # If you are using the full node facilities, you are recommended to connect
157
160
# to your own node and not to a public one, for this reason.
158
161
try :
159
162
j = response .json ()
@@ -179,7 +182,6 @@ def _send_batch_rpc_request(self, reqs):
179
182
try :
180
183
# Requests session is not needed for the full node but we can use it
181
184
# for the other providers in the future.
182
-
183
185
response = requests .post (
184
186
self .rpc_url ,
185
187
auth = (
@@ -223,56 +225,48 @@ def _get_block_height(self):
223
225
except Exception as e :
224
226
raise NetworkException (f"Failed to make RPC Call: { str (e )} " )
225
227
226
- def _get_raw_mempool (self ):
227
- height = self ._get_block_height ()
228
- h1 , h2 , h3 , h4 , h5 = [
229
- a ["result" ]
230
- for a in self ._send_batch_rpc_request (
231
- [
232
- ("getblockhash" , [height ]),
233
- ("getblockhash" , [height - 1 ]),
234
- ("getblockhash" , [height - 2 ]),
235
- ("getblockhash" , [height - 3 ]),
236
- ("getblockhash" , [height - 4 ]),
237
- ]
238
- )
239
- ]
240
- res = self ._send_batch_rpc_request (
241
- [
242
- ("getblock" , [h1 , 1 ]),
243
- ("getblock" , [h2 , 1 ]),
244
- ("getblock" , [h3 , 1 ]),
245
- ("getblock" , [h4 , 1 ]),
246
- ("getblock" , [h5 , 1 ]),
247
- ]
248
- )
249
- tx_counts = [len (r ["result" ]["tx" ]) for r in res ]
250
- avg_tx_count = sum (tx_counts ) // len (tx_counts )
228
+ # Internal methods are ran in a separate process which allows the OS
229
+ # to properly garbage collect the memory, as Python leaves a large footprint
230
+ # behind.
231
+ def _internal_mempool_fetch (self ):
251
232
res = self ._send_rpc_request ("getrawmempool" , [True ])
252
233
sorted_transactions = transform_and_sort_transactions (res )
253
- sorted_transactions = sorted_transactions [
254
- avg_tx_count
255
- * self .future_blocks_min : avg_tx_count
256
- * self .future_blocks_max
257
- ]
258
-
259
- transaction_batches = [
260
- sorted_transactions [i : i + self .max_batch ]
261
- for i in range (0 , len (sorted_transactions ), self .max_batch )
262
- ]
263
-
264
- for transaction_batch in transaction_batches :
234
+ return [tx ["txid" ] for tx in sorted_transactions ]
265
235
266
- txids = [tx ["txid" ] for tx in transaction_batch ]
236
+ def _internal_pass_1 (self , transaction_batch ):
237
+ sql_transaction_storage = SQLTransactionStorage (self .db_connection_parameters )
238
+ sql_transaction_storage .connect ()
239
+ try :
240
+ # The first pass will be to delete the confirmed transactions inside the DB
241
+ # if applicable.
242
+ txids = transaction_batch
243
+ all_txids = sql_transaction_storage .all_txids ()
244
+ for saved_txid in all_txids :
245
+ if saved_txid not in txids :
246
+ sql_transaction_storage .delete_transaction (saved_txid )
247
+
248
+ sql_transaction_storage .wipeout_reftxos ()
249
+ sql_transaction_storage .commit ()
250
+
251
+ # The second pass will be to create a copy of the mempool transactions
252
+ # without the ones we already have stored inside the list or DB.
253
+ new_txids = list (set (txids ) - set (all_txids ))
254
+
255
+ new_txid_batches = [
256
+ [t for t in new_txids [i : i + self .max_batch ]]
257
+ for i in range (0 , len (new_txids ), self .max_batch )
258
+ ]
259
+ return new_txid_batches
267
260
268
- # We are going to replace the mempool transactions with the ones
269
- # yielded by this function.
270
- # First we are going to yield the ones that are already stored
271
- # to save time.
272
- for tx in self .transactions :
273
- if tx .txid in txids :
274
- yield tx
261
+ except Exception as e :
262
+ sql_transaction_storage .rollback ()
263
+ raise e
275
264
265
+ def _internal_pass_2 (self , transaction_batch ):
266
+ sql_transaction_storage = SQLTransactionStorage (self .db_connection_parameters )
267
+ sql_transaction_storage .connect ()
268
+ try :
269
+ txids = transaction_batch
276
270
# Next we are going to yield new mempool transactions that we don't have
277
271
# Confirmed mempool transactions are dropped by this method and the one above.
278
272
txid_batches = [
@@ -296,8 +290,11 @@ def _get_raw_mempool(self):
296
290
# resolving txins.
297
291
if not self .full_transactions :
298
292
for tx in temp_transactions :
299
- tx .total_fee = 0
300
- yield (tx )
293
+ tx .total_fee = 0 # Because this is actually the (v)size
294
+ sql_transaction_storage .store_transaction (tx )
295
+ for i in range (len (tx .btclike_transaction .outputs )):
296
+ sql_transaction_storage .store_txo0 (tx , i )
297
+ sql_transaction_storage .commit ()
301
298
return
302
299
303
300
# Otherwise we have to get all of the input txids in one swoop so we can
@@ -306,27 +303,48 @@ def _get_raw_mempool(self):
306
303
self .txos [i : i + self .max_workers ]
307
304
for i in range (0 , len (self .txos ), self .max_workers )
308
305
]
309
-
310
306
with ThreadPoolExecutor (max_workers = self .rps ) as executor :
311
307
futures = [
312
308
executor .submit (self ._postprocess_transaction , txes )
313
309
for txes in txid_batches
314
310
]
315
- self .raw_txos = {}
316
311
for future in futures :
317
- self .raw_txos .update (future .result ())
312
+ results = future .result ()
313
+ for txid , result in results .items ():
314
+ try :
315
+ sql_transaction_storage .store_txo1 (txid , result )
316
+ except DatabaseError :
317
+ pass
318
318
319
319
self .txos = []
320
- new_transactions = []
321
320
for i in range (len (temp_transactions ) - 1 , - 1 , - 1 ):
322
321
temp_transaction = temp_transactions [i ]
323
- new_transactions .append (self ._post_clean_tx (temp_transaction ))
322
+ new_transaction = self ._post_clean_tx (
323
+ temp_transaction , sql_transaction_storage
324
+ )
325
+ if new_transaction is not None :
326
+ sql_transaction_storage .store_transaction (new_transaction )
327
+ for j in range (len (new_transaction .btclike_transaction .inputs )):
328
+ sql_transaction_storage .store_txo0 (
329
+ new_transaction , j , output = False
330
+ )
331
+ for j in range (len (new_transaction .btclike_transaction .outputs )):
332
+ sql_transaction_storage .store_txo0 (new_transaction , j )
324
333
del temp_transactions [i ]
334
+ sql_transaction_storage .wipeout_reftxos ()
335
+ sql_transaction_storage .commit ()
336
+ except Exception as e :
337
+ sql_transaction_storage .rollback ()
338
+ raise e
339
+
340
+ def _get_raw_mempool (self ):
341
+ with multiprocessing .Pool (1 ) as pool :
342
+ transaction_batches = pool .apply (self ._internal_mempool_fetch )
343
+
344
+ new_txid_batches = self ._internal_pass_1 (transaction_batches )
325
345
326
- self .raw_txos = {}
327
- for tx in new_transactions :
328
- if tx is not None :
329
- yield tx
346
+ for transaction_batch in new_txid_batches :
347
+ self ._internal_pass_2 (transaction_batch )
330
348
331
349
def _postprocess_transaction (self , txes ):
332
350
res = self ._send_batch_rpc_request (
@@ -336,7 +354,9 @@ def _postprocess_transaction(self, txes):
336
354
input_transactions = {}
337
355
for r in res :
338
356
if type (r ) is dict and "result" in r .keys ():
339
- input_transactions [r ["result" ]["txid" ]] = r ["result" ]
357
+ input_transactions [r ["result" ]["txid" ]] = self ._clean_tx (
358
+ r ["result" ]
359
+ ).SerializeToString ()
340
360
341
361
return input_transactions
342
362
@@ -364,5 +384,5 @@ def _process_transaction(self, txes, txids):
364
384
return temp_transactions
365
385
366
386
def get_raw_mempool (self ):
367
- self .transactions = [ * self . _get_raw_mempool ()]
368
- return self . transactions
387
+ self ._get_raw_mempool ()
388
+ return []
0 commit comments