diff --git a/.gitignore b/.gitignore index dae35e75..5496f334 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,8 @@ bin/ # tox files .cache/ .coverage + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries diff --git a/.idea/electrum.iml b/.idea/electrum.iml new file mode 100644 index 00000000..849be4f7 --- /dev/null +++ b/.idea/electrum.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..98a50070 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..fba7c97b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..94a25f7f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.rst b/README.rst index 43d3257f..90feb11d 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,7 @@ Electrum is a pure python application. If you want to use the Qt interface, install the Qt dependencies:: sudo apt-get install python3-pyqt5 + sudo pip2 install pyblake2 If you downloaded the official package (tar.gz), you can run Electrum from its root directory, without installing it on your diff --git a/lib/__init__.py b/lib/__init__.py index 286e4b6a..4fb98550 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -9,6 +9,7 @@ from .simple_config import SimpleConfig, get_config, set_config from . import bitcoin from . import transaction from . import daemon +from . import equihash from .transaction import Transaction from .plugins import BasePlugin from .commands import Commands, known_commands diff --git a/lib/bitcoin.py b/lib/bitcoin.py index 8c4e8726..c22cc8c0 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -29,6 +29,7 @@ import hmac import os import json +import struct import ecdsa import pyaes @@ -78,7 +79,8 @@ class NetworkConstants: cls.ADDRTYPE_P2PKH = 0 cls.ADDRTYPE_P2SH = 5 cls.SEGWIT_HRP = "bc" - cls.GENESIS = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" + cls.HEADERS_URL = "https://headers.electrum.org/blockchain_headers" #TODO + cls.GENESIS = "0007104ccda289427919efc39dc9e4d499804b7bebc22df55f8b834301260602" cls.DEFAULT_PORTS = {'t': '50001', 's': '50002'} cls.DEFAULT_SERVERS = read_json('servers.json', {}) cls.CHECKPOINTS = read_json('checkpoints.json', []) @@ -237,6 +239,75 @@ def op_push(i): def push_script(x): return op_push(len(x)//2) + x +# ZCASH specific utils methods +# https://github.com/zcash/zcash/blob/master/qa/rpc-tests/test_framework/mininode.py +def ser_char_vector(l): + r = b'' + if l is None: + l = '' + if len(l) < 253: + r = chr(len(l)) + elif len(l) < 0x10000: + r = chr(253) + struct.pack(">= 32 + return rs + + +def uint256_from_str(s): + r = 0 + t = struct.unpack(" target: - raise BaseException("insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target)) + if int('0x' + _powhash, 16) > target: + raise BaseException("insufficient proof of work: %s vs target %s" % (int('0x' + _powhash, 16), target)) + if not is_gbp_valid(nonce, n_solution): + raise BaseException("Equihash invalid") 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) / 1484 + prev_header = None + if index != 0: + prev_header = self.read_header(index * 2016 - 1) + headers = {} for i in range(num): - raw_header = data[i*80:(i+1) * 80] - header = deserialize_header(raw_header, index*2016 + i) - self.verify_header(header, prev_hash, target) - prev_hash = hash_header(header) + raw_header = data[i * 1484:(i + 1) * 1484] + header = self.deserialize_header(raw_header, index * 2016 + i) + headers[header.get('block_height')] = header + nonce, n_solution = headers.get('nonce'), header.get('n_solution') + bits, target = self.get_target(index * 2016 + i, headers) + self.verify_header(header, prev_header, bits, target, nonce, n_solution) + prev_header = header def path(self): d = util.get_headers_dir(self.config) @@ -177,7 +199,7 @@ class Blockchain(util.PrintError): def save_chunk(self, index, chunk): filename = self.path() - d = (index * 2016 - self.checkpoint) * 80 + d = (index * 2016 - self.checkpoint) * 1484 if d < 0: chunk = chunk[-d:] d = 0 @@ -197,10 +219,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)*1484) + parent_data = f.read(parent_branch_size*1484) self.write(parent_data, 0) - parent.write(my_data, (checkpoint - parent.checkpoint)*80) + parent.write(my_data, (checkpoint - parent.checkpoint)*1484) # store file path for b in blockchains.values(): b.old_path = b.path() @@ -222,7 +244,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 offset != self._size*1484: f.seek(offset) f.truncate() f.seek(offset) @@ -235,8 +257,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) == 1484 + self.write(data, delta*1484) self.swap_with_parent() def read_header(self, height): @@ -247,15 +269,16 @@ class Blockchain(util.PrintError): return self.parent().read_header(height) if height > self.height(): return - delta = height - self.checkpoint + + idx, h = 0, None name = self.path() + if os.path.exists(name): - with open(name, 'rb') as f: - f.seek(delta * 80) - h = f.read(80) - if h == bytes([0])*80: - return None - return deserialize_header(h, height) + while idx <= height: + f = open(name, 'rb') + h = deserialize_header(f, height) + idx += 1 + return h def get_hash(self, height): if height == -1: @@ -323,10 +346,11 @@ class Blockchain(util.PrintError): return False if prev_hash != header.get('prev_block_hash'): return False - target = self.get_target(height // 2016 - 1) + nonce, n_solution = headers.get('nonce'), header.get('n_solution') + bits, target = self.get_target(index * 2016 + i, headers) try: - self.verify_header(header, prev_hash, target) - except BaseException as e: + self.verify_header(header, prev_header, bits, target, nonce, n_solution) + except: return False return True diff --git a/lib/equihash.py b/lib/equihash.py new file mode 100644 index 00000000..bd637889 --- /dev/null +++ b/lib/equihash.py @@ -0,0 +1,321 @@ +# ZCASH implementation: https://github.com/zcash/zcash/blob/master/qa/rpc-tests/test_framework/equihash.py +from pyblake2 import blake2b +from operator import itemgetter +import struct + +DEBUG = False +VERBOSE = False + + +word_size = 32 +word_mask = (1<= 8 and word_size >= 7+bit_len + bit_len_mask = (1<= bit_len: + acc_bits -= bit_len + for x in range(byte_pad, out_width): + out[j+x] = ( + # Big-endian + acc_value >> (acc_bits+(8*(out_width-x-1))) + ) & ( + # Apply bit_len_mask across byte boundaries + (bit_len_mask >> (8*(out_width-x-1))) & 0xFF + ) + j += out_width + + return out + + +def compress_array(inp, out_len, bit_len, byte_pad=0): + assert bit_len >= 8 and word_size >= 7+bit_len + + in_width = (bit_len+7)/8 + byte_pad + assert out_len == bit_len*len(inp)/(8*in_width) + out = bytearray(out_len) + + bit_len_mask = (1 << bit_len) - 1 + + # The acc_bits least-significant bits of acc_value represent a bit sequence + # in big-endian order. + acc_bits = 0; + acc_value = 0; + + j = 0 + for i in range(out_len): + # When we have fewer than 8 bits left in the accumulator, read the next + # input element. + if acc_bits < 8: + acc_value = ((acc_value << bit_len) & word_mask) | inp[j] + for x in range(byte_pad, in_width): + acc_value = acc_value | ( + ( + # Apply bit_len_mask across byte boundaries + inp[j+x] & ((bit_len_mask >> (8*(in_width-x-1))) & 0xFF) + ) << (8*(in_width-x-1))); # Big-endian + j += in_width + acc_bits += bit_len + + acc_bits -= 8 + out[i] = (acc_value >> acc_bits) & 0xFF + + return out + + +def get_indices_from_minimal(minimal, bit_len): + eh_index_size = 4 + assert (bit_len+7)/8 <= eh_index_size + len_indices = 8*eh_index_size*len(minimal)/bit_len + byte_pad = eh_index_size - (bit_len+7)/8 + expanded = expand_array(minimal, len_indices, bit_len, byte_pad) + return [struct.unpack('>I', expanded[i:i+4])[0] for i in range(0, len_indices, eh_index_size)] + + +def get_minimal_from_indices(indices, bit_len): + eh_index_size = 4 + assert (bit_len+7)/8 <= eh_index_size + len_indices = len(indices)*eh_index_size + min_len = bit_len*len_indices/(8*eh_index_size) + byte_pad = eh_index_size - (bit_len+7)/8 + byte_indices = bytearray(''.join([struct.pack('>I', i) for i in indices])) + return compress_array(byte_indices, min_len, bit_len, byte_pad) + + +def hash_nonce(digest, nonce): + for i in range(8): + digest.update(struct.pack('> (32*i))) + + +def hash_xi(digest, xi): + digest.update(struct.pack(' 0: + # 2b) Find next set of unordered pairs with collisions on first n/(k+1) bits + j = 1 + while j < len(X): + if not has_collision(X[-1][0], X[-1-j][0], i, collision_length): + break + j += 1 + + # 2c) Store tuples (X_i ^ X_j, (i, j)) on the table + for l in range(0, j-1): + for m in range(l+1, j): + # Check that there are no duplicate indices in tuples i and j + if distinct_indices(X[-1-l][1], X[-1-m][1]): + if X[-1-l][1][0] < X[-1-m][1][0]: + concat = X[-1-l][1] + X[-1-m][1] + else: + concat = X[-1-m][1] + X[-1-l][1] + Xc.append((xor(X[-1-l][0], X[-1-m][0]), concat)) + + # 2d) Drop this set + while j > 0: + X.pop(-1) + j -= 1 + # 2e) Replace previous list with new list + X = Xc + + # k+1) Find a collision on last 2n(k+1) bits + if DEBUG: + print('Final round:') + print('- Sorting list') + X.sort(key=itemgetter(0)) + if DEBUG and VERBOSE: + for Xi in X[-32:]: + print('%s %s' % (print_hash(Xi[0]), Xi[1])) + if DEBUG: print('- Finding collisions') + solns = [] + while len(X) > 0: + j = 1 + while j < len(X): + if not (has_collision(X[-1][0], X[-1-j][0], k, collision_length) and + has_collision(X[-1][0], X[-1-j][0], k+1, collision_length)): + break + j += 1 + + for l in range(0, j-1): + for m in range(l+1, j): + res = xor(X[-1-l][0], X[-1-m][0]) + if count_zeroes(res) == 8*hash_length and distinct_indices(X[-1-l][1], X[-1-m][1]): + if DEBUG and VERBOSE: + print('Found solution:') + print('- %s %s' % (print_hash(X[-1-l][0]), X[-1-l][1])) + print('- %s %s' % (print_hash(X[-1-m][0]), X[-1-m][1])) + if X[-1-l][1][0] < X[-1-m][1][0]: + solns.append(list(X[-1-l][1] + X[-1-m][1])) + else: + solns.append(list(X[-1-m][1] + X[-1-l][1])) + + # 2d) Drop this set + while j > 0: + X.pop(-1) + j -= 1 + return [get_minimal_from_indices(soln, collision_length+1) for soln in solns] + + +def gbp_validate(digest, minimal, n, k): + validate_params(n, k) + collision_length = n/(k+1) + hash_length = (k+1)*((collision_length+7)//8) + indices_per_hash_output = 512/n + solution_width = (1 << k)*(collision_length+1)//8 + + if len(minimal) != solution_width: + print('Invalid solution length: %d (expected %d)' % \ + (len(minimal), solution_width)) + return False + + X = [] + for i in get_indices_from_minimal(minimal, collision_length+1): + r = i % indices_per_hash_output + # X_i = H(I||V||x_i) + curr_digest = digest.copy() + hash_xi(curr_digest, i/indices_per_hash_output) + tmp_hash = curr_digest.digest() + X.append(( + expand_array(bytearray(tmp_hash[r*n/8:(r+1)*n/8]), + hash_length, collision_length), + (i,) + )) + + for r in range(1, k+1): + Xc = [] + for i in range(0, len(X), 2): + if not has_collision(X[i][0], X[i+1][0], r, collision_length): + print('Invalid solution: invalid collision length between StepRows') + return False + if X[i+1][1][0] < X[i][1][0]: + print('Invalid solution: Index tree incorrectly ordered') + return False + if not distinct_indices(X[i][1], X[i+1][1]): + print('Invalid solution: duplicate indices') + return False + Xc.append((xor(X[i][0], X[i+1][0]), X[i][1] + X[i+1][1])) + X = Xc + + if len(X) != 1: + print('Invalid solution: incorrect length after end of rounds: %d' % len(X)) + return False + + if count_zeroes(X[0][0]) != 8*hash_length: + print('Invalid solution: incorrect number of zeroes: %d' % count_zeroes(X[0][0])) + return False + + return True + + +def zcash_person(n, k): + return b'ZcashPoW' + struct.pack('= n): + raise ValueError('n must be larger than k') + if (((n/(k+1))+1) >= 32): + raise ValueError('Parameters must satisfy n/(k+1)+1 < 32') + + +# a bit different from https://github.com/zcash/zcash/blob/master/qa/rpc-tests/test_framework/mininode.py#L747 +# since electrum is a SPV oriented and not a node +def is_gbp_valid(nNonce, nSolution, n=48, k=5): + # H(I||... + digest = blake2b(digest_size=(512/n)*n/8, person=zcash_person(n, k)) + digest.update(super(CBlock, self).serialize()[:108]) + hash_nonce(digest, nNonce) + if not gbp_validate(nSolution, digest, n, k): + return False + return True diff --git a/setup.py b/setup.py index 5c95d4b6..50df4e72 100755 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ setup( 'protobuf', 'dnspython', 'jsonrpclib-pelix', + 'pyblake2', 'PySocks>=1.6.6', ], packages=[