18
18
import time
19
19
from urllib .parse import urlparse , urljoin
20
20
21
- PAGING = 1000
22
- SLEEP_SECS = 600
21
+ PAGING = 1000 # Number of commitments per chunk
22
+ SLEEP_SECS = 600 # Once the backup is synced this is the polling interval to check for new chunks
23
23
24
24
25
25
class Backup :
@@ -29,27 +29,36 @@ def __init__(self, journal, calendar, cache_path):
29
29
self .cache_path = cache_path
30
30
os .makedirs (cache_path , exist_ok = True )
31
31
32
+ # Return the bytes of the chunk
32
33
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.
33
39
cached_kv_bytes = self .read_disk_cache (chunk )
34
40
if cached_kv_bytes is not None :
35
41
return cached_kv_bytes
36
42
37
43
backup_map = {}
38
44
start = chunk * PAGING
39
45
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 ]:
41
50
try :
42
51
current = self .journal [i ]
43
- # print(str(i) +":"+b2x(journal[i]))
44
52
current_el = self .calendar [current ]
45
- # print("\t"+str(current_el))
46
53
self .__create_kv_map (current_el , current_el .msg , backup_map )
47
54
except KeyError :
55
+ # according to https://docs.python.org/3/library/exceptions.html#IndexError IndexError is the more
56
+ # appropriate exception for this case
48
57
raise IndexError
49
58
if i % 100 == 0 :
50
- logging .info ( str (i ) + ":" + b2x (self .journal [i ]))
59
+ logging .debug ( "Got commitment " + str (i ) + ":" + b2x (self .journal [i ]))
51
60
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 ))
53
62
kv_bytes = self .__kv_map_to_bytes (backup_map )
54
63
self .write_disk_cache (chunk , kv_bytes )
55
64
@@ -92,6 +101,7 @@ def __create_kv_map(ts, msg, kv_map):
92
101
@staticmethod
93
102
def __kv_map_to_bytes (kv_map ):
94
103
ctx = BytesSerializationContext ()
104
+ # Sorting the map elements to create chunks deterministically, but this is not mandatory for importing the chunk
95
105
for key , value in sorted (kv_map .items ()):
96
106
ctx .write_varuint (len (key ))
97
107
ctx .write_bytes (key )
@@ -101,8 +111,11 @@ def __kv_map_to_bytes(kv_map):
101
111
return ctx .getbytes ()
102
112
103
113
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
104
117
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
106
119
107
120
try :
108
121
cache_file = self .cache_path + '/' + chunk_path + '/' + chunk_str
@@ -120,7 +133,8 @@ def write_disk_cache(self, chunk, bytes):
120
133
with open (cache_file , 'wb' ) as fd :
121
134
fd .write (bytes )
122
135
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
124
138
class RPCRequestHandler (http .server .BaseHTTPRequestHandler ):
125
139
126
140
def do_GET (self ):
@@ -197,6 +211,9 @@ def serve_forever(self):
197
211
super ().serve_forever ()
198
212
199
213
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
200
217
class AskBackup (threading .Thread ):
201
218
202
219
def __init__ (self , db , calendar_url , base_path ):
@@ -208,36 +225,33 @@ def __init__(self, db, calendar_url, base_path):
208
225
super ().__init__ (target = self .loop )
209
226
210
227
def loop (self ):
211
- print ("Starting loop for %s" % self .calendar_url )
228
+ logging . info ("Starting loop for %s" % self .calendar_url )
212
229
213
230
try :
214
231
with open (self .up_to_path , 'r' ) as up_to_fd :
215
232
last_known = int (up_to_fd .read ().strip ())
216
233
except FileNotFoundError as exp :
217
234
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 ))
219
236
220
237
while True :
221
238
start_time = time .time ()
222
239
backup_url = urljoin (self .calendar_url , "/experimental/backup/%d" % (last_known + 1 ))
223
- print ( str (backup_url ))
240
+ logging . debug ( "Asking " + str (backup_url ))
224
241
try :
225
242
r = requests .get (backup_url )
226
243
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 ))
228
245
break
229
246
230
247
if r .status_code == 404 :
231
- print ("%s not found, sleeping for %s seconds" % (backup_url , SLEEP_SECS ) )
248
+ logging . info ("%s not found, sleeping for %s seconds" % (backup_url , SLEEP_SECS ) )
232
249
time .sleep (SLEEP_SECS )
233
250
continue
234
251
235
- # print(r.raw.read(10))
236
252
kv_map = Backup .bytes_to_kv_map (r .content )
237
- # print(str(map))
238
253
attestations = {}
239
254
ops = {}
240
- print ("kv_maps elements " + str (len (kv_map )))
241
255
for key , value in kv_map .items ():
242
256
# print("--- key=" + b2x(key) + " value=" + b2x(value))
243
257
ctx = BytesDeserializationContext (value )
@@ -252,31 +266,28 @@ def loop(self):
252
266
253
267
proxy = bitcoin .rpc .Proxy ()
254
268
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 )))
257
271
for key , attestation in attestations .items ():
258
272
if attestation .__class__ == BitcoinBlockHeaderAttestation :
259
273
blockhash = proxy .getblockhash (attestation .height )
260
274
block_header = proxy .getblockheader (blockhash )
275
+ # the following raise an exception and block computation if the attestation does not verify
261
276
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 ))
263
278
264
279
# verify all ops connects to an attestation
265
- print ( "total ops: " + str (len (ops )))
280
+ logging . debug ( "Total ops: " + str (len (ops )))
266
281
for key , op in ops .items ():
267
-
268
- # print("key " + b2x(key) + " op " + str(op))
269
282
current_key = key
270
283
current_op = op
271
284
while True :
272
285
next_key = current_op (current_key )
273
- # print("next_key " + b2x(next_key))
274
286
if next_key in ops :
275
287
current_key = next_key
276
288
current_op = ops [next_key ]
277
289
else :
278
290
break
279
- # print("maps to " + b2x(next_key))
280
291
assert next_key in attestations
281
292
282
293
batch = leveldb .WriteBatch ()
@@ -289,11 +300,11 @@ def loop(self):
289
300
with open (self .up_to_path , 'w' ) as up_to_fd :
290
301
up_to_fd .write ('%d\n ' % last_known )
291
302
except FileNotFoundError as exp :
292
- print (str (exp ))
303
+ logging . error (str (exp ))
293
304
break
294
305
295
306
elapsed_time = time .time () - start_time
296
- print ("Took %ds" % elapsed_time )
307
+ logging . info ("Took %ds for %s " % ( elapsed_time , str ( backup_url )) )
297
308
298
309
299
310
0 commit comments