Skip to content

Commit cb25ede

Browse files
committed
Merge opentimestamps#33: Only calendar tx
2e0a21e init set with iterator, wordings (Riccardo Casatta) ad5b6e0 use assert instead of sys.exit(1) (Riccardo Casatta) e62198c correct some code comments (Riccardo Casatta) c527e39 call serialize on tx instead of the named tuple (Riccardo Casatta) 4539b7f pep8 too long lines and function call typo (Riccardo Casatta) ac02e66 rewrote do_bitcoin to consider only calendar's transactions (Riccardo Casatta) Pull request description: After discussing we realize we first need a calendar version considering only his own tx, when this version is stabilized we are going to create a more complex version supporting external txs. First tests of this version on regtest looks working as desired, I am continuing testing but I let the reviewers to weigh in in the meantime. Some choice like exiting if there are no new blocks (line 279) aren't the most efficient approach but they are made to simplify the code and the reasoning. Tree-SHA512: 566574a2c0b38df9716b5c7e54a09e12e4b6e6841d88961f0500e92e2257d3640fd20c8278282f1b164f0b641e51a38072a2e9ba9661195e9f74335a4eebd41d
2 parents e9bda76 + 2e0a21e commit cb25ede

File tree

1 file changed

+100
-106
lines changed

1 file changed

+100
-106
lines changed

otsserver/stamper.py

+100-106
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import logging
1414
import threading
1515
import time
16-
16+
import sys
1717
import bitcoin.rpc
1818

1919
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):
5050
return digests[0]
5151

5252

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):
7054

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
7558

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"
7763

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):]
8066

8167
digest_timestamp = Timestamp(digest)
8268

@@ -102,7 +88,7 @@ def make_timestamp_from_block(digest, block, blockheight, serde_txs, *, max_tx_s
10288
attestation = BitcoinBlockHeaderAttestation(blockheight)
10389
merkleroot_stamp.attestations.add(attestation)
10490

105-
return digest_timestamp, CTransaction.deserialize(serialized_tx)
91+
return digest_timestamp
10692

10793

10894
class OrderedSet(collections.OrderedDict):
@@ -130,7 +116,8 @@ def __detect_reorgs(self, proxy):
130116
# rollback!
131117
pass
132118

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)))
134121
self.__blocks.pop(-1)
135122

136123
def update_from_proxy(self, proxy):
@@ -210,15 +197,17 @@ def sort_filter_unspent(unspent):
210197
except IndexError:
211198
continue
212199

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})
215202

216203
return sorted(confirmed_unspent, key=lambda x: x['amount'])
217204

205+
218206
class Stamper:
219207
"""Timestamping bot"""
220208

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):
222211
"""Create a new timestamp transaction template
223212
224213
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
232221
[CTxOut(txout_value, change_scriptPubKey),
233222
CTxOut(-1, CScript())])
234223

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):
236226
"""Update an existing timestamp transaction
237227
238228
Returns the old transaction with a new commitment, and with the fee
@@ -271,20 +261,23 @@ def __pending_to_merkle_tree(self, n):
271261
tip_timestamp = make_merkle_tree(commitment_digest_timestamps)
272262
logging.debug("Done making merkle tree")
273263

274-
return (tip_timestamp, commitment_timestamps)
264+
return tip_timestamp, commitment_timestamps
275265

276266
def __do_bitcoin(self):
277267
"""Do Bitcoin-related maintenance"""
278268

279-
280-
281269
# FIXME: we shouldn't have to create a new proxy each time, but with
282270
# current python-bitcoinlib and the RPC implementation it seems that
283271
# the proxy connection can timeout w/o recovering properly.
284272
proxy = bitcoin.rpc.Proxy()
285273

286274
new_blocks = self.known_blocks.update_from_proxy(proxy)
287275

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+
288281
for (block_height, block_hash) in new_blocks:
289282
logging.info("New block %s at height %d" % (b2lx(block_hash), block_height))
290283

@@ -301,7 +294,8 @@ def __do_bitcoin(self):
301294
# FIXME: the reorged transaction might get mined in another
302295
# block, so just adding the commitments for it back to the pool
303296
# 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)))
305299
for reorged_commitment_timestamp in reorged_tx.commitment_timestamps:
306300
self.pending_commitments.add(reorged_commitment_timestamp.msg)
307301

@@ -314,26 +308,25 @@ def __do_bitcoin(self):
314308
logging.error("Failed to get block")
315309
return
316310

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)
322314

323315
# Check all potential pending txs against this block.
324316
# iterating in reverse order to prioritize most recent digest which commits to a bigger merkle tree
325317
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)
328318

329-
if block_timestamp is None:
319+
if unconfirmed_tx.tx.GetTxid() not in block_txids:
330320
continue
331321

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())))
334327
# 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)
337330
assert tip_timestamp.msg == unconfirmed_tx.tip_timestamp.msg
338331

339332
mined_tx.tip_timestamp.merge(block_timestamp)
@@ -350,26 +343,28 @@ def __do_bitcoin(self):
350343
# have been mined, and are waiting for confirmations.
351344
self.txs_waiting_for_confirmation[block_height] = mined_tx
352345

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()
357348

358349
# And finally, we can reset the last time a timestamp
359350
# transaction was mined to right now.
360351
self.last_timestamp_tx = time.time()
361352

362353
break
363354

364-
365355
time_to_next_tx = int(self.last_timestamp_tx + self.min_tx_interval - time.time())
366356
if time_to_next_tx > 0:
367357
# Minimum interval between transactions hasn't been reached, so do nothing
368358
logging.debug("Waiting %ds before next tx" % time_to_next_tx)
369359
return
370360

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
373368
# Find the biggest unspent output that's confirmed
374369
unspent = find_unspent(proxy)
375370

@@ -381,62 +376,59 @@ def __do_bitcoin(self):
381376
prev_tx = self.__create_new_timestamp_tx_template(unspent[-1]['outpoint'], unspent[-1]['amount'],
382377
change_addr.to_scriptPubKey())
383378

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'])))
385381

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()
388387

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)
416393

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
426401

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']
429407

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
431417

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!
437420

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)))
440432

441433
def __loop(self):
442434
logging.info("Starting stamper loop")
@@ -460,7 +452,8 @@ def __loop(self):
460452
# Is this commitment already stamped?
461453
if commitment not in self.calendar:
462454
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)))
464457
else:
465458
if idx % 1000 == 0:
466459
logging.debug('Commitment at idx %d already stamped' % idx)
@@ -496,7 +489,8 @@ def is_pending(self, commitment):
496489
for height, ttx in self.txs_waiting_for_confirmation.items():
497490
for commitment_timestamp in ttx.commitment_timestamps:
498491
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)
500494

501495
else:
502496
return False
@@ -514,7 +508,7 @@ def __init__(self, calendar, exit_event, relay_feerate, min_confirmations, min_t
514508

515509
self.known_blocks = KnownBlocks()
516510
self.unconfirmed_txs = []
517-
self.mines = set()
511+
518512
self.pending_commitments = OrderedSet()
519513
self.txs_waiting_for_confirmation = {}
520514

0 commit comments

Comments
 (0)