mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
We store it in the config object instead of in the blockchain object. The blockchain object now refers to its config, and calls refresh_height() to update it. The network objects also refer to the config rather than the blockchain. This is the first of many small steps to untangle the verifier from stored state and so permit the history tab to work in offline mode. The refactoring will simultaneously clean up a lot of accumulated cruft.
328 lines
10 KiB
Python
328 lines
10 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# Electrum - lightweight Bitcoin client
|
|
# Copyright (C) 2012 thomasv@ecdsa.org
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
import threading, time, Queue, os, sys, shutil
|
|
from util import user_dir, print_error
|
|
import util
|
|
from bitcoin import *
|
|
|
|
|
|
class Blockchain(util.DaemonThread):
|
|
|
|
def __init__(self, config, network):
|
|
util.DaemonThread.__init__(self)
|
|
self.config = config
|
|
self.network = network
|
|
self.lock = threading.Lock()
|
|
self.headers_url = 'http://headers.electrum.org/blockchain_headers'
|
|
self.queue = Queue.Queue()
|
|
|
|
def run(self):
|
|
self.init_headers_file()
|
|
self.print_error("%d blocks" % self.config.height)
|
|
|
|
while self.is_running():
|
|
try:
|
|
result = self.queue.get(timeout=0.1)
|
|
except Queue.Empty:
|
|
continue
|
|
if not result:
|
|
continue
|
|
i, header = result
|
|
if not header:
|
|
continue
|
|
height = header.get('block_height')
|
|
if height <= self.config.height:
|
|
continue
|
|
if height > self.config.height + 50:
|
|
if not self.get_and_verify_chunks(i, header, height):
|
|
continue
|
|
if height > self.config.height:
|
|
# get missing parts from interface (until it connects to my chain)
|
|
chain = self.get_chain( i, header )
|
|
# skip that server if the result is not consistent
|
|
if not chain:
|
|
self.print_error('e')
|
|
continue
|
|
# verify the chain
|
|
if self.verify_chain( chain ):
|
|
self.print_error("height:", height, i.server)
|
|
for header in chain:
|
|
self.save_header(header)
|
|
else:
|
|
self.print_error("error", i.server)
|
|
# todo: dismiss that server
|
|
continue
|
|
self.network.new_blockchain_height(height, i)
|
|
|
|
self.print_error("stopped")
|
|
|
|
|
|
def verify_chain(self, chain):
|
|
|
|
first_header = chain[0]
|
|
prev_header = self.read_header(first_header.get('block_height') -1)
|
|
|
|
for header in chain:
|
|
|
|
height = header.get('block_height')
|
|
|
|
prev_hash = self.hash_header(prev_header)
|
|
bits, target = self.get_target(height/2016, chain)
|
|
_hash = self.hash_header(header)
|
|
try:
|
|
assert prev_hash == header.get('prev_block_hash')
|
|
assert bits == header.get('bits')
|
|
assert int('0x'+_hash,16) < target
|
|
except Exception:
|
|
return False
|
|
|
|
prev_header = header
|
|
|
|
return True
|
|
|
|
|
|
|
|
def verify_chunk(self, index, hexdata):
|
|
data = hexdata.decode('hex')
|
|
height = index*2016
|
|
num = len(data)/80
|
|
|
|
if index == 0:
|
|
previous_hash = ("0"*64)
|
|
else:
|
|
prev_header = self.read_header(index*2016-1)
|
|
if prev_header is None: raise
|
|
previous_hash = self.hash_header(prev_header)
|
|
|
|
bits, target = self.get_target(index)
|
|
|
|
for i in range(num):
|
|
height = index*2016 + i
|
|
raw_header = data[i*80:(i+1)*80]
|
|
header = self.header_from_string(raw_header)
|
|
_hash = self.hash_header(header)
|
|
assert previous_hash == header.get('prev_block_hash')
|
|
assert bits == header.get('bits')
|
|
assert int('0x'+_hash,16) < target
|
|
|
|
previous_header = header
|
|
previous_hash = _hash
|
|
|
|
self.save_chunk(index, data)
|
|
self.print_error("validated chunk %d"%height)
|
|
|
|
|
|
|
|
def header_to_string(self, res):
|
|
s = int_to_hex(res.get('version'),4) \
|
|
+ rev_hex(res.get('prev_block_hash')) \
|
|
+ rev_hex(res.get('merkle_root')) \
|
|
+ int_to_hex(int(res.get('timestamp')),4) \
|
|
+ int_to_hex(int(res.get('bits')),4) \
|
|
+ int_to_hex(int(res.get('nonce')),4)
|
|
return s
|
|
|
|
|
|
def header_from_string(self, s):
|
|
hex_to_int = lambda s: int('0x' + s[::-1].encode('hex'), 16)
|
|
h = {}
|
|
h['version'] = hex_to_int(s[0:4])
|
|
h['prev_block_hash'] = hash_encode(s[4:36])
|
|
h['merkle_root'] = hash_encode(s[36:68])
|
|
h['timestamp'] = hex_to_int(s[68:72])
|
|
h['bits'] = hex_to_int(s[72:76])
|
|
h['nonce'] = hex_to_int(s[76:80])
|
|
return h
|
|
|
|
def hash_header(self, header):
|
|
return rev_hex(Hash(self.header_to_string(header).decode('hex')).encode('hex'))
|
|
|
|
def path(self):
|
|
return self.config.headers_filename()
|
|
|
|
def init_headers_file(self):
|
|
filename = self.path()
|
|
if os.path.exists(filename):
|
|
return
|
|
|
|
try:
|
|
import urllib, socket
|
|
socket.setdefaulttimeout(30)
|
|
self.print_error("downloading ", self.headers_url )
|
|
urllib.urlretrieve(self.headers_url, filename)
|
|
self.print_error("done.")
|
|
except Exception:
|
|
self.print_error( "download failed. creating file", filename )
|
|
open(filename,'wb+').close()
|
|
|
|
def save_chunk(self, index, chunk):
|
|
filename = self.path()
|
|
f = open(filename,'rb+')
|
|
f.seek(index*2016*80)
|
|
h = f.write(chunk)
|
|
f.close()
|
|
self.config.refresh_height()
|
|
|
|
def save_header(self, header):
|
|
data = self.header_to_string(header).decode('hex')
|
|
assert len(data) == 80
|
|
height = header.get('block_height')
|
|
filename = self.path()
|
|
f = open(filename,'rb+')
|
|
f.seek(height*80)
|
|
h = f.write(data)
|
|
f.close()
|
|
self.config.refresh_height()
|
|
|
|
def read_header(self, block_height):
|
|
name = self.path()
|
|
if os.path.exists(name):
|
|
f = open(name,'rb')
|
|
f.seek(block_height*80)
|
|
h = f.read(80)
|
|
f.close()
|
|
if len(h) == 80:
|
|
h = self.header_from_string(h)
|
|
return h
|
|
|
|
|
|
def get_target(self, index, chain=None):
|
|
if chain is None:
|
|
chain = [] # Do not use mutables as default values!
|
|
|
|
max_target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
|
|
if index == 0: return 0x1d00ffff, max_target
|
|
|
|
first = self.read_header((index-1)*2016)
|
|
last = self.read_header(index*2016-1)
|
|
if last is None:
|
|
for h in chain:
|
|
if h.get('block_height') == index*2016-1:
|
|
last = h
|
|
|
|
nActualTimespan = last.get('timestamp') - first.get('timestamp')
|
|
nTargetTimespan = 14*24*60*60
|
|
nActualTimespan = max(nActualTimespan, nTargetTimespan/4)
|
|
nActualTimespan = min(nActualTimespan, nTargetTimespan*4)
|
|
|
|
bits = last.get('bits')
|
|
# convert to bignum
|
|
MM = 256*256*256
|
|
a = bits%MM
|
|
if a < 0x8000:
|
|
a *= 256
|
|
target = (a) * pow(2, 8 * (bits/MM - 3))
|
|
|
|
# new target
|
|
new_target = min( max_target, (target * nActualTimespan)/nTargetTimespan )
|
|
|
|
# convert it to bits
|
|
c = ("%064X"%new_target)[2:]
|
|
i = 31
|
|
while c[0:2]=="00":
|
|
c = c[2:]
|
|
i -= 1
|
|
|
|
c = int('0x'+c[0:6],16)
|
|
if c >= 0x800000:
|
|
c /= 256
|
|
i += 1
|
|
|
|
new_bits = c + MM * i
|
|
return new_bits, new_target
|
|
|
|
|
|
def request_header(self, i, h, queue):
|
|
self.print_error("requesting header %d from %s"%(h, i.server))
|
|
i.send_request({'method':'blockchain.block.get_header', 'params':[h]}, queue)
|
|
|
|
def retrieve_request(self, queue):
|
|
t = time.time()
|
|
while self.is_running():
|
|
try:
|
|
ir = queue.get(timeout=0.1)
|
|
except Queue.Empty:
|
|
if time.time() - t > 10:
|
|
return
|
|
else:
|
|
continue
|
|
i, r = ir
|
|
result = r['result']
|
|
return result
|
|
|
|
def get_chain(self, interface, final_header):
|
|
|
|
header = final_header
|
|
chain = [ final_header ]
|
|
requested_header = False
|
|
queue = Queue.Queue()
|
|
|
|
while self.is_running():
|
|
|
|
if requested_header:
|
|
header = self.retrieve_request(queue)
|
|
if not header:
|
|
self.print_error('chain request timed out, giving up')
|
|
return
|
|
chain = [ header ] + chain
|
|
requested_header = False
|
|
|
|
height = header.get('block_height')
|
|
previous_header = self.read_header(height -1)
|
|
if not previous_header:
|
|
self.request_header(interface, height - 1, queue)
|
|
requested_header = True
|
|
continue
|
|
|
|
# verify that it connects to my chain
|
|
prev_hash = self.hash_header(previous_header)
|
|
if prev_hash != header.get('prev_block_hash'):
|
|
self.print_error("reorg")
|
|
self.request_header(interface, height - 1, queue)
|
|
requested_header = True
|
|
continue
|
|
|
|
else:
|
|
# the chain is complete
|
|
return chain
|
|
|
|
|
|
def get_and_verify_chunks(self, i, header, height):
|
|
|
|
queue = Queue.Queue()
|
|
min_index = (self.config.height + 1)/2016
|
|
max_index = (height + 1)/2016
|
|
n = min_index
|
|
while n < max_index + 1:
|
|
self.print_error( "Requesting chunk:", n )
|
|
i.send_request({'method':'blockchain.block.get_chunk', 'params':[n]}, queue)
|
|
r = self.retrieve_request(queue)
|
|
if not r:
|
|
return False
|
|
try:
|
|
self.verify_chunk(n, r)
|
|
n = n + 1
|
|
except Exception:
|
|
self.print_error('Verify chunk failed!')
|
|
n = n - 1
|
|
if n < 0:
|
|
return False
|
|
|
|
return True
|