13
13
import logging
14
14
import threading
15
15
import time
16
-
16
+ import sys
17
17
import bitcoin .rpc
18
18
19
19
from bitcoin .core import COIN , b2lx , b2x , CTxIn , CTxOut , CTransaction , str_money_value
@@ -50,33 +50,19 @@ def make_btc_block_merkle_tree(blk_txids):
50
50
return digests [0 ]
51
51
52
52
53
- def make_timestamp_from_block (digest , block , blockheight , serde_txs , * , max_tx_size = 500 ):
54
- """Make a timestamp for a message in a block with cached serialized txs
55
- see python-opentimestamps.bitcoin.make_timestamp_from_block
56
- """
57
- len_smallest_tx_found = max_tx_size + 1
58
- commitment_tx = None
59
- prefix = None
60
- suffix = None
61
- for (tx , serialized_tx ) in serde_txs :
62
-
63
- if len (serialized_tx ) > len_smallest_tx_found :
64
- continue
65
-
66
- try :
67
- i = serialized_tx .index (digest )
68
- except ValueError :
69
- continue
53
+ def make_timestamp_from_block_tx (confirmed_tx , block , blockheight ):
70
54
71
- # Found it!
72
- commitment_tx = tx
73
- prefix = serialized_tx [0 :i ]
74
- suffix = serialized_tx [i + len (digest ):]
55
+ commitment_tx = confirmed_tx .tx
56
+ serialized_tx = commitment_tx .serialize (params = {'include_witness' : False })
57
+ digest = confirmed_tx .tip_timestamp .msg
75
58
76
- len_smallest_tx_found = len (serialized_tx )
59
+ try :
60
+ i = serialized_tx .index (digest )
61
+ except ValueError :
62
+ assert False , "can't build a block_timestamp from my tx, this is not supposed to happen, exiting"
77
63
78
- if len_smallest_tx_found > max_tx_size :
79
- return None , None
64
+ prefix = serialized_tx [ 0 : i ]
65
+ suffix = serialized_tx [ i + len ( digest ):]
80
66
81
67
digest_timestamp = Timestamp (digest )
82
68
@@ -102,7 +88,7 @@ def make_timestamp_from_block(digest, block, blockheight, serde_txs, *, max_tx_s
102
88
attestation = BitcoinBlockHeaderAttestation (blockheight )
103
89
merkleroot_stamp .attestations .add (attestation )
104
90
105
- return digest_timestamp , CTransaction . deserialize ( serialized_tx )
91
+ return digest_timestamp
106
92
107
93
108
94
class OrderedSet (collections .OrderedDict ):
@@ -130,7 +116,8 @@ def __detect_reorgs(self, proxy):
130
116
# rollback!
131
117
pass
132
118
133
- logging .info ("Reorg detected at height %d, rolling back block %s" % (self .__blocks [- 1 ].height , b2lx (self .__blocks [- 1 ].hash )))
119
+ logging .info ("Reorg detected at height %d, rolling back block %s"
120
+ % (self .__blocks [- 1 ].height , b2lx (self .__blocks [- 1 ].hash )))
134
121
self .__blocks .pop (- 1 )
135
122
136
123
def update_from_proxy (self , proxy ):
@@ -210,15 +197,17 @@ def sort_filter_unspent(unspent):
210
197
except IndexError :
211
198
continue
212
199
213
- confirmed_unspent .append ({'outpoint' :txin .prevout ,
214
- 'amount' :confirmed_outpoint ['txout' ].nValue })
200
+ confirmed_unspent .append ({'outpoint' : txin .prevout ,
201
+ 'amount' : confirmed_outpoint ['txout' ].nValue })
215
202
216
203
return sorted (confirmed_unspent , key = lambda x : x ['amount' ])
217
204
205
+
218
206
class Stamper :
219
207
"""Timestamping bot"""
220
208
221
- def __create_new_timestamp_tx_template (self , outpoint , txout_value , change_scriptPubKey ):
209
+ @staticmethod
210
+ def __create_new_timestamp_tx_template (outpoint , txout_value , change_scriptPubKey ):
222
211
"""Create a new timestamp transaction template
223
212
224
213
The transaction created will have one input and two outputs, with the
@@ -232,7 +221,8 @@ def __create_new_timestamp_tx_template(self, outpoint, txout_value, change_scrip
232
221
[CTxOut (txout_value , change_scriptPubKey ),
233
222
CTxOut (- 1 , CScript ())])
234
223
235
- def __update_timestamp_tx (self , old_tx , new_commitment , new_min_block_height , relay_feerate ):
224
+ @staticmethod
225
+ def __update_timestamp_tx (old_tx , new_commitment , new_min_block_height , relay_feerate ):
236
226
"""Update an existing timestamp transaction
237
227
238
228
Returns the old transaction with a new commitment, and with the fee
@@ -271,20 +261,23 @@ def __pending_to_merkle_tree(self, n):
271
261
tip_timestamp = make_merkle_tree (commitment_digest_timestamps )
272
262
logging .debug ("Done making merkle tree" )
273
263
274
- return ( tip_timestamp , commitment_timestamps )
264
+ return tip_timestamp , commitment_timestamps
275
265
276
266
def __do_bitcoin (self ):
277
267
"""Do Bitcoin-related maintenance"""
278
268
279
-
280
-
281
269
# FIXME: we shouldn't have to create a new proxy each time, but with
282
270
# current python-bitcoinlib and the RPC implementation it seems that
283
271
# the proxy connection can timeout w/o recovering properly.
284
272
proxy = bitcoin .rpc .Proxy ()
285
273
286
274
new_blocks = self .known_blocks .update_from_proxy (proxy )
287
275
276
+ # code after this if it's executed only when we have new blocks, it simplify reasoning at the cost of not
277
+ # having a broadcasted tx immediately after we have a new cycle (the calendar wait the next block)
278
+ if not new_blocks :
279
+ return
280
+
288
281
for (block_height , block_hash ) in new_blocks :
289
282
logging .info ("New block %s at height %d" % (b2lx (block_hash ), block_height ))
290
283
@@ -301,7 +294,8 @@ def __do_bitcoin(self):
301
294
# FIXME: the reorged transaction might get mined in another
302
295
# block, so just adding the commitments for it back to the pool
303
296
# isn't ideal, but it is safe
304
- logging .info ('tx %s at height %d removed by reorg, adding %d commitments back to pending' % (b2lx (reorged_tx .tx .GetTxid ()), block_height , len (reorged_tx .commitment_timestamps )))
297
+ logging .info ('tx %s at height %d removed by reorg, adding %d commitments back to pending'
298
+ % (b2lx (reorged_tx .tx .GetTxid ()), block_height , len (reorged_tx .commitment_timestamps )))
305
299
for reorged_commitment_timestamp in reorged_tx .commitment_timestamps :
306
300
self .pending_commitments .add (reorged_commitment_timestamp .msg )
307
301
@@ -314,26 +308,25 @@ def __do_bitcoin(self):
314
308
logging .error ("Failed to get block" )
315
309
return
316
310
317
- # the following is an optimization, by pre computing the serialization of tx
318
- # we avoid this step for every unconfirmed tx
319
- serde_txs = []
320
- for tx in block .vtx :
321
- serde_txs .append ((tx , tx .serialize (params = {'include_witness' :False })))
311
+ # the following is an optimization, by pre computing the tx_id we rapidly check if our unconfirmed tx
312
+ # is in the block
313
+ block_txids = set (tx .GetTxid () for tx in block .vtx )
322
314
323
315
# Check all potential pending txs against this block.
324
316
# iterating in reverse order to prioritize most recent digest which commits to a bigger merkle tree
325
317
for unconfirmed_tx in self .unconfirmed_txs [::- 1 ]:
326
- (block_timestamp , found_tx ) = make_timestamp_from_block (unconfirmed_tx .tip_timestamp .msg , block ,
327
- block_height , serde_txs )
328
318
329
- if block_timestamp is None :
319
+ if unconfirmed_tx . tx . GetTxid () not in block_txids :
330
320
continue
331
321
332
- logging .info ("Found %s which contains %s" % (b2lx (found_tx .GetTxid ()),
333
- b2x (unconfirmed_tx .tip_timestamp .msg )))
322
+ confirmed_tx = unconfirmed_tx # Success! Found tx
323
+ block_timestamp = make_timestamp_from_block_tx (confirmed_tx , block , block_height )
324
+
325
+ logging .info ("Found commitment %s in tx %s"
326
+ % (b2x (confirmed_tx .tip_timestamp .msg ), b2lx (confirmed_tx .tx .GetTxid ())))
334
327
# Success!
335
- (tip_timestamp , commitment_timestamps ) = self .__pending_to_merkle_tree (unconfirmed_tx .n )
336
- mined_tx = TimestampTx (found_tx , tip_timestamp , commitment_timestamps )
328
+ (tip_timestamp , commitment_timestamps ) = self .__pending_to_merkle_tree (confirmed_tx .n )
329
+ mined_tx = TimestampTx (confirmed_tx . tx , tip_timestamp , commitment_timestamps )
337
330
assert tip_timestamp .msg == unconfirmed_tx .tip_timestamp .msg
338
331
339
332
mined_tx .tip_timestamp .merge (block_timestamp )
@@ -350,26 +343,28 @@ def __do_bitcoin(self):
350
343
# have been mined, and are waiting for confirmations.
351
344
self .txs_waiting_for_confirmation [block_height ] = mined_tx
352
345
353
- # Erasing all unconfirmed txs if the transaction was mine
354
- if mined_tx .tx .GetTxid () in self .mines :
355
- self .unconfirmed_txs .clear ()
356
- self .mines .clear ()
346
+ # Erase all unconfirmed txs, as they all conflict with each other
347
+ self .unconfirmed_txs .clear ()
357
348
358
349
# And finally, we can reset the last time a timestamp
359
350
# transaction was mined to right now.
360
351
self .last_timestamp_tx = time .time ()
361
352
362
353
break
363
354
364
-
365
355
time_to_next_tx = int (self .last_timestamp_tx + self .min_tx_interval - time .time ())
366
356
if time_to_next_tx > 0 :
367
357
# Minimum interval between transactions hasn't been reached, so do nothing
368
358
logging .debug ("Waiting %ds before next tx" % time_to_next_tx )
369
359
return
370
360
371
- prev_tx = None
372
- if self .pending_commitments and not self .unconfirmed_txs :
361
+ if not self .pending_commitments :
362
+ logging .debug ("No pending commitments, no tx needed" )
363
+ return
364
+
365
+ if self .unconfirmed_txs :
366
+ (prev_tx , prev_tip_timestamp , prev_commitment_timestamps ) = self .unconfirmed_txs [- 1 ]
367
+ else : # first tx of a new cycle
373
368
# Find the biggest unspent output that's confirmed
374
369
unspent = find_unspent (proxy )
375
370
@@ -381,62 +376,59 @@ def __do_bitcoin(self):
381
376
prev_tx = self .__create_new_timestamp_tx_template (unspent [- 1 ]['outpoint' ], unspent [- 1 ]['amount' ],
382
377
change_addr .to_scriptPubKey ())
383
378
384
- logging .debug ('New timestamp tx, spending output %r, value %s' % (unspent [- 1 ]['outpoint' ], str_money_value (unspent [- 1 ]['amount' ])))
379
+ logging .debug ('New timestamp tx, spending output %r, value %s' % (unspent [- 1 ]['outpoint' ],
380
+ str_money_value (unspent [- 1 ]['amount' ])))
385
381
386
- elif self .unconfirmed_txs :
387
- (prev_tx , prev_tip_timestamp , prev_commitment_timestamps ) = self .unconfirmed_txs [- 1 ]
382
+ (tip_timestamp , commitment_timestamps ) = self .__pending_to_merkle_tree (len (self .pending_commitments ))
383
+ logging .debug ("New tip is %s" % b2x (tip_timestamp .msg ))
384
+ # make_merkle_tree() seems to take long enough on really big adds
385
+ # that the proxy dies
386
+ proxy = bitcoin .rpc .Proxy ()
388
387
389
- # Send the first transaction even if we don't have a new block
390
- if prev_tx and (new_blocks or not self .unconfirmed_txs ):
391
- (tip_timestamp , commitment_timestamps ) = self .__pending_to_merkle_tree (len (self .pending_commitments ))
392
- logging .debug ("New tip is %s" % b2x (tip_timestamp .msg ))
393
- # make_merkle_tree() seems to take long enough on really big adds
394
- # that the proxy dies
395
- proxy = bitcoin .rpc .Proxy ()
396
-
397
- sent_tx = None
398
- relay_feerate = self .relay_feerate
399
- while sent_tx is None :
400
- unsigned_tx = self .__update_timestamp_tx (prev_tx , tip_timestamp .msg ,
401
- proxy .getblockcount (), relay_feerate )
402
-
403
- fee = _get_tx_fee (unsigned_tx , proxy )
404
- if fee is None :
405
- logging .debug ("Can't determine txfee of transaction; skipping" )
406
- return
407
- if fee > self .max_fee :
408
- logging .error ("Maximum txfee reached!" )
409
- return
410
-
411
- r = proxy .signrawtransaction (unsigned_tx )
412
- if not r ['complete' ]:
413
- logging .error ("Failed to sign transaction! r = %r" % r )
414
- return
415
- signed_tx = r ['tx' ]
388
+ sent_tx = None
389
+ relay_feerate = self .relay_feerate
390
+ while sent_tx is None :
391
+ unsigned_tx = self .__update_timestamp_tx (prev_tx , tip_timestamp .msg ,
392
+ proxy .getblockcount (), relay_feerate )
416
393
417
- try :
418
- txid = proxy .sendrawtransaction (signed_tx )
419
- except bitcoin .rpc .JSONRPCError as err :
420
- if err .error ['code' ] == - 26 :
421
- logging .debug ("Err: %r" % err .error )
422
- # Insufficient priority - basically means we didn't
423
- # pay enough, so try again with a higher feerate
424
- relay_feerate *= 2
425
- continue
394
+ fee = _get_tx_fee (unsigned_tx , proxy )
395
+ if fee is None :
396
+ logging .debug ("Can't determine txfee of transaction; skipping" )
397
+ return
398
+ if fee > self .max_fee :
399
+ logging .error ("Maximum txfee reached!" )
400
+ return
426
401
427
- else :
428
- raise err # something else, fail!
402
+ r = proxy .signrawtransaction (unsigned_tx )
403
+ if not r ['complete' ]:
404
+ logging .error ("Failed to sign transaction! r = %r" % r )
405
+ return
406
+ signed_tx = r ['tx' ]
429
407
430
- sent_tx = signed_tx
408
+ try :
409
+ proxy .sendrawtransaction (signed_tx )
410
+ except bitcoin .rpc .JSONRPCError as err :
411
+ if err .error ['code' ] == - 26 :
412
+ logging .debug ("Err: %r" % err .error )
413
+ # Insufficient priority - basically means we didn't
414
+ # pay enough, so try again with a higher feerate
415
+ relay_feerate *= 2
416
+ continue
431
417
432
- if self .unconfirmed_txs :
433
- logging .info ("Sent timestamp tx %s, replacing %s; %d total commitments; %d prior tx versions" %
434
- (b2lx (sent_tx .GetTxid ()), b2lx (prev_tx .GetTxid ()), len (commitment_timestamps ), len (self .unconfirmed_txs )))
435
- else :
436
- logging .info ("Sent timestamp tx %s; %d total commitments" % (b2lx (sent_tx .GetTxid ()), len (commitment_timestamps )))
418
+ else :
419
+ raise err # something else, fail!
437
420
438
- self .unconfirmed_txs .append (UnconfirmedTimestampTx (sent_tx , tip_timestamp , len (commitment_timestamps )))
439
- self .mines .add (sent_tx .GetTxid ())
421
+ sent_tx = signed_tx
422
+
423
+ if self .unconfirmed_txs :
424
+ logging .info ("Sent timestamp tx %s, replacing %s; %d total commitments; %d prior tx versions" %
425
+ (b2lx (sent_tx .GetTxid ()), b2lx (prev_tx .GetTxid ()), len (commitment_timestamps ),
426
+ len (self .unconfirmed_txs )))
427
+ else :
428
+ logging .info ("Sent timestamp tx %s; %d total commitments" % (b2lx (sent_tx .GetTxid ()),
429
+ len (commitment_timestamps )))
430
+
431
+ self .unconfirmed_txs .append (UnconfirmedTimestampTx (sent_tx , tip_timestamp , len (commitment_timestamps )))
440
432
441
433
def __loop (self ):
442
434
logging .info ("Starting stamper loop" )
@@ -460,7 +452,8 @@ def __loop(self):
460
452
# Is this commitment already stamped?
461
453
if commitment not in self .calendar :
462
454
self .pending_commitments .add (commitment )
463
- logging .debug ('Added %s (idx %d) to pending commitments; %d total' % (b2x (commitment ), idx , len (self .pending_commitments )))
455
+ logging .debug ('Added %s (idx %d) to pending commitments; %d total'
456
+ % (b2x (commitment ), idx , len (self .pending_commitments )))
464
457
else :
465
458
if idx % 1000 == 0 :
466
459
logging .debug ('Commitment at idx %d already stamped' % idx )
@@ -496,7 +489,8 @@ def is_pending(self, commitment):
496
489
for height , ttx in self .txs_waiting_for_confirmation .items ():
497
490
for commitment_timestamp in ttx .commitment_timestamps :
498
491
if commitment == commitment_timestamp .msg :
499
- return "Timestamped by transaction %s; waiting for %d confirmations" % (b2lx (ttx .tx .GetTxid ()), self .min_confirmations - 1 )
492
+ return "Timestamped by transaction %s; waiting for %d confirmations" \
493
+ % (b2lx (ttx .tx .GetTxid ()), self .min_confirmations - 1 )
500
494
501
495
else :
502
496
return False
@@ -514,7 +508,7 @@ def __init__(self, calendar, exit_event, relay_feerate, min_confirmations, min_t
514
508
515
509
self .known_blocks = KnownBlocks ()
516
510
self .unconfirmed_txs = []
517
- self . mines = set ()
511
+
518
512
self .pending_commitments = OrderedSet ()
519
513
self .txs_waiting_for_confirmation = {}
520
514
0 commit comments