LBRY-Vault/lib/blockchain.py
Neil Booth 175bfae9e6 Move away from requiring network and blockchain objects to be able to request local height.
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.
2015-05-03 15:19:29 +09:00

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