def test_pool():
    p = BlockPool()

    assert p.get_current_height() == 0
    assert not p.get_block_dict()

    mb = MainBlock("0", "1", "2", [], [], [])

    p.add(mb)

    assert p.get_block_dict()[mb.height][0] == mb
def test_remove_non_existing_block():
    p = BlockPool()
    mb1 = MainBlock("0", "1", "2", [], [], [])

    with pytest.raises(ValueError):
        p.remove(mb1)

    mb2 = MainBlock("3", "4", "5", [], [], [])
    p.add(mb2)

    with pytest.raises(ValueError):
        p.remove(mb1)
def test_with_shard_block():
    p = BlockPool()
    sb1 = ShardBlock("0", "1", "2", [])
    sb1.height = 50000

    p.add(sb1)

    assert p.get_blocks_by_height(50000) == [sb1]

    p.remove(sb1)

    assert not p.get_blocks_by_height(50000)
    assert not p.get_block_dict()
def test_current_height():
    p = BlockPool()
    mb1 = MainBlock("0", "1", "2", [], [], [])
    mb2 = MainBlock("3", "4", "5", [], [], [])
    mb1.height = 100
    mb2.height = 101

    p.add(mb1)

    assert p.get_current_height() == 100

    p.add(mb2)

    assert p.get_current_height() == 101
def test_add_and_remove_fork_block():
    height = 100
    p = BlockPool()
    mb1 = MainBlock("0", "1", "2", [], [], [])
    mb2 = MainBlock("3", "4", "5", [], [], [])
    mb1.height = height
    mb2.height = height

    p.add(mb1)
    p.add(mb2)

    assert p.get_block_dict()[height] == [mb1, mb2]

    p.remove(mb1)

    assert p.get_block_dict()[height] == [mb2]
class Client(threading.Thread):
    """this class does all the previously described tasks"""

    fields = [
        ("wallet", Wallet),
        ("p2p_api", P2P_API),

        ("vote_pool", VotePool),
        ("tx_pool", TransactionPool),
        ("vtx_pool", TransactionPool),
        ("main_block_pool", BlockPool),
        ("shard_block_pool", BlockPool),

        ("event_manager", EventManager),
        ("event_stop", threading.Event),

        ("state", State),

        ("current_difficulty", str),

        ("current_height", int),
        ("stable_height", int),
        ("last_fork_height", int)
    ]

    def __init__(self):
        threading.Thread.__init__(self)
        #self.wallet = wallet_database.load_wallet()
        self.p2p_api = P2P_API()

        self.vote_pool = VotePool()
        self.tx_pool = TransactionPool()
        self.vtx_pool = TransactionPool()
        self.main_block_pool = BlockPool()
        self.shard_block_pool = BlockPool()

        self.event_manager = self.p2p_api.event_mg
        self.stop = threading.Event()

        self.state = State.DEFAULT

        # self.start()

    def connect(self, connect_to_peer: Peer=None):
        """
        lets you connect to a peer 
        or connects you to the list of peers in the config file
        """
        if connect_to_peer:
            self.p2p_api.connect(connect_to_peer)
        else:
            for peer_to_connect_to in get_peer_list():
                self.p2p_api.connect(peer_to_connect_to)

    def run(self):
        """method required by threading to start a thread"""
        loggerutil.info("client update started")
        self.update()
        loggerutil.info("client update complete")

        loggerutil.info("client started")
        self.loop()
        loggerutil.info("client stopped")

    def update(self):
        """updates a client to the current state of the network"""
        self.state = State.UPDATING

        # blocking call, connects this peer to other known peers
        standalone = self.connect()

        if not standalone:
            # blocking calls
            self.current_difficulty = p2p_api.get_current_difficulty()
            self.current_height = p2p_api.get_current_height()
            # get last stable chain height
            # (stable means no forks and 7 confirmations)
            self.stable_height = block_database.get_stable_height()

            loggerutil.debug("client connected")

            # blocking call
            client_net = self.p2p_api.list_peers_in_net(self.peer)
            loggerutil.debug(
                "Client with host {0} - Current state of the net {1}".format(self.peer_host, client_net))

            # init all the pools

            # blocking call only returns list of missing blocks
            self.p2p_api.update_chain(
                self.my_height, self.current_height)

            # go over all the blocks that came along since this client was last
            # online
            for b in block_list:
                self.main_block_pool.add_block(b)

            # not blocking call because peers could have different versions of the
            # pool, makes the p2p api request blocks from different peers and send
            # them all to this client
            self.p2p_api.update_block_pool()

            # not blocking call because peers could have different versions of the
            # pool, makes the p2p api request blocks from different peers and send
            # them all to this client
            self.p2p_api.update_tx_pool()

            # here we should be finished with the update of the client (except for
            # all blocks in pool and tx in pool)
        else:
            self.stable_height = block_database.get_stable_height()
            self.current_height = self.current_height

            if self.stable_height == 0:
                self.main_block_pool.add(
                    client_genesis_generator.generate_genesis_block())

    def loop(self):
        """this method loops for ever until it is stopped by force or with the "stop" event"""
        while True:
            if self.stop.isSet():
                loggerutil.debug("shutting down client")
                break
            if self.manage_events():
                loggerutil.debug("shutting down client due to an error")
                break

    def validate_block(block: str) -> bool:
        """a lot of steps to validate if a block is correct"""
        return True

    def validate_tx(tx: str) -> bool:
        """a lot of steps to validate if a tx is correct"""
        return True

    def manage_events(self) -> bool:
        """
        Manages all functionality that has to be asynchronous.
        All these events can be set by the p2p api, the client then handles these events in its own thread

        returns a bool that is only True if the client needs to be shut down in case of an error
        """
        # get data from the p2p
        if self.event_manager.block_received.isSet():
            block = self.p2p_api.get_recv_block(self.peer_host)
            loggerutil.debug(
                "block received event is triggered by: {0}:".format(self.peer_host))
            loggerutil.debug("Block string: {0}".format(block))
            self.event_manager.block_received.clear()
            handle_new_block(block)

        if self.event_manager.tx_received.isSet():
            tx = self.p2p_api.get_recv_tx(self.peer_host)
            loggerutil.debug(
                "transaction received event is triggered by: {0}:".format(self.peer_host))
            loggerutil.debug("Tx string: {0}".format(tx))
            self.event_manager.tx_received.clear()
            handle_new_tx(tx)

        # give data to the p2p
        if self.event_manager.height_request.isSet():
            client_height = len(self.chain) - 1
            self.p2p_api.send_height(self.peer_host, client_height)
            # do stuff
            self.event_manager.height_request.clear()

        if self.event_manager.block_request.isSet():
            # returns list of requested block numbers
            number_list = self.p2p_api.get_requ_block_numbers()
            # get the highest block from the number list
            last_num_from_list = number_list[-1]
            # check if the highest blockchain of a client is the same as from
            # the list
            if len(self.chain) == last_num_from_list:
                # send the blocks that are missing from smaller chains
                for number in number_list:
                    self.p2p_api.send_block(number, self.chain[number - 1])
            # do stuff
            self.event_manager.block_request.clear()

        if self.event_manager.tx_request.isSet():
            # do stuff
            self.event_manager.tx_request.clear()

        if self.event_manager.tx_pool_request.isSet():
            self.p2p_api.send_pool(self.pool.string())
            self.event_manager.tx_pool_request.clear()

        if self.event_manager.bootstr_request.isSet():
            from_block_number, to_block_number, receiver = self.p2p_api.get_requ_bootstr_numbers()
            temp = []
            # fill temp with the blocks from and to
            self.p2p_api.send_bootstr(receiver, temp)
            # do stuff
            self.event_manager.bootstr_request.clear()

        if self.event_manager.connection_lost.isSet():
            # reconnect maybe ? (probably not till milestone 4)
            # do stuff
            self.event_manager.connection_lost.clear()

        if self.event_manager.error.isSet():
            loggerutil.debug("shutdown event is triggered")
            # loggerutil.error(self.p2p_api.get_error_message())
            # shut down the client after logging the error
            self.p2p_api.stop_peer_thread(self.peer)
            return True

        return False

    def handle_new_block(self, block):
        """handles blocks received from the p2p network"""
        if isinstance(block, MainBlock):
            handle_new_main_block(block)
        elif isinstance(block, ShardBlock):
            handle_new_shard_block(block)
        else:
            loggerutil.error(
                "received block that was neither main nor shard block")

    def handle_new_main_block(self, main_block: MainBlock):
        self.main_block_pool.add_block(main_block)
        loggerutil.debug("got new main_block: " + main_block.str())

    def handle_new_shard_block(self, shard_block: ShardBlock):
        self.shard_block_pool.add_block(shard_block)
        loggerutil.debug("got new shard_block: " + shard_block.str())

    def handle_new_tx(self, tx):
        """handles tx received from the p2p network"""
        if isinstance(tx, Transaction):
            handle_new_transaction(tx)
        elif isinstance(tx, VotingTokenTransaction):
            handle_new_vtx(tx)
        else:
            loggerutil.error("received tx that was neither tx nor vtx")

    def handle_new_transaction(self, tx: Transaction):
        self.tx_pool.add_transaction(tx)
        loggerutil.debug("got new tx: " + tx.str())

    def handle_new_vtx(self, vtx: VotingTokenTransaction):
        self.vtx_pool.add_transaction(tx)
        loggerutil.debug("got new vtx: " + vtx.str())