def crypto_block(crypto_transaction, crypto_transaction_2): # TODO: Block is not from testnet and it has wrong data and signature # Change it once we're able to forge blocks with our code block = Block( height=2243161, id="10977713934532967004", id_hex=b"9858aca939b17a5c", number_of_transactions=2, payload_hash=( "3784b953afcf936bdffd43fdf005b5732b49c1fc6b11e195c364c20b2eb06282" ), payload_length=224, previous_block="3112633353705641986", previous_block_hex=b"2b324b8b33a85802", reward=200000000, timestamp=24760440, total_amount=3890300, total_fee=70000000, version=0, generator_public_key=( "020f5df4d2bc736d12ce43af5b1663885a893fade7ee5e62b3cc59315a63e6a325" ), block_signature=( "3045022100eee6c37b5e592e99811d588532726353592923f347c701d52912e6d583443e40022" "0277ffe38ad31e216ba0907c4738fed19b2071246b150c72c0a52bae4477ebe29" ), ) crypto_transaction.block_id = "7176646138626297930" crypto_transaction_2.block_id = "7176646138626297930" block.transactions = [crypto_transaction, crypto_transaction_2] return block
def test_from_dict_raises_exception_for_wrong_type(dummy_block): data = deepcopy(dummy_block) data["id"] = float(data["id"]) with pytest.raises(TypeError) as excinfo: Block.from_dict(data) assert str(excinfo.value) == ( "Attribute id (<class 'float'>) must be of type (<class 'str'>, <class 'bytes'>" ")")
def test_apply_transaction_raises_if_cant_be_applied_to_sender_wallet(redis, mocker): manager = WalletManager() block = Block() transaction = TransferTransaction() transaction.type = TRANSACTION_TYPE_TRANSFER transaction.fee = 10000 transaction.amount = 430000 transaction.id = "hehe" transaction.sender_public_key = ( "020f5df4d2bc736d12ce43af5b1663885a893fade7ee5e62b3cc59315a63e6a325" ) transaction.recipient_id = "AZYnpgXS3x43nxqhT4q29sZScRwZeNKLpW" redis.set( "wallets:address:AThM5PNSKdU9pu1ydqQnzRWVeNCGr8HKof", Wallet( { "address": "AThM5PNSKdU9pu1ydqQnzRWVeNCGr8HKof", "public_key": ( "020f5df4d2bc736d12ce43af5b1663885a893fade7ee5e62b3cc59315a63e6a325" ), "balance": 5, } ).to_json(), ) with pytest.raises(Exception) as excinfo: manager.apply_transaction(transaction, block) assert str(excinfo.value) == ( "Can't apply transaction hehe from sender AThM5PNSKdU9pu1ydqQnzRWVeNCGr8HKof" )
def get_block_by_id(self, block_id): try: block = Block.get(Block.id == block_id) except Block.DoesNotExist: return None else: return CryptoBlock.from_object(block)
def test_from_serialized_correctly_deserializes_full_data( dummy_block_full_hash, dummy_block): block = Block.from_serialized(dummy_block_full_hash) assert block.version == 0 assert block.timestamp == 24760440 assert block.height == 2243161 assert block.previous_block_hex == b"2b324b8b33a85802" assert block.previous_block == "3112633353705641986" assert block.number_of_transactions == 2 assert block.total_amount == 3890300 assert block.total_fee == 70000000 assert block.reward == 200000000 assert block.payload_length == 224 assert block.payload_hash == ( "3784b953afcf936bdffd43fdf005b5732b49c1fc6b11e195c364c20b2eb06282") assert block.generator_public_key == ( "020f5df4d2bc736d12ce43af5b1663885a893fade7ee5e62b3cc59315a63e6a325") assert block.block_signature == ( "3045022100eee6c37b5e592e99811d588532726353592923f347c701d52912e6d583443e40022" "0277ffe38ad31e216ba0907c4738fed19b2071246b150c72c0a52bae4477ebe29") assert block.id == "10977713934532967004" assert block.id_hex == b"9858aca939b17a5c" assert block.transactions is not None assert len(block.transactions) == 2 for transaction, expected in zip(block.transactions, dummy_block["transactions"]): assert transaction.version == 1 assert transaction.network == 23 assert transaction.type == expected["type"] assert transaction.timestamp == expected["timestamp"] assert transaction.sender_public_key == expected["senderPublicKey"] assert transaction.fee == expected["fee"] assert transaction.amount == expected["amount"] assert transaction.asset == expected["asset"]
def get_blocks(self, height, limit, serialized, with_transactions=False): blocks = (Block.select().where( Block.height.between(height, height + limit)).order_by(Block.height.asc())) if with_transactions: block_ids = [block.id for block in blocks] transactions = (Transaction.select().where( Transaction.block_id.in_(block_ids)).order_by( Transaction.block_id.asc(), Transaction.sequence.asc())) transactions_map = defaultdict(list) for trans in transactions: # TODO: implement from_object on transaction and use that, instead of # creating it from serialized data. if serialized: transactions_map[trans.block_id].append(trans.serialized) else: transactions_map[trans.block_id].append( from_serialized(trans.serialized)) crypto_blocks = [] for block in blocks: crypto_block = CryptoBlock.from_object(block) if with_transactions: crypto_block.transactions = transactions_map[block.id] crypto_blocks.append(crypto_block) return crypto_blocks
def consume_queue(self): while True: serialized_block = self.process_queue.pop_block() if serialized_block: last_block = self.database.get_last_block() block = Block.from_serialized(serialized_block) status = self.process_block(block, last_block) logger.info(status) if status in [BLOCK_ACCEPTED, BLOCK_DISCARDED_BUT_CAN_BE_BROADCASTED]: # TODO: Broadcast only current block milestone = config.get_milestone(block.height) current_slot = slots.get_slot_number(block.height, time.get_time()) if current_slot * milestone["blocktime"] <= block.timestamp: # TODO: THIS IS MISSING logger.error("MISSING: IMPLEMENT BROADCASTING") else: # TODO: change this logger.info("Nothing to process. Sleeping for 1 sec") sleep(1) # Our chain can get out of sync when it doesn't receive all the blocks # to the p2p endpoint, so if we're not in sync, force sync to the last # block last_block = self.database.get_last_block() if not self.is_synced(last_block): logger.info("Force syncing with the network as we got out of sync") self.sync_chain() logger.info("Done force syncing")
def fetch_blocks_from_height(self, from_height): payload = { "lastBlockHeight": from_height, "serialized": True, "headersOnly": False, } blocks = self._fetch("p2p.peer.getBlocks", payload) return [Block.from_dict(block) for block in blocks]
def _create_genesis_block(): _clear_db() block = CryptoBlock.from_dict(config.genesis_block) db_block = Block.from_crypto(block) db_block.save(force_insert=True) for transaction in block.transactions: db_trans = Transaction.from_crypto(transaction) db_trans.save(force_insert=True)
def get_last_block(self): """Get the last block Returns None if block can't be found. """ try: block = Block.select().order_by(Block.height.desc()).get() except Block.DoesNotExist: return None else: crypto_block = CryptoBlock.from_object(block) return crypto_block
async def post_block(self, data, ip): # TODO: Wrap everything in try except # TODO: Validate request data that it's correct block structure block_data = data["block"] # if not block_data: # raise Exception( # "There was no block in request to the /peer/blocks endpoint" # ) block = Block.from_dict(block_data) self.socket.log_info( "Received new block at height %s with %s transactions, from %s", block.height, block.number_of_transactions, ip, ) is_verified, errors = block.verify() if not is_verified: self.socket.log_error(errors) # TODO: raise Exception("Verification failed") last_block = self.db.get_last_block() if last_block.height >= block.height: self.socket.log_info( "Received block with height %s which was already processed. Our last " "block height %s. Skipping process queue.", block.height, last_block.height, ) return if self.process_queue.block_exists(block): self.socket.log_info( "Received block with height %s is already in process queue.", block.height, ) return current_slot = slots.get_slot_number(last_block.height, time.get_time()) received_slot = slots.get_slot_number(last_block.height, block.timestamp) if current_slot >= received_slot and is_block_chained( last_block, block): # Put the block to process queue self.process_queue.push_block(block) else: self.socket.log_info( "Discarded block %s because it takes a future slot", block.height)
def test_apply_transaction_force_apply_skips_can_be_applied_check(redis, mocker): manager = WalletManager() balances_mock = mocker.patch( "chain.plugins.database.wallet_manager.WalletManager._update_vote_balances" ) mocker.patch( "chain.plugins.database.wallet_manager.is_transaction_exception", return_value=True, ) block = Block() transaction = TransferTransaction() transaction.type = TRANSACTION_TYPE_TRANSFER transaction.fee = 10000 transaction.amount = 430000 transaction.id = "hehe" transaction.sender_public_key = ( "020f5df4d2bc736d12ce43af5b1663885a893fade7ee5e62b3cc59315a63e6a325" ) transaction.recipient_id = "AZYnpgXS3x43nxqhT4q29sZScRwZeNKLpW" redis.set( "wallets:address:AThM5PNSKdU9pu1ydqQnzRWVeNCGr8HKof", Wallet( { "address": "AThM5PNSKdU9pu1ydqQnzRWVeNCGr8HKof", "public_key": ( "020f5df4d2bc736d12ce43af5b1663885a893fade7ee5e62b3cc59315a63e6a325" ), "balance": 0, } ).to_json(), ) manager.apply_transaction(transaction, block) # updated sender wallet is saved back to redis sender = Wallet( json.loads(redis.get("wallets:address:AThM5PNSKdU9pu1ydqQnzRWVeNCGr8HKof")) ) assert sender.balance == -440000 # updated recipient wallet is saved back to redis recipient = Wallet( json.loads(redis.get("wallets:address:AZYnpgXS3x43nxqhT4q29sZScRwZeNKLpW")) ) assert recipient.balance == 430000 balances_mock.assert_called_once()
def test_apply_block_updates_vote_balance_of_voted_delegate(redis, mocker): manager = WalletManager() apply_transaction_mock = mocker.patch( "chain.plugins.database.wallet_manager.WalletManager.apply_transaction" ) transaction_1 = TransferTransaction() transaction_2 = TransferTransaction() block = Block() block.height = 1337 block.reward = 2 block.transactions = [transaction_1, transaction_2] block.generator_public_key = ( "0316510c1409d3307d9f205cac58f1a871499c3ffea3878ddbbb48c821cfbc079a" ) redis.set( "wallets:address:AWoysqF1xm1LXYLQvmRDpfVNKzzaLVwPVM", json.dumps( { "address": "AWoysqF1xm1LXYLQvmRDpfVNKzzaLVwPVM", "public_key": ( "0316b3dc139c1a35927ecbdcb8d8b628ad06bd4f1869fe3ad0e23c8106678a460f" ), "vote_balance": 2000000, } ), ) redis.set( "wallets:address:ASt5oBHKDW8AeJe2Ybc1RucMLS7mRCiuRe", Wallet( { "address": "ASt5oBHKDW8AeJe2Ybc1RucMLS7mRCiuRe", "public_key": ( "0316510c1409d3307d9f205cac58f1a871499c3ffea3878ddbbb48c821cfbc079a" ), "balance": 0, "vote": ( "0316b3dc139c1a35927ecbdcb8d8b628ad06bd4f1869fe3ad0e23c8106678a460f" ), } ).to_json(), ) manager.apply_block(block) assert apply_transaction_mock.call_count == 2 delegate = Wallet( json.loads(redis.get("wallets:address:ASt5oBHKDW8AeJe2Ybc1RucMLS7mRCiuRe")) ) assert delegate.balance == 2 vote_wallet = Wallet( json.loads(redis.get("wallets:address:AWoysqF1xm1LXYLQvmRDpfVNKzzaLVwPVM")) ) assert vote_wallet.vote_balance == 2000002
def test_apply_transaction_raises_if_voted_delegate_doesnt_exist(): """ Should raise exception if delegate already exists """ manager = WalletManager() public_key = "020f5df4d2bc736d12ce43af5b1663885a893fade7ee5e62b3cc59315a63e6a325" block = Block() transaction = TransferTransaction() transaction.type = TRANSACTION_TYPE_VOTE transaction.id = "hehe" transaction.asset["votes"] = ["+{}".format(public_key)] with pytest.raises(Exception) as excinfo: manager.apply_transaction(transaction, block) assert str( excinfo.value ) == "Can't apply transaction hehe: delegate {} does not exist".format(public_key)
def test_from_serialized_correctly_sets_deserialized_types( dummy_block_hash, dummy_block): block = Block.from_serialized(dummy_block_hash) assert isinstance(block.version, int) assert isinstance(block.timestamp, int) assert isinstance(block.height, int) assert isinstance(block.previous_block_hex, bytes) assert isinstance(block.previous_block, str) assert isinstance(block.number_of_transactions, int) assert isinstance(block.total_amount, int) assert isinstance(block.total_fee, int) assert isinstance(block.reward, int) assert isinstance(block.payload_length, int) assert isinstance(block.payload_hash, str) assert isinstance(block.generator_public_key, str) assert isinstance(block.block_signature, str) assert isinstance(block.id, str) assert isinstance(block.id_hex, bytes) assert isinstance(block.transactions, list)
def test_apply_transaction_raises_if_delegate_already_exists_for_delegate_reg(redis): """ Should raise exception if delegate already exists """ manager = WalletManager() block = Block() transaction = TransferTransaction() transaction.type = TRANSACTION_TYPE_DELEGATE_REGISTRATION transaction.id = "hehe" transaction.asset["delegate"] = {"username": "******"} manager = WalletManager() redis.set("wallets:username:harambe", "") with pytest.raises(Exception) as excinfo: manager.apply_transaction(transaction, block) assert "Can't apply transaction hehe: delegate name harambe already taken" == str( excinfo.value )
def test_apply_block_raises_if_not_genesis_and_no_delegate(redis): manager = WalletManager() transaction_1 = TransferTransaction() transaction_2 = TransferTransaction() block = Block() block.height = 1337 block.reward = 2 block.transactions = [transaction_1, transaction_2] block.generator_public_key = ( "0316510c1409d3307d9f205cac58f1a871499c3ffea3878ddbbb48c821cfbc079a" ) with pytest.raises(Exception) as excinfo: manager.apply_block(block) assert str(excinfo.value) == ( "Could not find a delegate with public key: " "0316510c1409d3307d9f205cac58f1a871499c3ffea3878ddbbb48c821cfbc079a" )
def test_apply_block_correctly_handles_genesis_block(redis, mocker): manager = WalletManager() apply_transaction_mock = mocker.patch( "chain.plugins.database.wallet_manager.WalletManager.apply_transaction" ) transaction_1 = TransferTransaction() transaction_2 = TransferTransaction() block = Block() block.height = 1 block.reward = 2 block.transactions = [transaction_1, transaction_2] block.generator_public_key = ( "0316510c1409d3307d9f205cac58f1a871499c3ffea3878ddbbb48c821cfbc079a" ) manager.apply_block(block) assert apply_transaction_mock.call_count == 2 delegate = Wallet( json.loads(redis.get("wallets:address:ASt5oBHKDW8AeJe2Ybc1RucMLS7mRCiuRe")) ) assert delegate.balance == 2
def get_blocks_by_id(self, block_ids): blocks = (Block.select().where(Block.id.in_(block_ids)).order_by( Block.height.desc())) return [CryptoBlock.from_object(block) for block in blocks]
def test_to_bytes_hex_returns_correct_hex_value(value, expected): result = Block.to_bytes_hex(value) assert result == expected
def get_blocks_by_heights(self, heights): if not isinstance(heights, list): raise Exception("heights must be a type of list") blocks = Block.select().where(Block.height.in_(heights)) return [CryptoBlock.from_object(block) for block in blocks]
def start(self): """Starts the blockchain. Depending of the state of the blockchain it will decide what needs to be done in order to correctly start syncing. """ logger.info("Starting the blockchain") apply_genesis_round = False try: block = self.database.get_last_block() # If block is not found in the db, insert a genesis block if not block: logger.info("No block found in the database") block = Block.from_dict(config.genesis_block) if block.payload_hash != config.network["nethash"]: logger.error( "FATAL: The genesis block payload hash is different from " "the configured nethash" ) self.stop() return else: self.database.save_block(block) apply_genesis_round = True logger.info("Verifying database integrity") is_valid = False errors = None for _ in range(5): is_valid, errors = self.database.verify_blockchain() if is_valid: break else: logger.error("Database is corrupted: {}".format(errors)) milestone = config.get_milestone(block.height) previous_round = math.floor( (block.height - 1) / milestone["activeDelegates"] ) if previous_round <= 1: raise Exception( "FATAL: Database is corrupted: {}".format(errors) ) logger.info("Rolling back to round {}".format(previous_round)) self.database.rollback_to_round(previous_round) logger.info("Rolled back to round {}".format(previous_round)) else: raise Exception( "FATAL: After rolling back for 5 rounds, database is still " "corrupted: {}".format(errors) ) logger.info("Verified database integrity") # if (stateStorage.networkStart) { # await blockchain.database.buildWallets(block.data.height); # await blockchain.database.saveWallets(true); # await blockchain.database.applyRound(block.data.height); # await blockchain.transactionPool.buildWallets(); # return blockchain.dispatch("STARTED"); # } logger.info("Last block in database: %s", block.height) # if the node is shutdown between round, the round has already been applied # so we delete it to start a new, fresh round if is_new_round(block.height + 1): current_round, _, _ = calculate_round(block.height + 1) logger.info( "Start of new round detected %s. Removing it in order to correctly " "start the chain with new round.", current_round, ) self.database.delete_round(current_round) # Rebuild wallets self.database.wallets.build() self.transaction_pool.build_wallets() if apply_genesis_round: self.database.apply_round(block.height) self.sync_chain() logger.info("Blockhain is syced!") # Blockchain was just synced, so remove all blocks from process queue # as it was just synced. We clear it only on the start of the chain, to # awoid processing old blocks. If we ever run sync while it's already # runing, we don't want to run clear after sync as that might leave us # with missing blocks which will cause the blockchain to always sync back # rather than sync by accepting block from peers. self.process_queue.clear() self.consume_queue() except Exception as e: self.stop() # TODO: log exception raise e # TODO: