Zcash changes to lib/(network|blockchain).py

This commit is contained in:
zebra-lucky 2018-06-16 04:29:29 +03:00
parent 59e3058de2
commit 4aea667280
2 changed files with 175 additions and 77 deletions

View File

@ -28,30 +28,54 @@ from . import bitcoin
from . import constants
from .bitcoin import *
MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
HDR_LEN = 1487
CHUNK_LEN = 100
MAX_TARGET = 0x0007FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
POW_AVERAGING_WINDOW = 17
POW_MEDIAN_BLOCK_SPAN = 11
POW_MAX_ADJUST_DOWN = 32
POW_MAX_ADJUST_UP = 16
POW_DAMPING_FACTOR = 4
POW_TARGET_SPACING = 150
AVERAGING_WINDOW_TIMESPAN = POW_AVERAGING_WINDOW * POW_TARGET_SPACING
MIN_ACTUAL_TIMESPAN = AVERAGING_WINDOW_TIMESPAN * \
(100 - POW_MAX_ADJUST_UP) // 100
MAX_ACTUAL_TIMESPAN = AVERAGING_WINDOW_TIMESPAN * \
(100 + POW_MAX_ADJUST_DOWN) // 100
def serialize_header(res):
s = int_to_hex(res.get('version'), 4) \
+ rev_hex(res.get('prev_block_hash')) \
+ rev_hex(res.get('merkle_root')) \
+ rev_hex(res.get('reserved_hash')) \
+ int_to_hex(int(res.get('timestamp')), 4) \
+ int_to_hex(int(res.get('bits')), 4) \
+ int_to_hex(int(res.get('nonce')), 4)
+ rev_hex(res.get('nonce')) \
+ rev_hex(res.get('sol_size')) \
+ rev_hex(res.get('solution'))
return s
def deserialize_header(s, height):
if not s:
raise Exception('Invalid header: {}'.format(s))
if len(s) != 80:
if len(s) != HDR_LEN:
raise Exception('Invalid header length: {}'.format(len(s)))
hex_to_int = lambda s: int('0x' + bh2u(s[::-1]), 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])
h['reserved_hash'] = hash_encode(s[68:100])
h['timestamp'] = hex_to_int(s[100:104])
h['bits'] = hex_to_int(s[104:108])
h['nonce'] = hash_encode(s[108:140])
h['sol_size'] = hash_encode(s[140:143])
h['solution'] = hash_encode(s[143:1487])
h['block_height'] = height
return h
@ -151,7 +175,7 @@ class Blockchain(util.PrintError):
def update_size(self):
p = self.path()
self._size = os.path.getsize(p)//80 if os.path.exists(p) else 0
self._size = os.path.getsize(p)//HDR_LEN if os.path.exists(p) else 0
def verify_header(self, header, prev_hash, target):
_hash = hash_header(header)
@ -166,13 +190,21 @@ class Blockchain(util.PrintError):
raise Exception("insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target))
def verify_chunk(self, index, data):
num = len(data) // 80
prev_hash = self.get_hash(index * 2016 - 1)
target = self.get_target(index-1)
num = len(data) // HDR_LEN
prev_hash = self.get_hash(index * CHUNK_LEN - 1)
chunk_headers = {'empty': True}
for i in range(num):
raw_header = data[i*80:(i+1) * 80]
header = deserialize_header(raw_header, index*2016 + i)
raw_header = data[i*HDR_LEN:(i+1) * HDR_LEN]
height = index * CHUNK_LEN + i
header = deserialize_header(raw_header, height)
target = self.get_target(height, chunk_headers)
self.verify_header(header, prev_hash, target)
chunk_headers[height] = header
if i == 0:
chunk_headers['min_height'] = height
chunk_headers['empty'] = False
chunk_headers['max_height'] = height
prev_hash = hash_header(header)
def path(self):
@ -182,7 +214,7 @@ class Blockchain(util.PrintError):
def save_chunk(self, index, chunk):
filename = self.path()
d = (index * 2016 - self.checkpoint) * 80
d = (index * CHUNK_LEN - self.checkpoint) * HDR_LEN
if d < 0:
chunk = chunk[-d:]
d = 0
@ -203,10 +235,10 @@ class Blockchain(util.PrintError):
with open(self.path(), 'rb') as f:
my_data = f.read()
with open(parent.path(), 'rb') as f:
f.seek((checkpoint - parent.checkpoint)*80)
parent_data = f.read(parent_branch_size*80)
f.seek((checkpoint - parent.checkpoint)*HDR_LEN)
parent_data = f.read(parent_branch_size*HDR_LEN)
self.write(parent_data, 0)
parent.write(my_data, (checkpoint - parent.checkpoint)*80)
parent.write(my_data, (checkpoint - parent.checkpoint)*HDR_LEN)
# store file path
for b in blockchains.values():
b.old_path = b.path()
@ -228,7 +260,7 @@ class Blockchain(util.PrintError):
filename = self.path()
with self.lock:
with open(filename, 'rb+') as f:
if truncate and offset != self._size*80:
if truncate and offset != self._size*HDR_LEN:
f.seek(offset)
f.truncate()
f.seek(offset)
@ -241,8 +273,8 @@ class Blockchain(util.PrintError):
delta = header.get('block_height') - self.checkpoint
data = bfh(serialize_header(header))
assert delta == self.size()
assert len(data) == 80
self.write(data, delta*80)
assert len(data) == HDR_LEN
self.write(data, delta*HDR_LEN)
self.swap_with_parent()
def read_header(self, height):
@ -257,56 +289,98 @@ class Blockchain(util.PrintError):
name = self.path()
if os.path.exists(name):
with open(name, 'rb') as f:
f.seek(delta * 80)
h = f.read(80)
if len(h) < 80:
f.seek(delta * HDR_LEN)
h = f.read(HDR_LEN)
if len(h) < HDR_LEN:
raise Exception('Expected to read a full header. This was only {} bytes'.format(len(h)))
elif not os.path.exists(util.get_headers_dir(self.config)):
raise Exception('Electrum datadir does not exist. Was it deleted while running?')
else:
raise Exception('Cannot find headers file but datadir is there. Should be at {}'.format(name))
if h == bytes([0])*80:
if h == bytes([0])*HDR_LEN:
return None
return deserialize_header(h, height)
def get_hash(self, height):
len_checkpoints = len(self.checkpoints)
if height == -1:
return '0000000000000000000000000000000000000000000000000000000000000000'
elif height == 0:
return constants.net.GENESIS
elif height < len(self.checkpoints) * 2016:
assert (height+1) % 2016 == 0, height
index = height // 2016
elif height < len_checkpoints * CHUNK_LEN - POW_AVERAGING_WINDOW:
assert (height+1) % CHUNK_LEN == 0, height
index = height // CHUNK_LEN
h, t = self.checkpoints[index]
return h
else:
return hash_header(self.read_header(height))
def get_target(self, index):
# compute target from chunk x, used in chunk x+1
if constants.net.TESTNET:
return 0
if index == -1:
def get_median_time(self, height, chunk_headers=None):
if chunk_headers is None or chunk_headers['empty']:
chunk_empty = True
else:
chunk_empty = False
min_height = chunk_headers['min_height']
max_height = chunk_headers['max_height']
height_range = range(max(0, height - POW_MEDIAN_BLOCK_SPAN),
max(1, height))
median = []
for h in height_range:
header = self.read_header(h)
if not header and not chunk_empty \
and min_height <= h <= max_height:
header = chunk_headers[h]
assert header and header.get('block_height') == h
median.append(header.get('timestamp'))
median.sort()
return median[len(median)//2];
def get_target(self, height, chunk_headers=None):
if chunk_headers is None or chunk_headers['empty']:
chunk_empty = True
else:
chunk_empty = False
min_height = chunk_headers['min_height']
max_height = chunk_headers['max_height']
if height <= POW_AVERAGING_WINDOW:
return MAX_TARGET
if index < len(self.checkpoints):
h, t = self.checkpoints[index]
return t
# new target
first = self.read_header(index * 2016)
last = self.read_header(index * 2016 + 2015)
bits = last.get('bits')
target = self.bits_to_target(bits)
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)
return new_target
height_range = range(max(0, height - POW_AVERAGING_WINDOW),
max(1, height))
mean_target = 0
for h in height_range:
header = self.read_header(h)
if not header and not chunk_empty \
and min_height <= h <= max_height:
header = chunk_headers[h]
assert header and header.get('block_height') == h
mean_target += self.bits_to_target(header.get('bits'))
mean_target //= POW_AVERAGING_WINDOW
actual_timespan = self.get_median_time(height, chunk_headers) - \
self.get_median_time(height - POW_AVERAGING_WINDOW, chunk_headers)
actual_timespan = AVERAGING_WINDOW_TIMESPAN + \
int((actual_timespan - AVERAGING_WINDOW_TIMESPAN) / \
POW_DAMPING_FACTOR)
if actual_timespan < MIN_ACTUAL_TIMESPAN:
actual_timespan = MIN_ACTUAL_TIMESPAN
elif actual_timespan > MAX_ACTUAL_TIMESPAN:
actual_timespan = MAX_ACTUAL_TIMESPAN
next_target = mean_target // AVERAGING_WINDOW_TIMESPAN * actual_timespan
if next_target > MAX_TARGET:
next_target = MAX_TARGET
return next_target
def bits_to_target(self, bits):
bitsN = (bits >> 24) & 0xff
if not (bitsN >= 0x03 and bitsN <= 0x1d):
raise Exception("First part of bits should be in [0x03, 0x1d]")
if not (bitsN >= 0x03 and bitsN <= 0x1f):
raise Exception("First part of bits should be in [0x03, 0x1f]")
bitsBase = bits & 0xffffff
if not (bitsBase >= 0x8000 and bitsBase <= 0x7fffff):
raise Exception("Second part of bits should be in [0x8000, 0x7fffff]")
@ -337,7 +411,7 @@ class Blockchain(util.PrintError):
return False
if prev_hash != header.get('prev_block_hash'):
return False
target = self.get_target(height // 2016 - 1)
target = self.get_target(height)
try:
self.verify_header(header, prev_hash, target)
except BaseException as e:
@ -358,9 +432,10 @@ class Blockchain(util.PrintError):
def get_checkpoints(self):
# for each chunk, store the hash of the last block and the target after the chunk
cp = []
n = self.height() // 2016
n = self.height() // CHUNK_LEN
for index in range(n):
h = self.get_hash((index+1) * 2016 -1)
target = self.get_target(index)
height = (index + 1) * CHUNK_LEN - 1
h = self.get_hash(height)
target = self.get_target(height)
cp.append((h, target))
return cp

View File

@ -37,6 +37,7 @@ import socks
from . import util
from . import bitcoin
from .bitcoin import *
from .blockchain import HDR_LEN, CHUNK_LEN
from . import constants
from .interface import Connection, Interface
from . import blockchain
@ -566,10 +567,14 @@ class Network(util.DaemonThread):
if error is None:
self.relay_fee = int(result * COIN) if result is not None else None
self.print_error("relayfee", self.relay_fee)
elif method == 'blockchain.block.get_chunk':
self.on_get_chunk(interface, response)
elif method == 'blockchain.block.get_header':
self.on_get_header(interface, response)
elif method == 'blockchain.block.headers':
height, count = params
if count == 1:
self.on_get_header(interface, response, height)
elif count == CHUNK_LEN:
self.on_get_chunk(interface, response, height)
else:
self.print_error('Unknown chunk lenght: %s' % count)
for callback in callbacks:
callback(response)
@ -717,7 +722,7 @@ class Network(util.DaemonThread):
interface.mode = 'default'
interface.request = None
self.interfaces[server] = interface
self.queue_request('blockchain.headers.subscribe', [], interface)
self.queue_request('blockchain.headers.subscribe', [True], interface)
if server == self.default_server:
self.switch_to_interface(server)
#self.notify('interfaces')
@ -773,18 +778,18 @@ class Network(util.DaemonThread):
return
interface.print_error("requesting chunk %d" % index)
self.requested_chunks.add(index)
self.queue_request('blockchain.block.get_chunk', [index], interface)
self.queue_request('blockchain.block.headers',
[CHUNK_LEN*index, CHUNK_LEN], interface)
def on_get_chunk(self, interface, response):
def on_get_chunk(self, interface, response, height):
'''Handle receiving a chunk of block headers'''
error = response.get('error')
result = response.get('result')
params = response.get('params')
blockchain = interface.blockchain
if result is None or params is None or error is not None:
if result is None or error is not None:
interface.print_error(error or 'bad response')
return
index = params[0]
index = height // CHUNK_LEN
# Ignore unsolicited chunks
if index not in self.requested_chunks:
interface.print_error("received chunk %d (unsolicited)" % index)
@ -792,7 +797,8 @@ class Network(util.DaemonThread):
else:
interface.print_error("received chunk %d" % index)
self.requested_chunks.remove(index)
connect = blockchain.connect_chunk(index, result)
hex_chunk = result.get('hex', None)
connect = blockchain.connect_chunk(index, hex_chunk)
if not connect:
self.connection_down(interface.server)
return
@ -807,22 +813,31 @@ class Network(util.DaemonThread):
def request_header(self, interface, height):
#interface.print_error("requesting header %d" % height)
self.queue_request('blockchain.block.get_header', [height], interface)
self.queue_request('blockchain.block.headers', [height, 1], interface)
interface.request = height
interface.req_time = time.time()
def on_get_header(self, interface, response):
def on_get_header(self, interface, response, height):
'''Handle receiving a single block header'''
header = response.get('result')
if not header:
interface.print_error(response)
self.connection_down(interface.server)
return
height = header.get('block_height')
result = response.get('result', {})
hex_header = result.get('hex', None)
if interface.request != height:
interface.print_error("unsolicited header",interface.request, height)
self.connection_down(interface.server)
return
if not hex_header:
interface.print_error(response)
self.connection_down(interface.server)
return
if len(hex_header) != HDR_LEN*2:
interface.print_error('wrong header length', interface.request)
self.connection_down(interface.server)
return
header = blockchain.deserialize_header(bfh(hex_header), height)
chain = blockchain.check_header(header)
if interface.mode == 'backward':
can_connect = blockchain.can_connect(header)
@ -925,14 +940,14 @@ class Network(util.DaemonThread):
else:
raise Exception(interface.mode)
# If not finished, get the next header
interface.request = None
if next_height:
if interface.mode == 'catch_up' and interface.tip > next_height + 50:
self.request_chunk(interface, next_height // 2016)
self.request_chunk(interface, next_height // CHUNK_LEN)
else:
self.request_header(interface, next_height)
else:
interface.mode = 'default'
interface.request = None
self.notify('updated')
# refresh network dialog
self.notify('interfaces')
@ -969,7 +984,7 @@ class Network(util.DaemonThread):
def init_headers_file(self):
b = self.blockchains[0]
filename = b.path()
length = 80 * len(constants.net.CHECKPOINTS) * 2016
length = HDR_LEN * len(constants.net.CHECKPOINTS) * CHUNK_LEN
if not os.path.exists(filename) or os.path.getsize(filename) < length:
with open(filename, 'wb') as f:
if length>0:
@ -990,9 +1005,17 @@ class Network(util.DaemonThread):
self.on_stop()
def on_notify_header(self, interface, header):
height = header.get('block_height')
if not height:
height = header.get('height')
hex_header = header.get('hex')
if not height or not hex_header:
return
if len(hex_header) != HDR_LEN*2:
interface.print_error('wrong header length', interface.request)
self.connection_down(interface.server)
return
header = blockchain.deserialize_header(bfh(hex_header), height)
if height < self.max_checkpoint():
self.connection_down(interface.server)
return
@ -1094,4 +1117,4 @@ class Network(util.DaemonThread):
f.write(json.dumps(cp, indent=4))
def max_checkpoint(self):
return max(0, len(constants.net.CHECKPOINTS) * 2016 - 1)
return max(0, len(constants.net.CHECKPOINTS) * CHUNK_LEN - 1)