def add_unit_and_extend_linear_order(self, U):
        '''
        Add a (compliant) unit to the poset, try to find a new timing unit and in the case of success, extend the linear order.
        :param Unit U: the unit to be added to the poset
        '''
        #NOTE: it is assumed at this point that U is not yet in the poset
        assert U.hash(
        ) not in self.poset.units, "A duplicate unit is being added to the poset."
        self.poset.add_unit(U)
        self.unordered_units.append(U)
        if self.poset.is_prime(U):

            with timer(self.process_id, 'attempt_timing'):
                if consts.PRECOMPUTE_POPULARITY:
                    self.poset.precompute_popularity_proof(U)
                new_timing_units = self.poset.attempt_timing_decision()
            timer.write_summary(where=self.logger, groups=[self.process_id])
            timer.reset(self.process_id)

            self.logger.info(
                f'prime_unit {self.process_id} | New prime unit at level {U.level} : {U.short_name()}'
            )

            for U_timing in new_timing_units:
                self.logger.info(
                    f'timing_new {self.process_id} | Timing unit at level {U_timing.level} established.'
                )
            for U_timing in new_timing_units:
                with timer(self.process_id, f'linear_order_{U_timing.level}'):
                    units_to_order = []
                    updated_unordered_units = []
                    for V in self.unordered_units:
                        if self.poset.below(V, U_timing):
                            units_to_order.append(V)
                        else:
                            updated_unordered_units.append(V)

                    ordered_units = self.poset.break_ties(units_to_order)
                    self.linear_order += [W.hash() for W in ordered_units]
                    self.unordered_units = updated_unordered_units

                    printable_unit_hashes = ' '.join(W.short_name()
                                                     for W in ordered_units)
                    n_txs = self.process_txs_in_unit_list(ordered_units)

                self.logger.info(
                    f'add_linear_order {self.process_id} | At lvl {U_timing.level} added {len(units_to_order)} units and {n_txs} txs to the linear order {printable_unit_hashes}'
                )
                timer.write_summary(where=self.logger,
                                    groups=[self.process_id])
                timer.reset(self.process_id)
    def _verify_signatures_and_add_units(self, units_received, peer_id, mode,
                                         ids):
        with timer(ids, 'verify_signatures'):
            succesful = self._verify_signatures(units_received, mode, ids)
        if not succesful:
            self.logger.error(
                f'{mode}_invalid_sign {ids} | Got a unit from {peer_id} with invalid signature; aborting'
            )
            return False

        with timer(ids, 'add_units'):
            succesful = self._add_units(units_received, peer_id, mode, ids)
        if not succesful:
            self.logger.error(
                f'{mode}_not_compliant {ids} | Got unit from {peer_id} that does not comply to the rules; aborting'
            )
            return False
        return True
    def create_unit(self, txs):
        '''
        Attempts to create a new unit in the poset.
        :param list txs: the transactions to include in the unit
        :returns: A new unit if creation was successfull, None otherwise
        '''
        with timer(self.process_id, 'create_unit'):
            U = create_unit(self.poset, self.process_id, txs)

        return U
 async def _receive_units(self, channel, mode, ids):
     self.logger.info(
         f'receive_units_start_{mode} {ids} | Receiving units from {channel.peer_id}'
     )
     data = await channel.read()
     n_bytes = len(data)
     self.logger.info(
         f'receive_units_bytes_{mode} {ids} | Received {n_bytes} bytes from {channel.peer_id}'
     )
     with timer(ids, 'unpickle_units'):
         units_received = pickle.loads(data)
     self.logger.info(
         f'receive_units_done_{mode} {ids} | Received {n_bytes} bytes and {len(units_received)} units'
     )
     return units_received
 async def _send_units(self, to_send, channel, mode, ids):
     self.logger.info(
         f'send_units_start_{mode} {ids} | Sending units to {channel.peer_id}'
     )
     with timer(ids, 'pickle_units'):
         data = pickle.dumps(to_send)
     self.logger.info(
         f'send_units_wait_{mode} {ids} | Sending {len(to_send)} units and {len(data)} bytes to {channel.peer_id}'
     )
     await channel.write(data)
     self.logger.info(
         f'send_units_sent_{mode} {ids} | Sent {len(to_send)} units and {len(data)} bytes to {channel.peer_id}'
     )
     self.logger.info(
         f'send_units_done_{mode} {ids} | Units sent {channel.peer_id}')
    async def listener(self, peer_id):
        '''
        Listen indefinitely for incoming syncs from process peer_id.
        This version is a counterpart for sync, follows the same 3-exchange protocol.
        '''
        channel = self.listen_channels[peer_id]
        while True:
            # step 1
            their_poset_info, ids = await self._receive_poset_info(
                channel, 'listener', None)

            self.n_recv_syncs += 1
            self.logger.info(
                f'listener_sync_no {ids} | Number of syncs is {self.n_recv_syncs}'
            )

            if self.n_recv_syncs > consts.N_RECV_SYNC:
                self.logger.info(
                    f'listener_too_many_syncs {ids} | Too many syncs, rejecting {peer_id}'
                )
                await channel.reject()
                await self.maybe_close(channel)
                self.n_recv_syncs -= 1
                continue

            self.logger.info(
                f'listener_establish {ids} | Connection established with {peer_id}'
            )

            # step 2
            await self._send_poset_info(channel, 'listener', ids)
            with timer(ids, 'prepare_units'):
                to_send, to_request = units_to_send(self.process.poset,
                                                    their_poset_info)
            await self._send_units(to_send, channel, 'listener', ids)
            await self._send_requests(to_request, channel, 'listener', ids)

            # step 3
            units_received = await self._receive_units(channel, 'listener',
                                                       ids)
            their_requests = await self._receive_requests(
                channel, 'listener', ids)

            # step 4 (only if they requested something)
            if any(their_requests):
                self.logger.info(
                    f'listener_extended {ids} | Sync with {peer_id} extended due to forks'
                )
                with timer(ids, 'prepare_units'):
                    to_send, _ = units_to_send(self.process.poset,
                                               their_poset_info,
                                               their_requests)
                await self._send_units(to_send, channel, 'listener', ids)

            if self._verify_signatures_and_add_units(units_received, peer_id,
                                                     'listener', ids):
                self.logger.info(
                    f'listener_succ {ids} | Syncing with {peer_id} successful')
                timer.write_summary(where=self.logger, groups=[ids])
            else:
                self.logger.info(
                    f'listener_fail {ids} | Syncing with {peer_id} failed')

            await self.maybe_close(channel)
            self.n_recv_syncs -= 1
    async def sync(self, peer_id):
        '''
        Sync with process peer_id. This version uses 3-exchange "pullpush" protocol: send heights, receive heights, units and
        requests, send units and requests. If we sent some requests there is a 4th exchange where we once again get units. This
        should only happen due to forks.
        '''

        self.n_init_syncs += 1
        self.logger.info(
            f'sync_sync_no | Number of syncs is {self.n_init_syncs}')
        if self.n_init_syncs > consts.N_INIT_SYNC:
            self.logger.info(
                f'sync_too_many_syncs | Too many syncs, not initiating a new one with {peer_id}'
            )
            self.n_init_syncs -= 1
            return
        channel = self.sync_channels[peer_id]
        if channel.in_use.locked():
            self.logger.info(
                f'sync_canceled {self.process.process_id} | Previous sync with {peer_id} still in progress'
            )
            self.n_init_syncs -= 1
            return

        async with channel.in_use:
            ids = self._new_sync_id(peer_id)
            self.logger.info(
                f'sync_establish_try {ids} | Establishing connection to {peer_id}'
            )
            self.logger.info(
                f'sync_establish {ids} | Established connection to {peer_id}')

            # step 1
            await self._send_poset_info(channel, 'sync', ids)

            # step 2
            try:
                their_poset_info, _ = await self._receive_poset_info(
                    channel, 'sync', ids)
                units_received = await self._receive_units(
                    channel, 'sync', ids)
                their_requests = await self._receive_requests(
                    channel, 'sync', ids)
            except RejectException:
                self.logger.info(
                    f'sync_rejected {ids} | Process {peer_id} rejected sync attempt'
                )
                self.n_init_syncs -= 1
                await self.maybe_close(channel)
                return

            # step 3
            with timer(ids, 'prepare_units'):
                to_send, to_request = units_to_send(self.process.poset,
                                                    their_poset_info,
                                                    their_requests)
            await self._send_units(to_send, channel, 'sync', ids)
            received_hashes = [U.hash() for U in units_received]
            to_request = [[r for r in reqs if r not in received_hashes]
                          for reqs in to_request]
            await self._send_requests(to_request, channel, 'sync', ids)

            # step 4 (only if we requested something)
            if any(to_request):
                self.logger.info(
                    f'sync_extended {ids} | Sync with {peer_id} extended due to forks'
                )
                units_received = await self._receive_units(
                    channel, 'sync', ids)

        await self.maybe_close(channel)

        if self._verify_signatures_and_add_units(units_received, peer_id,
                                                 'sync', ids):
            self.logger.info(
                f'sync_succ {ids} | Syncing with {peer_id} successful')
            timer.write_summary(where=self.logger, groups=[ids])
        else:
            self.logger.info(
                f'sync_fail {ids} | Syncing with {peer_id} failed')
        self.n_init_syncs -= 1
    async def create_add(self, txs_queue, server_started):
        '''
        A task that will keep creating new units.
        :param multiprocessing.Queue txs_queue: a queue of transactions to be added to units
        :param asyncio.Event server_started: a mutex to ensure that the basic connection server starts before we commence unit creation
        '''
        await server_started.wait()
        created_count, max_level_reached = 0, False
        while created_count != consts.UNITS_LIMIT and not max_level_reached:

            # log current memory consumption
            memory_usage_in_mib = (psutil.Process(
                os.getpid()).memory_info().rss) / (2**20)
            self.logger.info(
                f'memory_usage {self.process_id} | {memory_usage_in_mib:.4f} MiB'
            )
            self.logger.info(
                f'max_units {self.process_id} | There are {len(self.poset.max_units)} maximal units just before create_unit'
            )

            txs = self.prepared_txs

            new_unit = self.create_unit(txs)
            created_count += 1

            if new_unit is not None:

                with timer(self.process_id, 'create_unit'):
                    self.poset.prepare_unit(new_unit)
                    assert self.poset.check_compliance(
                        new_unit
                    ), "A unit created by our process is not passing the compliance test!"
                    self.sign_unit(new_unit)

                self.add_unit_to_poset(new_unit)

                n_parents = len(new_unit.parents)
                self.logger.info(
                    f"create_add {self.process_id} | Created a new unit {new_unit.short_name()} with {n_parents} parents"
                )

                self.our_units.append(new_unit)
                if consts.ADAPTIVE_DELAY:
                    self.adjust_create_delay()

                if new_unit.level == consts.LEVEL_LIMIT:
                    max_level_reached = True

                if not txs_queue.empty():
                    self.prepared_txs = txs_queue.get()
                else:
                    self.prepared_txs = []
            else:
                self.logger.info(
                    f'create_fail {self.process_id} | Failed to create a new unit'
                )

            timer.write_summary(where=self.logger, groups=[self.process_id])
            timer.reset(self.process_id)

            await asyncio.sleep(self.create_delay)

        self.keep_syncing = False
        logger = logging.getLogger(consts.LOGGER_NAME)
        if max_level_reached:
            logger.info(
                f'create_stop {self.process_id} | process reached max_level {consts.LEVEL_LIMIT}'
            )
        elif created_count == consts.UNITS_LIMIT:
            logger.info(
                f'create_stop {self.process_id} | process created {consts.UNITS_LIMIT} units'
            )

        # dump the final poset to disc
        self.poset.dump_to_file(f'poset.dag')