#!/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 . import os import util from bitcoin import * class Blockchain(util.PrintError): '''Manages blockchain headers and their verification''' def __init__(self, config, network): self.config = config self.network = network self.headers_url = 'https://headers.electrum.org/blockchain_headers' self.local_height = 0 self.set_local_height() def height(self): return self.local_height def init(self): self.init_headers_file() self.set_local_height() self.print_error("%d blocks" % self.local_height) 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) if prev_hash != header.get('prev_block_hash'): self.print_error("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash'))) return False try: bits, target = self.get_target(height/2016, chain) except AssertionError as e: self.print_error("target calculation error: %s" % (str(e))) return False if bits != header.get('bits'): self.print_error("bits mismatch: %s vs %s" % (bits, header.get('bits'))) return False _hash = self.hash_header(header) if int('0x'+_hash, 16) > target: self.print_error("insufficient proof of work: %s vs target %s" % (int('0x'+_hash, 16), target)) 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 to height %d" % (index, 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 os.path.join(self.config.path, 'blockchain_headers') 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.set_local_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.set_local_height() def set_local_height(self): name = self.path() if os.path.exists(name): h = os.path.getsize(name)/80 - 1 if self.local_height != h: self.local_height = h 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 # bits to target bits = last.get('bits') bitsN = (bits >> 24) & 0xff assert bitsN >= 0x03 and bitsN <= 0x1d, "First part of bits should be in [0x03, 0x1d]" bitsBase = bits & 0xffffff assert bitsN >= 0x8000 and bitsN <= 0x7fffff, "Second part of bits should be in [0x8000, 0x7fffff]" target = bitsBase << (8*(bitsN-3)) # new target nActualTimespan = last.get('timestamp') - first.get('timestamp') nTargetTimespan = 14*24*60*60 nActualTimespan = max(nActualTimespan, nTargetTimespan/4) nActualTimespan = min(nActualTimespan, nTargetTimespan*4) new_target = min(max_target, (target * nActualTimespan)/nTargetTimespan) # convert new target to bits c = ("%064x" % new_target)[2:] while c[:2] == '00' and len(c) > 6: c = c[2:] bitsN, bitsBase = len(c)/2, int('0x'+c[:6], 16) if bitsBase >= 0x800000: bitsN += 1 bitsBase >>= 8 new_bits = bitsN << 24 | bitsBase return new_bits, bitsBase << (8*(bitsN-3)) def connect_header(self, chain, header): '''Builds a header chain until it connects. Returns True if it has successfully connected, False if verification failed, otherwise the height of the next header needed.''' chain.append(header) # Ordered by decreasing height previous_height = header['block_height'] - 1 previous_header = self.read_header(previous_height) # Missing header, request it if not previous_header: return previous_height # Does it connect to my chain? prev_hash = self.hash_header(previous_header) if prev_hash != header.get('prev_block_hash'): self.print_error("reorg") return previous_height # The chain is complete. Reverse to order by increasing height chain.reverse() if self.verify_chain(chain): self.print_error("connected at height:", previous_height) for header in chain: self.save_header(header) return True return False def connect_chunk(self, idx, chunk): try: self.verify_chunk(idx, chunk) return idx + 1 except Exception: self.print_error('verify_chunk failed') return idx - 1