Skip to content

Commit e45f8ff

Browse files
committedApr 11, 2018
Merge branch 'online_backup' of https://github.com/RCasatta/opentimestamps-server into online_backup

File tree

4 files changed

+101
-61
lines changed

4 files changed

+101
-61
lines changed
 

‎otsserver/backup.py

+39-28
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
import time
1919
from urllib.parse import urlparse, urljoin
2020

21-
PAGING = 1000
22-
SLEEP_SECS = 600
21+
PAGING = 1000 # Number of commitments per chunk
22+
SLEEP_SECS = 60 # Once the backup is synced this is the polling interval to check for new chunks
2323

2424

2525
class Backup:
@@ -29,27 +29,36 @@ def __init__(self, journal, calendar, cache_path):
2929
self.cache_path = cache_path
3030
os.makedirs(cache_path, exist_ok=True)
3131

32+
# Return the bytes of the chunk
3233
def __getitem__(self, chunk):
34+
35+
# We use a disk cache, creating a chunk of 1000 commitments is a quite expensive operation of about 10s.
36+
# The server isn't blocked in the meantime but this could be used by an attacker to degrade calendar performance
37+
# Moreover is not recommended to set up an HTTP cache more than one year (see RFC 2616), thus, a disk cache is
38+
# mandatory.
3339
cached_kv_bytes = self.read_disk_cache(chunk)
3440
if cached_kv_bytes is not None:
3541
return cached_kv_bytes
3642

3743
backup_map = {}
3844
start = chunk*PAGING
3945
end = start+PAGING
40-
for i in range(start, end)[::-1]: # iterate in reverse to fail fast
46+
47+
# Iterate in reverse to fail fast if this chunk is not complete, a chunk is considered complete if all relative
48+
# 1000 commitments are complete. Which means a tx with more of 6 confirmations timestamp them
49+
for i in range(start, end)[::-1]:
4150
try:
4251
current = self.journal[i]
43-
# print(str(i) +":"+b2x(journal[i]))
4452
current_el = self.calendar[current]
45-
# print("\t"+str(current_el))
4653
self.__create_kv_map(current_el, current_el.msg, backup_map)
4754
except KeyError:
55+
# according to https://docs.python.org/3/library/exceptions.html#IndexError IndexError is the more
56+
# appropriate exception for this case
4857
raise IndexError
4958
if i % 100 == 0:
50-
logging.info(str(i) + ":" + b2x(self.journal[i]))
59+
logging.debug("Got commitment " + str(i) + ":" + b2x(self.journal[i]))
5160

52-
logging.info("map len " + str(len(backup_map)) + " start:" + str(start) + " end:" + str(end))
61+
logging.debug("map len " + str(len(backup_map)) + " start:" + str(start) + " end:" + str(end))
5362
kv_bytes = self.__kv_map_to_bytes(backup_map)
5463
self.write_disk_cache(chunk, kv_bytes)
5564

@@ -92,6 +101,7 @@ def __create_kv_map(ts, msg, kv_map):
92101
@staticmethod
93102
def __kv_map_to_bytes(kv_map):
94103
ctx = BytesSerializationContext()
104+
# Sorting the map elements to create chunks deterministically, but this is not mandatory for importing the chunk
95105
for key, value in sorted(kv_map.items()):
96106
ctx.write_varuint(len(key))
97107
ctx.write_bytes(key)
@@ -101,8 +111,11 @@ def __kv_map_to_bytes(kv_map):
101111
return ctx.getbytes()
102112

103113
def read_disk_cache(self, chunk):
114+
# For the disk cache we are using 6 digits file name which will support a total of 1 billion commitments,
115+
# because every chunk contain 1000 commitments. Supposing 1 commitment per second this could last for 32 years
116+
# which appear to be ok for this version
104117
chunk_str = "{0:0>6}".format(chunk)
105-
chunk_path = chunk_str[0:3]
118+
chunk_path = chunk_str[0:3] # we create a path to avoid creating more than 1000 files per directory
106119

107120
try:
108121
cache_file = self.cache_path + '/' + chunk_path + '/' + chunk_str
@@ -120,7 +133,8 @@ def write_disk_cache(self, chunk, bytes):
120133
with open(cache_file, 'wb') as fd:
121134
fd.write(bytes)
122135

123-
136+
# The following is a shrinked version of the standard calendar http server, it only support the '/timestamp' endpoint
137+
# This way the backup server could serve request in place of the calendar serve which is backupping
124138
class RPCRequestHandler(http.server.BaseHTTPRequestHandler):
125139

126140
def do_GET(self):
@@ -197,6 +211,9 @@ def serve_forever(self):
197211
super().serve_forever()
198212

199213

214+
# This is the thread responsible for asking the chunks to the running calendar and import them in the db.
215+
# The main script allow to launch 1 thread of this for every calendar to backup, thus a backup server could
216+
# theoretically serve timestamp in place of every calendar server which supports this incremental live backup mechanism
200217
class AskBackup(threading.Thread):
201218

202219
def __init__(self, db, calendar_url, base_path):
@@ -208,36 +225,33 @@ def __init__(self, db, calendar_url, base_path):
208225
super().__init__(target=self.loop)
209226

210227
def loop(self):
211-
print("Starting loop for %s" % self.calendar_url)
228+
logging.info("Starting loop for %s" % self.calendar_url)
212229

213230
try:
214231
with open(self.up_to_path, 'r') as up_to_fd:
215232
last_known = int(up_to_fd.read().strip())
216233
except FileNotFoundError as exp:
217234
last_known = -1
218-
print("Checking calendar " + str(self.calendar_url) + ", last_known commitment:" + str(last_known))
235+
logging.info("Checking calendar " + str(self.calendar_url) + ", last_known commitment:" + str(last_known))
219236

220237
while True:
221238
start_time = time.time()
222239
backup_url = urljoin(self.calendar_url, "/experimental/backup/%d" % (last_known + 1))
223-
print(str(backup_url))
240+
logging.debug("Asking " + str(backup_url))
224241
try:
225242
r = requests.get(backup_url)
226243
except Exception as err:
227-
print("Exception asking " + str(backup_url) + " message " + str(err))
244+
logging.error("Exception asking " + str(backup_url) + " message " + str(err))
228245
break
229246

230-
if r.status_code == 404:
231-
print("%s not found, sleeping for %s seconds" % (backup_url, SLEEP_SECS) )
247+
if r.status_code != 200:
248+
logging.info("%s not found, sleeping for %s seconds" % (backup_url, SLEEP_SECS) )
232249
time.sleep(SLEEP_SECS)
233250
continue
234251

235-
# print(r.raw.read(10))
236252
kv_map = Backup.bytes_to_kv_map(r.content)
237-
# print(str(map))
238253
attestations = {}
239254
ops = {}
240-
print("kv_maps elements " + str(len(kv_map)))
241255
for key, value in kv_map.items():
242256
# print("--- key=" + b2x(key) + " value=" + b2x(value))
243257
ctx = BytesDeserializationContext(value)
@@ -252,31 +266,28 @@ def loop(self):
252266

253267
proxy = bitcoin.rpc.Proxy()
254268

255-
# verify all bitcoin attestation are valid
256-
print("total attestations: " + str(len(attestations)))
269+
# Verify all bitcoin attestation are valid
270+
logging.debug("Total attestations: " + str(len(attestations)))
257271
for key, attestation in attestations.items():
258272
if attestation.__class__ == BitcoinBlockHeaderAttestation:
259273
blockhash = proxy.getblockhash(attestation.height)
260274
block_header = proxy.getblockheader(blockhash)
275+
# the following raise an exception and block computation if the attestation does not verify
261276
attested_time = attestation.verify_against_blockheader(key, block_header)
262-
print("verifying " + b2x(key) + " result " + str(attested_time))
277+
logging.debug("Verifying " + b2x(key) + " result " + str(attested_time))
263278

264279
# verify all ops connects to an attestation
265-
print("total ops: " + str(len(ops)))
280+
logging.debug("Total ops: " + str(len(ops)))
266281
for key, op in ops.items():
267-
268-
# print("key " + b2x(key) + " op " + str(op))
269282
current_key = key
270283
current_op = op
271284
while True:
272285
next_key = current_op(current_key)
273-
# print("next_key " + b2x(next_key))
274286
if next_key in ops:
275287
current_key = next_key
276288
current_op = ops[next_key]
277289
else:
278290
break
279-
# print("maps to " + b2x(next_key))
280291
assert next_key in attestations
281292

282293
batch = leveldb.WriteBatch()
@@ -289,11 +300,11 @@ def loop(self):
289300
with open(self.up_to_path, 'w') as up_to_fd:
290301
up_to_fd.write('%d\n' % last_known)
291302
except FileNotFoundError as exp:
292-
print(str(exp))
303+
logging.error(str(exp))
293304
break
294305

295306
elapsed_time = time.time() - start_time
296-
print("Took %ds" % elapsed_time)
307+
logging.info("Took %ds for %s" % (elapsed_time, str(backup_url)))
297308

298309

299310

‎otsserver/rpc.py

+49-26
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
import socketserver
1616
import threading
1717
import time
18+
import pystache
19+
import datetime
20+
from functools import reduce
1821

1922
import bitcoin.core
2023
from bitcoin.core import b2lx, b2x
@@ -24,7 +27,7 @@
2427
from opentimestamps.core.serialize import StreamSerializationContext
2528

2629
from otsserver.calendar import Journal
27-
30+
renderer = pystache.Renderer()
2831

2932
class RPCRequestHandler(http.server.BaseHTTPRequestHandler):
3033
MAX_DIGEST_LENGTH = 64
@@ -181,42 +184,62 @@ def do_GET(self):
181184
# need to investigate further, but this seems to work.
182185
str_wallet_balance = str(proxy._call("getbalance"))
183186

184-
welcome_page = """\
185-
<html>
187+
transactions = proxy._call("listtransactions", "", 50)
188+
# We want only the confirmed txs containing an OP_RETURN, from most to least recent
189+
transactions = list(filter(lambda x: x["confirmations"] > 0 and x["amount"] == 0, transactions))
190+
a_week_ago = (datetime.date.today() - datetime.timedelta(days=7)).timetuple()
191+
a_week_ago_posix = time.mktime(a_week_ago)
192+
transactions_in_last_week = list(filter(lambda x: x["time"] > a_week_ago_posix, transactions))
193+
fees_in_last_week = reduce(lambda a,b: a-b["fee"], transactions_in_last_week, 0)
194+
time_between_transactions = round(168 / len(transactions_in_last_week)) # in hours based on 168 hours in a week
195+
transactions.sort(key=lambda x: x["confirmations"])
196+
homepage_template = """<html>
186197
<head>
187198
<title>OpenTimestamps Calendar Server</title>
188199
</head>
189200
<body>
190-
<p>This is an <a href="https://opentimestamps.org">OpenTimestamps</a> <a href="https://github.com/opentimestamps/opentimestamps-server">Calendar Server</a> (v%s)</p>
191-
201+
<p>This is an <a href="https://opentimestamps.org">OpenTimestamps</a> <a href="https://github.com/opentimestamps/opentimestamps-server">Calendar Server</a> (v{{ version }})</p>
192202
<p>
193-
Pending commitments: %d</br>
194-
Transactions waiting for confirmation: %d</br>
195-
Most recent timestamp tx: %s (%d prior versions)</br>
196-
Most recent merkle tree tip: %s</br>
197-
Best-block: %s, height %d</br>
203+
Pending commitments: {{ pending_commitments }}</br>
204+
Transactions waiting for confirmation: {{ txs_waiting_for_confirmation }}</br>
205+
Most recent timestamp tx: {{ most_recent_tx }} ({{ prior_versions }} prior versions)</br>
206+
Most recent merkle tree tip: {{ tip }}</br>
207+
Best-block: {{ best_block }}, height {{ block_height }}</br>
198208
</br>
199-
Wallet balance: %s BTC</br>
209+
Wallet balance: {{ balance }} BTC</br>
200210
</p>
201-
202211
<p>
203-
You can donate to the wallet by sending funds to: %s</br>
212+
You can donate to the wallet by sending funds to: {{ address }}</br>
204213
This address changes after every donation.
205214
</p>
206-
215+
<p>
216+
Average time between transactions in the last week: {{ time_between_transactions }} hour(s)</br>
217+
Fees used in the last week: {{ fees_in_last_week }} BTC</br>
218+
Latest transactions: </br>
219+
{{#transactions}}
220+
{{txid}} </br>
221+
{{/transactions}}
222+
</p>
207223
</body>
208-
</html>
209-
""" % (otsserver.__version__,
210-
len(self.calendar.stamper.pending_commitments),
211-
len(self.calendar.stamper.txs_waiting_for_confirmation),
212-
b2lx(self.calendar.stamper.unconfirmed_txs[-1].tx.GetTxid()) if self.calendar.stamper.unconfirmed_txs else 'None',
213-
max(0, len(self.calendar.stamper.unconfirmed_txs) - 1),
214-
b2x(self.calendar.stamper.unconfirmed_txs[-1].tip_timestamp.msg) if self.calendar.stamper.unconfirmed_txs else 'None',
215-
bitcoin.core.b2lx(proxy.getbestblockhash()), proxy.getblockcount(),
216-
str_wallet_balance,
217-
str(proxy.getaccountaddress('')))
218-
219-
self.wfile.write(welcome_page.encode())
224+
</html>"""
225+
226+
stats = { 'version': otsserver.__version__,
227+
'pending_commitments': len(self.calendar.stamper.pending_commitments),
228+
'txs_waiting_for_confirmation':len(self.calendar.stamper.txs_waiting_for_confirmation),
229+
'most_recent_tx': b2lx(self.calendar.stamper.unconfirmed_txs[-1].tx.GetTxid()) if self.calendar.stamper.unconfirmed_txs else 'None',
230+
'prior_versions': max(0, len(self.calendar.stamper.unconfirmed_txs) - 1),
231+
'tip': b2x(self.calendar.stamper.unconfirmed_txs[-1].tip_timestamp.msg) if self.calendar.stamper.unconfirmed_txs else 'None',
232+
'best_block': bitcoin.core.b2lx(proxy.getbestblockhash()),
233+
'block_height': proxy.getblockcount(),
234+
'balance': str_wallet_balance,
235+
'address': str(proxy.getaccountaddress('')),
236+
'transactions': transactions[:5],
237+
'time_between_transactions': time_between_transactions,
238+
'fees_in_last_week': fees_in_last_week,
239+
}
240+
welcome_page = renderer.render(homepage_template, stats)
241+
self.wfile.write(str.encode(welcome_page))
242+
220243

221244
elif self.path.startswith('/timestamp/'):
222245
self.get_timestamp()

‎otsserver/stamper.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -300,13 +300,18 @@ def __do_bitcoin(self):
300300
self.pending_commitments.add(reorged_commitment_timestamp.msg)
301301

302302
# Check if this block contains any of the pending transactions
303-
304-
try:
305-
block = proxy.getblock(block_hash)
306-
except KeyError:
307-
# Must have been a reorg or something, return
308-
logging.error("Failed to get block")
309-
return
303+
block = None
304+
while block is None:
305+
try:
306+
block = proxy.getblock(block_hash)
307+
except KeyError:
308+
# Must have been a reorg or something, return
309+
logging.error("Failed to get block")
310+
return
311+
except BrokenPipeError:
312+
logging.error("BrokenPipeError to get block")
313+
time.sleep(5)
314+
proxy = bitcoin.rpc.Proxy()
310315

311316
# the following is an optimization, by pre computing the tx_id we rapidly check if our unconfirmed tx
312317
# is in the block

‎requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ opentimestamps>=0.2.1
22
GitPython>=2.0.8
33
leveldb>=0.20
44
pysha3>=1.0.2
5+
pystache>=0.5

0 commit comments

Comments
 (0)
Please sign in to comment.