Zcash changes to lib/(network|blockchain).py
This commit is contained in:
parent
59e3058de2
commit
4aea667280
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue