def __init__(self, host, ip, port, path, initial_peers, username, password): super().__init__(initial_peers) self.path = path self.host = host self.username = username self.password = password print("Loading the blockchain...") self.blockchain = Blockchain(self.path) print("Starting a full-node on " + self.host + "...") self.block_queue = Queue() self.transaction_queue = Queue() self.transaction_pool = {} self.next_block = None self.mining_interrupt = threading.Condition() self.network_time_offset = 0 self.byte_price = 0 self.listeners = {} self.miner = None threading.Thread(target=self.heartbeat, daemon=True).start() threading.Thread(target=self.block_broadcaster, daemon=True).start() threading.Thread(target=self.transaction_broadcaster, daemon=True).start() app = web.Application(middlewares=[Node.access_control_allow_origin]) app.router.add_static('/static/', path=os.path.join(RESOURCES_PATH, 'static')) app.router.add_get('/', self.index) app.router.add_route('*', '/blocks', self.blocks) app.router.add_get('/blocks/{index}', self.block) app.router.add_get('/blocks/{start}/{end}', self.block_range) app.router.add_route('*', '/peers', self.nodes) app.router.add_route('*', '/transactions', self.transactions) app.router.add_get('/status', self.status) app.router.add_get('/find', self.find) app.router.add_get('/latest', self.latest) app.router.add_get('/resolve', self.resolve) app.router.add_get('/confirm', self.confirm) app.router.add_get('/live', self.live) app.router.add_get('/mine', self.mine) self.app = app web.run_app(app, host=ip, port=port)
def test_push_pop_block(self): config.MINIMUM_BYTE_PRICE = 0 with patch.object(difficulty, 'less_or_equal', return_value=True) as mock_less_or_equal: with patch.object(ecdsa, 'verify', return_value=True) as mock_verify: bc = Blockchain(mkdtemp()) self.put_block(bc, 'rnd1', config.SUPPLY_NAME, BlockchainTest.BOB_ADDRESS, 1000) self.assertEquals(bc.get_balance(BlockchainTest.BOB_ADDRESS), 1000) self.put_block(bc, 'rnd2', config.SUPPLY_NAME, BlockchainTest.BOB_ADDRESS, 500) self.assertEquals(bc.get_balance(BlockchainTest.BOB_ADDRESS), 1500) self.put_block(bc, 'bob', BlockchainTest.BOB_ADDRESS, BlockchainTest.CHARLIE_ADDRESS, 0) self.assertEquals(bc.resolve(Address.from_string('@bob')), BlockchainTest.BOB_ADDRESS) bc.pop_block() self.assertIsNone(bc.resolve(Address.from_string('@bob'))) bc.pop_block() self.assertEquals(bc.get_balance(BlockchainTest.BOB_ADDRESS), 1000) bc.pop_block() self.assertEquals(bc.get_balance(BlockchainTest.BOB_ADDRESS), 0)
def test_fork(self): with patch.object(difficulty, 'less_or_equal', return_value=True) as mock_less_or_equal: with patch.object(ecdsa, 'verify', return_value=True) as mock_verify: bc1_path = mkdtemp() bc2_path = mkdtemp() bc1 = Blockchain(bc1_path) bc2 = Blockchain(bc2_path) self.put_block(bc1, '1') self.put_block(bc1, '2') bc2.fork(bc1.get_blocks()[1:]) self.put_block(bc1, '3') self.put_block(bc1, '4') self.put_block(bc2, '5') self.put_block(bc2, '6') self.put_block(bc2, '7') self.put_block(bc2, '8') bc3_path = bc1_path + '_copy' copytree(bc1_path, bc3_path) bc3 = Blockchain(bc3_path) bc1.fork(bc2.get_blocks()[3:]) bc3.fork(bc2.get_blocks()[4:]) self.assertEquals(bc1.get_height(), 7) self.assertEquals(bc3.get_height(), 5)
class Node(LightNode): @web.middleware async def access_control_allow_origin(request, handler): resp = await handler(request) resp.headers['ACCESS-CONTROL-ALLOW-ORIGIN'] = '*' return resp def __init__(self, host, ip, port, path, initial_peers, username, password): super().__init__(initial_peers) self.path = path self.host = host self.username = username self.password = password print("Loading the blockchain...") self.blockchain = Blockchain(self.path) print("Starting a full-node on " + self.host + "...") self.block_queue = Queue() self.transaction_queue = Queue() self.transaction_pool = {} self.next_block = None self.mining_interrupt = threading.Condition() self.network_time_offset = 0 self.byte_price = 0 self.listeners = {} self.miner = None threading.Thread(target=self.heartbeat, daemon=True).start() threading.Thread(target=self.block_broadcaster, daemon=True).start() threading.Thread(target=self.transaction_broadcaster, daemon=True).start() app = web.Application(middlewares=[Node.access_control_allow_origin]) app.router.add_static('/static/', path=os.path.join(RESOURCES_PATH, 'static')) app.router.add_get('/', self.index) app.router.add_route('*', '/blocks', self.blocks) app.router.add_get('/blocks/{index}', self.block) app.router.add_get('/blocks/{start}/{end}', self.block_range) app.router.add_route('*', '/peers', self.nodes) app.router.add_route('*', '/transactions', self.transactions) app.router.add_get('/status', self.status) app.router.add_get('/find', self.find) app.router.add_get('/latest', self.latest) app.router.add_get('/resolve', self.resolve) app.router.add_get('/confirm', self.confirm) app.router.add_get('/live', self.live) app.router.add_get('/mine', self.mine) self.app = app web.run_app(app, host=ip, port=port) def add_peer(self, peer): if peer != self.host: super().add_peer(peer) def update_peers(self): if self.host in self.peers: self.peers.remove(self.host) super().update_peers() if self.host in self.peers: self.peers.remove(self.host) def broadcast_node(self, host): for node in self.random_peers(): url = LightNode.PEERS_URL.format(node) try: response = requests.post(url, data={'peer': host}) except requests.exceptions.RequestException: self.set_bad_peer(node) def get_time(self): return int(time.time()) - self.network_time_offset def adjust_network_time(self): node_time = self.get_time() times = [node_time] for node in self.random_peers(): url = LightNode.STATUS_URL.format(node) try: response = requests.get(url) times.append(response.json()['time']) except requests.exceptions.RequestException: self.set_bad_peer(node) network_time = misc.median(times) self.network_time_offset += node_time - network_time def broadcast_block(self, block): oks = 0 peers = self.random_peers() if len(peers) == 0: return (0, 0) for node in peers: url = LightNode.BLOCKS_URL.format(node) try: response = requests.post(url, data=block.serialize()) if response.json()['ok']: oks += 1 except requests.exceptions.RequestException: self.set_bad_peer(node) return (oks, len(peers)) def broadcast_transaction(self, transaction): peers = self.random_peers() for node in peers: url = LightNode.TRANSACTIONS_URL.format(node) try: response = requests.post(url, data=transaction.serialize()) except requests.exceptions.RequestException: self.set_bad_peer(node) def transaction_exists(self, tx): for t in self.transaction_pool[tx.target]: if t.calculate_hash() == tx.calculate_hash(): return True return False async def index(self, request): with io.open(os.path.join(RESOURCES_PATH, 'index.html')) as f: html = f.read() return web.Response(text=html, headers={'content-type': 'text/html'}) async def status(self, request): return web.json_response( data={ 'height': self.blockchain.get_height(), 'time': self.get_time(), 'bytePrice': self.byte_price }) async def transactions(self, request): if request.method == 'GET': return web.json_response( data=[tx.json() for tx in self.blockchain.transactions]) elif request.method == 'POST': try: tx = Transaction.deserialize(await request.content.read()) self.blockchain.is_valid_transaction(tx) self.transaction_queue.put(tx) return web.json_response(data={'ok': True}) except (CommonException, BlockchainException) as e: return web.json_response(data={'ok': False, 'error': str(e)}) async def nodes(self, request): if request.method == 'GET': return web.json_response(data={ 'ok': True, 'peers': list(self.all_peers()) }) elif request.method == 'POST': peer = (await request.post())['peer'] self.add_peer(peer) return web.json_response(data={'ok': True}) async def blocks(self, request): if request.method == 'GET': return web.Response(text='All blocks') elif request.method == 'POST': try: b = Block.deserialize(await request.content.read()) self.blockchain.is_valid_block(b) self.block_queue.put(b) return web.json_response(data={'ok': True}) except (CommonException, BlockchainException) as e: return web.json_response(data={'ok': False, 'error': str(e)}) async def block(self, request): header_only = 'header' in request.query index = request.match_info['index'] if index == 'latest': index = self.blockchain.get_height() - 1 else: index = int(index) if index < self.blockchain.get_height(): return web.Response( body=self.blockchain.get_block(index).serialize( header_only=header_only)) else: return web.Response(body=b'') async def block_range(self, request): header_only = 'header' in request.query start = int(request.match_info['start']) end = request.match_info['end'] if end == 'latest': end = self.blockchain.get_height() else: end = int(end) + 1 end = min(self.blockchain.get_height(), end) if end > start: result = self.blockchain.get_block_range(start, end) else: result = [] return web.Response( body=Block.serialize_list(result, header_only=header_only)) async def find(self, request): children = 'children' in request.query name = Address.from_string(request.query.get('name', None)) if not children: tx = self.blockchain.find(name) return web.Response(body=tx.serialize()) else: txs = self.blockchain.find_children(name) return web.Response(body=Transaction.serialize_list(txs)) async def latest(self, request): address = Address.from_string(request.query.get('address')) txs = self.blockchain.latest(address=address) return web.Response(body=Transaction.serialize_list(txs)) async def resolve(self, request): address = request.query.get('address', None) raw_name = self.blockchain.resolve(Address.from_string(address)) if raw_name is None: return web.json_response({ 'ok': False, 'error': 'Invalid address.' }) return web.json_response( data={'balance': self.blockchain.get_balance(raw_name)}) async def confirm(self, request): index = int(request.query['target']) hashed = bytes.fromhex(request.query['hash']) try: return web.json_response( data={ 'ok': True, 'path': self.blockchain.get_block(index).get_merkle_path( hashed).hex() }) except IndexError: return web.json_response(data={ 'ok': False, 'error': 'Block not mined yet!' }) except Exception as e: return web.json_response(data={'ok': False, 'error': str(e)}) async def live(self, request): address = request.query.get('address', None) raw_name = self.blockchain.resolve(Address.from_string(address)) if not raw_name: return web.json_response({ 'ok': False, 'error': 'Invalid address.' }) ws = web.WebSocketResponse() await ws.prepare(request) if raw_name not in self.listeners: self.listeners[raw_name] = [] self.listeners[raw_name].append(ws) async for msg in ws: pass self.listeners[raw_name].remove(ws) if len(self.listeners[raw_name]) == 0: del self.listeners[raw_name] return ws def heartbeat(self): while True: try: self.update_peers() self.broadcast_node(self.host) self.adjust_network_time() except Exception as ex: print("An unhandled exception occurred!", ex) time.sleep(config.HEARTBEAT_INTERVAL) def block_broadcaster(self): latest_index = self.blockchain.get_latest_block().index while True: b = self.block_queue.get() try: if b.index > self.blockchain.get_latest_block().index + 1: # Synchronize just with that peer! self.synchronize() else: try: ok, total = self.broadcast_block(b) if total and ok / total < 0.5: self.synchronize() else: self.blockchain.push_block(b) except: pass if latest_index != self.blockchain.get_latest_block().index: latest_block = self.blockchain.get_latest_block() latest_index = latest_block.index print("Block {} - {}".format( latest_block.index, latest_block.calculate_hash().hex())) coro = self.mine_next_block() asyncio.run_coroutine_threadsafe(coro, self.app.loop) except Exception as ex: print("An unhandled exception occurred!", ex) def transaction_exists(self, transaction): if transaction.target not in self.transaction_pool: return False for tx in self.transaction_pool[transaction.target]: if tx.calculate_hash() == transaction.calculate_hash(): return True return False def transaction_broadcaster(self): while True: tx = self.transaction_queue.get() try: if tx.target not in self.transaction_pool: self.transaction_pool[tx.target] = [] if self.transaction_exists(tx): continue else: print("New transaction!") self.transaction_pool[tx.target].append(tx) raw_name = self.blockchain.resolve(tx.destination) if raw_name in self.listeners: for ws in self.listeners[raw_name]: coro = ws.send_bytes(tx.serialize()) asyncio.run_coroutine_threadsafe(coro, self.app.loop) self.broadcast_transaction(tx) except Exception as ex: print("An unhandled exception occurred!", ex) def synchronize_with(self, peer): latest_block = self.blockchain.get_latest_block() remote_diff_blocks = self.get_block_range_from(latest_block.index + 1, 'latest', peer) if len(remote_diff_blocks) > 0: if remote_diff_blocks[ 0].previous_hash == latest_block.calculate_hash(): for block in remote_diff_blocks: try: self.blockchain.push_block(block) except: self.set_bad_peer(peer) break else: for i in range(latest_block.index, 0, -1): block = self.get_block_from(i, peer) remote_diff_blocks.insert(0, block) if block.previous_hash == self.blockchain.get_block( i - 1).calculate_hash(): self.blockchain.fork(remote_diff_blocks) break def synchronize(self): latest_block = self.blockchain.get_latest_block() latest_blocks = {} for node in self.random_peers(): remote_block = self.get_block_from('latest', node, True) if remote_block and remote_block.index > latest_block.index: if remote_block.index not in latest_blocks: latest_blocks[remote_block.index] = {} remote_hash = remote_block.calculate_hash() if remote_hash not in latest_blocks[remote_block.index]: latest_blocks[remote_block.index][remote_hash] = [] latest_blocks[remote_block.index][remote_hash].append(node) if len(latest_blocks) > 0: for index, hash_nodes in sorted(latest_blocks.items(), reverse=True): for hashed, nodes in hash_nodes.items(): latest_block = self.blockchain.get_latest_block() if index > latest_block.index: self.synchronize_with(nodes[0]) else: return def get_next_block(self): new_block_index = self.blockchain.get_latest_block().index + 1 if not self.next_block or self.next_block.index != new_block_index: if new_block_index in self.transaction_pool: for tx in self.transaction_pool[new_block_index]: try: self.blockchain.add_transaction(tx) except BlockchainException as e: pass del self.transaction_pool[new_block_index] self.next_block = self.blockchain.new_block( self.miner_address, self.get_time()) return self.next_block async def mine_next_block(self): if self.miner is not None and not self.miner.closed: block = self.get_next_block() await self.miner.send_json({ 'id': block.index, 'data': block.serialize(header_only=True).hex(), 'nonceOffset': 76, 'nonceSize': 4, 'timestampOffset': 72, 'timestampSize': 4, 'target': difficulty.decompress(block.difficulty).hex() }) async def mine(self, request): username = request.query.get('username', None) password = request.query.get('password', None) if username != self.username or password != self.password: return web.json_response(data={ 'ok': False, 'error': 'Auth failed!' }) miner_address = Address.from_string(request.query.get('address', None)) if self.blockchain.resolve(miner_address) is None: return web.json_response(data={ 'ok': False, 'error': 'Miner not valid!' }) self.miner_address = miner_address print("Miner: " + str(self.miner_address)) if self.miner: await self.miner.close() self.miner = web.WebSocketResponse() await self.miner.prepare(request) await self.mine_next_block() async for msg in self.miner: response = msg.json() header = Block.deserialize(bytes.fromhex(response['data']), header_only=True) self.next_block.nonce = header.nonce self.next_block.timestamp = header.timestamp try: self.blockchain.is_valid_block(self.next_block) self.block_queue.put(self.next_block) except BlockchainException as e: print(e) await self.mine_next_block()