Example #1
0
class Domain0Manager:
    """Management for inter-domain collaboration over domain_global_0"""
    DOMAIN_INFO_ADVERTISE_INTERVAL = 1800  # seconds
    DOMAIN_INFO_LIFETIME = 3600
    INITIAL_ACCEPT_LIMIT = 10
    DOMAIN_ACCEPTANCE_RECOVER_INTERVAL = 600  # seconds
    CROSS_REF_PROBABILITY = 0.1
    NUM_OF_COPIES = 3

    ADV_DOMAIN_LIST = to_2byte(0)
    DISTRIBUTE_CROSS_REF = to_2byte(1)
    NOTIFY_CROSS_REF_REGISTERED = to_2byte(2)
    REQUEST_VERIFY = to_2byte(4)
    REQUEST_VERIFY_FROM_OUTER_DOMAIN = to_2byte(5)
    RESPONSE_VERIFY_FROM_OUTER_DOMAIN = to_2byte(6)

    def __init__(self,
                 networking=None,
                 node_id=None,
                 loglevel="all",
                 logname=None):
        self.networking = networking
        self.stats = networking.core.stats
        self.my_node_id = node_id
        self.logger = logger.get_logger(key="domain0",
                                        level=loglevel,
                                        logname=logname)
        self.domains_belong_to = set()
        self.domain_list = dict()  # {domain_id: list(node_id,,,)}
        self.node_domain_list = dict(
        )  # {node_id: {domain_id: expiration_time}}
        self.domain_accept_margin = dict()
        self.requested_cross_refs = dict()
        self.remove_lock = threading.Lock()
        self.advertise_timer_entry = None
        self._update_advertise_timer_entry()
        self.cross_ref_timer_entry = None
        self._update_cross_ref_timer_entry()

    def stop_all_timers(self):
        """Invalidate all running timers"""
        if self.advertise_timer_entry is not None:
            self.advertise_timer_entry.deactivate()
        if self.cross_ref_timer_entry is not None:
            self.cross_ref_timer_entry.deactivate()

    def _register_node(self, domain_id, node_id):
        """Register node of other domain

        Args:
            domain_id (bytes): target domain_id
            node_id (bytes): target node_id
        """
        self.domain_list.setdefault(domain_id, list())
        if node_id not in self.domain_list[domain_id]:
            self.domain_list[domain_id].append(node_id)
        self.node_domain_list.setdefault(node_id,
                                         dict())[domain_id] = int(time.time())

    def _remove_node(self, domain_id, node_id):
        """Remove node from the lists

        Args:
            domain_id (bytes): target domain_id
            node_id (bytes): target node_id
        """
        #print("*** _remove_node at %s:" % self.my_node_id.hex(), node_id.hex(), "in domain", domain_id.hex())
        #print(" ==> before: len(node_domain_list)=%d" % len(self.node_domain_list.keys()))
        self.remove_lock.acquire()
        if domain_id in self.domain_list:
            if node_id in self.domain_list[domain_id]:
                self.domain_list[domain_id].remove(node_id)
                if len(self.domain_list[domain_id]) == 0:
                    self.domain_list.pop(domain_id, None)

        if node_id in self.node_domain_list:
            if domain_id in self.node_domain_list[node_id]:
                self.node_domain_list[node_id].pop(domain_id, None)
                if len(self.node_domain_list[node_id]) == 0:
                    self.node_domain_list.pop(node_id, None)
        self.remove_lock.release()
        #print(" ==> after: len(node_domain_list)=%d" % len(self.node_domain_list.keys()))

    def update_domain_belong_to(self):
        """Update the list domain_belong_to

        domain_belong_to holds all domain_ids that this node belongs to
        """
        self.domains_belong_to = set(self.networking.domains.keys())

    def _update_advertise_timer_entry(self):
        """Update advertisement timer"""
        rand_interval = random.randint(
            int(Domain0Manager.DOMAIN_INFO_ADVERTISE_INTERVAL * 5 / 6),
            int(Domain0Manager.DOMAIN_INFO_ADVERTISE_INTERVAL * 7 / 6))
        self.logger.debug("_update_advertise_timer_entry: %d" % rand_interval)
        self.advertise_timer_entry = query_management.QueryEntry(
            expire_after=rand_interval,
            callback_expire=self._advertise_domain_info,
            retry_count=0)

    def _update_cross_ref_timer_entry(self):
        """Update cross_ref timer"""
        rand_interval = random.randint(
            int(Domain0Manager.DOMAIN_ACCEPTANCE_RECOVER_INTERVAL * 5 / 6),
            int(Domain0Manager.DOMAIN_ACCEPTANCE_RECOVER_INTERVAL * 7 / 6))
        self.logger.debug("update_cross_ref_timer_entry: %d" % rand_interval)
        self.cross_ref_timer_entry = query_management.QueryEntry(
            expire_after=rand_interval,
            callback_expire=self._purge_left_cross_ref,
            retry_count=0)

    def _eliminate_obsoleted_entries(self):
        """Check expiration of the node_domain_list"""
        #print("_eliminate_obsoleted_entries at %s: len(node_domain_list)=%d" % (self.my_node_id.hex(),
        #                                                                       len(self.node_domain_list.keys())))
        for node_id in list(self.node_domain_list.keys()):
            for domain_id in list(self.node_domain_list[node_id].keys()):
                prev_time = self.node_domain_list[node_id][domain_id]
                if int(time.time()
                       ) - prev_time > Domain0Manager.DOMAIN_INFO_LIFETIME:
                    #print(" --> expire node_id=%s in domain %s" % (node_id.hex(), domain_id.hex()))
                    self._remove_node(domain_id, node_id)

    def _advertise_domain_info(self, query_entry):
        """Advertise domain list in the domain_global_0 network"""
        #print("[%s]: _advertise_domain_info" % self.my_node_id.hex()[:4])
        self._eliminate_obsoleted_entries()
        domain_list = list(
            filter(lambda d: d != domain_global_0,
                   self.networking.domains.keys()))
        if len(domain_list) > 0:
            msg = {
                KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_DOMAIN0,
                KeyType.domain_id: domain_global_0,
                KeyType.command: Domain0Manager.ADV_DOMAIN_LIST,
                KeyType.domain_list: domain_list,
            }
            # TODO: need modify below in the case of using Kademlia (or DHT algorithm)
            self.networking.broadcast_message_in_network(
                domain_id=domain_global_0,
                payload_type=PayloadType.Type_msgpack,
                msg=msg)
            self.stats.update_stats_increment("domain0", "send_advertisement",
                                              1)
        self._update_advertise_timer_entry()

    def _update_domain_list(self, msg):
        """Parse binary data and update domain_list

        Args:
            msg (dict): received message
        """
        src_node_id = msg[KeyType.source_node_id]
        new_domains = set(
            filter(lambda d: d not in self.domains_belong_to,
                   msg[KeyType.domain_list]))
        #print("newdomain:", [dm.hex() for dm in new_domains])

        if src_node_id in self.node_domain_list:
            self.remove_lock.acquire()
            deleted = set(
                self.node_domain_list[src_node_id].keys()) - new_domains
            self.remove_lock.release()
            #print("deleted:", [dm.hex() for dm in deleted])
            for dm in deleted:
                self._remove_node(dm, src_node_id)
        for dm in new_domains:
            #print("NEW:", dm.hex())
            self._register_node(dm, src_node_id)

    def distribute_cross_ref_in_domain0(self, domain_id, transaction_id):
        """Determine if the node distributes the cross_ref (into domain_global_0)

        Args:
            domain_id (bytes): target domain_id
            transaction_id (bytes): target transaction_id
        """
        # TODO: probability calculation needs to be modified
        if random.random() > Domain0Manager.CROSS_REF_PROBABILITY:
            return

        self.stats.update_stats_increment("domain0",
                                          "distribute_cross_ref_in_domain0", 1)
        num_copies = Domain0Manager.NUM_OF_COPIES
        if len(self.domain_list) <= Domain0Manager.NUM_OF_COPIES:
            num_copies = len(self.domain_list)
        target_domains = random.sample(tuple(self.domain_list.keys()),
                                       num_copies)
        msg = {
            KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_DOMAIN0,
            KeyType.command: Domain0Manager.DISTRIBUTE_CROSS_REF,
            KeyType.domain_id: domain_global_0,
            KeyType.cross_ref: (domain_id, transaction_id),
        }
        for dm in target_domains:
            if len(self.domain_list[dm]) == 0:
                continue
            dst_node_id = random.choice(self.domain_list[dm])
            msg[KeyType.destination_node_id] = dst_node_id
            self.networking.send_message_in_network(
                nodeinfo=None,
                payload_type=PayloadType.Type_any,
                domain_id=domain_global_0,
                msg=msg)

    def _assign_cross_ref(self, cross_ref):
        """Assign cross_ref to core nodes in domains this node belongs to"""
        now = int(time.time())
        if cross_ref[0] not in self.domain_accept_margin:
            count = self._get_acceptance_margin(cross_ref[0])
            self.domain_accept_margin[cross_ref[0]] = [
                now, count
            ]  # last accepting time, margin
        elif self.domain_accept_margin[cross_ref[0]][1] > 0:
            self.domain_accept_margin[cross_ref[0]][1] -= 1
        else:
            if now - self.domain_accept_margin[cross_ref[0]][
                    0] > Domain0Manager.DOMAIN_ACCEPTANCE_RECOVER_INTERVAL:
                count = self._get_acceptance_margin(cross_ref[0])
                self.domain_accept_margin[cross_ref[0]] = [now, count]
            else:
                self.stats.update_stats_increment(
                    "domain0", "drop_cross_ref_because_exceed_margin", 1)
                return
        self.requested_cross_refs.setdefault(cross_ref[0], dict()).setdefault(
            cross_ref[1], now)

        msg = {
            KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_USER,
            KeyType.infra_command:
            user_message_routing.UserMessageRouting.CROSS_REF_ASSIGNMENT,
            KeyType.cross_ref: cross_ref,
        }

        i = len(self.networking.domains)
        if domain_global_0 in self.networking.domains:
            i -= 1
        if i <= 0:
            return
        if i > Domain0Manager.NUM_OF_COPIES:
            i = Domain0Manager.NUM_OF_COPIES
        dup_check = set()
        while i > 0:
            target_domain = random.choice(tuple(
                self.networking.domains.keys()))
            if target_domain == domain_global_0:
                continue
            if len(self.networking.domains[target_domain]
                   ['neighbor'].nodeinfo_list) > 0:
                dst_node_id = random.choice(
                    tuple(self.networking.domains[target_domain]
                          ['neighbor'].nodeinfo_list.keys()))
                if (target_domain, dst_node_id) in dup_check:
                    continue
                msg[KeyType.domain_id] = target_domain
                msg[KeyType.destination_node_id] = dst_node_id
                self.networking.send_message_in_network(
                    nodeinfo=None,
                    payload_type=PayloadType.Type_any,
                    domain_id=target_domain,
                    msg=msg)
                dup_check.add((target_domain, dst_node_id))
                self.stats.update_stats_increment("domain0",
                                                  "assign_cross_ref_to_nodes",
                                                  1)
            i -= 1

    def _get_acceptance_margin(self, domain_id):
        """Check how many cross ref will be accepted

        Args:
            domain_id (bytes): target domain_id
        """
        if domain_id not in self.networking.domains:
            return 1
        ret = self.networking.domains[domain_id][
            'data'].count_domain_in_cross_ref(domain_id)
        if ret is None:
            ret = 0
        # TODO: need implementation
        return ret + 1

    def _purge_left_cross_ref(self, query_entry):
        """Purge expired cross_ref entries of requested_cross_refs"""
        now = int(time.time())
        for dm in tuple(self.requested_cross_refs.keys()):
            for txid in tuple(self.requested_cross_refs[dm].keys()):
                if now - self.requested_cross_refs[dm][
                        txid] > Domain0Manager.DOMAIN_ACCEPTANCE_RECOVER_INTERVAL:
                    del self.requested_cross_refs[dm][txid]

    def cross_ref_registered(self, domain_id, transaction_id, cross_ref):
        """Notify cross_ref inclusion in a transaction of the outer domain and insert the info into DB

        Args:
            domain_id (bytes): domain_id where the cross_ref is from
            transaction_id (bytes): transaction_id that the cross_ref proves
            cross_ref (bytes): the registered cross_ref in other domain
        """
        cross_ref_domain_id = cross_ref[0]
        cross_ref_txid = cross_ref[1]
        if cross_ref_domain_id not in self.requested_cross_refs or cross_ref_txid not in self.requested_cross_refs[
                cross_ref_domain_id]:
            return
        del self.requested_cross_refs[cross_ref_domain_id][cross_ref_txid]
        self.stats.update_stats_increment("domain0", "cross_ref_registered", 1)
        if len(self.domain_list[cross_ref_domain_id]) == 0:
            return
        try:
            msg = {
                KeyType.infra_msg_type:
                InfraMessageCategory.CATEGORY_DOMAIN0,
                KeyType.command:
                Domain0Manager.NOTIFY_CROSS_REF_REGISTERED,
                KeyType.domain_id:
                domain_global_0,
                KeyType.destination_node_id:
                random.choice(self.domain_list[cross_ref_domain_id]),
                KeyType.outer_domain_id:
                domain_id,
                KeyType.txid_having_cross_ref:
                transaction_id,
                KeyType.cross_ref:
                cross_ref,
            }
            self.networking.send_message_in_network(
                nodeinfo=None,
                payload_type=PayloadType.Type_any,
                domain_id=domain_global_0,
                msg=msg)
        except:
            return

    def _get_transaction_data_for_verification(self, domain_id,
                                               transaction_id):
        """Get transaction object and verify it"""
        txobjs, asts = self.networking.domains[domain_id][
            'data'].search_transaction(transaction_id=transaction_id)
        if transaction_id not in txobjs:
            return None
        txobj = txobjs[transaction_id]
        if txobj.WITH_WIRE:
            self.logger.info(
                "To use cross_reference the transaction object must not be in bson/msgpack format"
            )
            return None
        txobj_is_valid, valid_assets, invalid_assets = bbclib.validate_transaction_object(
            txobj, asts)
        if not txobj_is_valid:
            msg = {
                KeyType.command:
                repair_manager.RepairManager.REQUEST_REPAIR_TRANSACTION,
                KeyType.transaction_id: transaction_id,
            }
            self.networking.domains[domain_id]['repair'].put_message(msg)
            return None
        txobj.digest()
        cross_ref_dat = txobj.cross_ref.pack()
        sigdata = txobj.signatures[0].pack()
        return txobj.transaction_base_digest, cross_ref_dat, sigdata

    def process_message(self, msg):
        """Process received message

        Args:
            msg (dict): received message
        """
        if KeyType.command not in msg:
            return

        if msg[KeyType.command] == Domain0Manager.ADV_DOMAIN_LIST:
            if KeyType.domain_list in msg:
                #print("RECV domain_list at %s from %s" % (self.my_node_id.hex(), msg[KeyType.source_node_id].hex()))
                self.stats.update_stats_increment("domain0", "ADV_DOMAIN_LIST",
                                                  1)
                self._update_domain_list(msg)

        elif msg[KeyType.command] == Domain0Manager.DISTRIBUTE_CROSS_REF:
            if KeyType.cross_ref not in msg:
                return
            self.stats.update_stats_increment("domain0",
                                              "GET_CROSS_REF_DISTRIBUTION", 1)
            self._assign_cross_ref(msg[KeyType.cross_ref])

        elif msg[
                KeyType.command] == Domain0Manager.NOTIFY_CROSS_REF_REGISTERED:
            if KeyType.domain_id not in msg or KeyType.txid_having_cross_ref not in msg:
                return
            outer_domain_id = msg[KeyType.outer_domain_id]
            txid_having_cross_ref = msg[KeyType.txid_having_cross_ref]
            domain_id = msg[KeyType.cross_ref][0]
            transaction_id = msg[KeyType.cross_ref][1]
            self.networking.domains[domain_id]['data'].insert_cross_ref(
                transaction_id, outer_domain_id, txid_having_cross_ref)

        elif msg[KeyType.command] == Domain0Manager.REQUEST_VERIFY:
            self.stats.update_stats_increment("domain0", "REQUEST_VERIFY", 1)
            domain_id = msg[KeyType.domain_id]
            transaction_id = msg[KeyType.transaction_id]
            domain_list = self.networking.domains[domain_id][
                'data'].search_domain_having_cross_ref(transaction_id)
            if domain_list is None or len(domain_list) == 0:
                return
            dm = random.choice(
                domain_list
            )  # "id", "transaction_id", "outer_domain_id", "txid_having_cross_ref"
            if dm[2] not in self.domain_list or len(
                    self.domain_list[dm[2]]) == 0:
                return
            dst_node_id = random.choice(self.domain_list[dm[2]])
            msg2 = {
                KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_DOMAIN0,
                KeyType.command:
                Domain0Manager.REQUEST_VERIFY_FROM_OUTER_DOMAIN,
                KeyType.domain_id: domain_global_0,
                KeyType.destination_node_id: dst_node_id,
                KeyType.source_user_id: msg[KeyType.source_user_id],
                KeyType.transaction_id: transaction_id,
                KeyType.source_domain_id: domain_id,
                KeyType.outer_domain_id: dm[2],
                KeyType.txid_having_cross_ref: dm[3],
            }
            self.networking.send_message_in_network(
                nodeinfo=None,
                payload_type=PayloadType.Type_any,
                domain_id=domain_global_0,
                msg=msg2)

        elif msg[KeyType.
                 command] == Domain0Manager.REQUEST_VERIFY_FROM_OUTER_DOMAIN:
            if KeyType.outer_domain_id not in msg or KeyType.txid_having_cross_ref not in msg:
                return
            domain_id = msg.pop(KeyType.outer_domain_id, None)
            if domain_id not in self.networking.domains:
                return
            transaction_id = msg.pop(KeyType.txid_having_cross_ref, None)
            if transaction_id is None:
                return
            ret = self._get_transaction_data_for_verification(
                domain_id, transaction_id)
            if ret is None:
                return
            msg[KeyType.cross_ref_verification_info] = ret
            msg[KeyType.
                command] = Domain0Manager.RESPONSE_VERIFY_FROM_OUTER_DOMAIN
            msg[KeyType.destination_node_id] = msg[KeyType.source_node_id]
            self.networking.send_message_in_network(
                nodeinfo=None,
                payload_type=PayloadType.Type_any,
                domain_id=domain_global_0,
                msg=msg)

        elif msg[KeyType.
                 command] == Domain0Manager.RESPONSE_VERIFY_FROM_OUTER_DOMAIN:
            domain_id = msg[KeyType.source_domain_id]
            msg2 = {
                KeyType.infra_msg_type:
                InfraMessageCategory.CATEGORY_USER,
                KeyType.command:
                MsgType.RESPONSE_CROSS_REF_VERIFY,
                KeyType.domain_id:
                domain_id,
                KeyType.destination_user_id:
                msg[KeyType.source_user_id],
                KeyType.source_user_id:
                msg[KeyType.source_user_id],
                KeyType.transaction_id:
                msg[KeyType.transaction_id],
                KeyType.cross_ref_verification_info:
                msg[KeyType.cross_ref_verification_info],
            }
            self.networking.domains[domain_id]['user'].send_message_to_user(
                msg2)
Example #2
0
class BBcNetwork:
    """Socket and thread management for infrastructure layers"""
    NOTIFY_LEAVE = to_2byte(0)
    REQUEST_KEY_EXCHANGE = to_2byte(1)
    RESPONSE_KEY_EXCHANGE = to_2byte(2)
    CONFIRM_KEY_EXCHANGE = to_2byte(3)

    def __init__(self,
                 config,
                 core=None,
                 p2p_port=None,
                 external_ip4addr=None,
                 external_ip6addr=None,
                 loglevel="all",
                 logname=None):
        self.core = core
        self.stats = core.stats
        self.logger = logger.get_logger(key="bbc_network",
                                        level=loglevel,
                                        logname=logname)
        self.logname = logname
        self.loglevel = loglevel
        self.config = config
        self.domain0manager = None
        conf = self.config.get_config()
        self.domains = dict()
        self.ip_address, self.ip6_address = _check_my_IPaddresses()
        if external_ip4addr is not None:
            self.external_ip4addr = external_ip4addr
        else:
            self.external_ip4addr = self.ip_address
        if external_ip6addr is not None:
            self.external_ip6addr = external_ip6addr
        else:
            self.external_ip6addr = self.ip6_address
        if p2p_port is not None:
            conf['network']['p2p_port'] = p2p_port
            self.config.update_config()
        self.port = conf['network']['p2p_port']
        self.socket_udp = None
        self.socket_udp6 = None
        if not self.setup_udp_socket():
            self.logger.error("** Fail to setup UDP socket **")
            return
        self.listen_socket = None
        self.listen_socket6 = None
        self.max_connections = conf['network']['max_connections']
        if not self.setup_tcp_server():
            self.logger.error("** Fail to setup TCP server **")
            return

    def _get_my_nodeinfo(self, node_id):
        """Return NodeInfo

        Args:
            node_id (bytes): my node_id
        Returns:
            NodeInfo: my NodeInfo
        """
        ipv4 = self.ip_address
        if self.external_ip4addr is not None:
            ipv4 = self.external_ip4addr
        if ipv4 is None or len(ipv4) == 0:
            ipv4 = "0.0.0.0"
        ipv6 = self.ip6_address
        if self.external_ip6addr is not None:
            ipv6 = self.external_ip6addr
        if ipv6 is None or len(ipv6) == 0:
            ipv6 = "::"
        domain0 = True if self.domain0manager is not None else False
        return NodeInfo(node_id=node_id,
                        ipv4=ipv4,
                        ipv6=ipv6,
                        port=self.port,
                        domain0=domain0)

    def include_admin_info_into_message_if_needed(self, domain_id, msg,
                                                  admin_info):
        """Serialize admin info into one binary object and add signature"""
        admin_info[KeyType.message_seq] = self.domains[domain_id][
            "neighbor"].admin_sequence_number + 1
        self.domains[domain_id]["neighbor"].admin_sequence_number += 1
        if "keypair" in self.domains[domain_id] and self.domains[domain_id][
                "keypair"] is not None:
            msg[KeyType.
                admin_info] = message_key_types.make_TLV_formatted_message(
                    admin_info)
            digest = hashlib.sha256(msg[KeyType.admin_info]).digest()
            msg[KeyType.nodekey_signature] = self.domains[domain_id][
                "keypair"]['keys'][0].sign(digest)
        else:
            msg.update(admin_info)

    def send_key_exchange_message(self, domain_id, node_id, command, pubkey,
                                  nonce, random_val, key_name):
        """Send ECDH key exchange message"""
        if command == "request":
            command = BBcNetwork.REQUEST_KEY_EXCHANGE
        elif command == "response":
            command = BBcNetwork.RESPONSE_KEY_EXCHANGE
        else:
            command = BBcNetwork.CONFIRM_KEY_EXCHANGE
        msg = {
            KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_NETWORK,
            KeyType.domain_id: domain_id,
            KeyType.destination_node_id: node_id,
        }
        admin_info = {
            KeyType.destination_node_id:
            node_id,  # To defend from replay attack
            KeyType.command: command,
            KeyType.ecdh: pubkey,
            KeyType.nonce: nonce,
            KeyType.random: random_val,
            KeyType.hint: key_name,
        }
        self.include_admin_info_into_message_if_needed(domain_id, msg,
                                                       admin_info)
        return self.send_message_in_network(None, PayloadType.Type_msgpack,
                                            domain_id, msg)

    def create_domain(self, domain_id=ZEROS, config=None):
        """Create domain and register user in the domain

        Args:
            domain_id (bytes): target domain_id to create
            config (dict): configuration for the domain
        Returns:
            bool:
        """
        if domain_id in self.domains:
            return False

        conf = self.config.get_domain_config(domain_id, create_if_new=True)
        if config is not None:
            conf.update(config)
        if 'node_id' not in conf or conf['node_id'] == "":
            node_id = bbclib.get_random_id()
            conf['node_id'] = bbclib.convert_id_to_string(node_id)
            self.config.update_config()
        else:
            node_id = bbclib.convert_idstring_to_bytes(conf.get('node_id'))

        self.domains[domain_id] = dict()
        self.domains[domain_id]['node_id'] = node_id
        self.domains[domain_id]['name'] = node_id.hex()[:4]
        self.domains[domain_id]['neighbor'] = NeighborInfo(
            network=self,
            domain_id=domain_id,
            node_id=node_id,
            my_info=self._get_my_nodeinfo(node_id))
        self.domains[domain_id]['topology'] = TopologyManagerBase(
            network=self,
            domain_id=domain_id,
            node_id=node_id,
            logname=self.logname,
            loglevel=self.loglevel)
        self.domains[domain_id]['user'] = UserMessageRouting(
            self, domain_id, logname=self.logname, loglevel=self.loglevel)
        self.get_domain_keypair(domain_id)

        workingdir = self.config.get_config()['workingdir']
        if domain_id == ZEROS:
            self.domains[domain_id]['data'] = DataHandlerDomain0(
                self,
                domain_id=domain_id,
                logname=self.logname,
                loglevel=self.loglevel)
            self.domain0manager = Domain0Manager(self,
                                                 node_id=node_id,
                                                 logname=self.logname,
                                                 loglevel=self.loglevel)
        else:
            self.domains[domain_id]['data'] = DataHandler(
                self,
                config=conf,
                workingdir=workingdir,
                domain_id=domain_id,
                logname=self.logname,
                loglevel=self.loglevel)

        self.domains[domain_id]['repair'] = RepairManager(
            self,
            domain_id,
            workingdir=workingdir,
            logname=self.logname,
            loglevel=self.loglevel)

        if self.domain0manager is not None:
            self.domain0manager.update_domain_belong_to()
            for dm in self.domains.keys():
                if dm != ZEROS:
                    self.domains[dm]['neighbor'].my_info.update(domain0=True)
            self.domains[domain_id]['topology'].update_refresh_timer_entry(1)
        self.stats.update_stats_increment("network", "num_domains", 1)
        self.logger.info("Domain %s is created" % (domain_id.hex()))
        return True

    def remove_domain(self, domain_id=ZEROS):
        """Leave the domain and remove it

        Args:
            domain_id (bytes): target domain_id to remove
        Returns:
            bool: True if successful
        """
        if domain_id not in self.domains:
            return False
        self.domains[domain_id]['topology'].stop_all_timers()
        self.domains[domain_id]['user'].stop_all_timers()
        self.domains[domain_id]['repair'].exit_loop()
        for nd in self.domains[domain_id]["neighbor"].nodeinfo_list.values():
            nd.key_manager.stop_all_timers()

        msg = {
            KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_NETWORK,
            KeyType.domain_id: domain_id,
            KeyType.command: BBcNetwork.NOTIFY_LEAVE,
        }
        admin_info = {
            KeyType.source_node_id:
            self.domains[domain_id]["neighbor"].my_node_id,
            KeyType.nonce:
            bbclib.get_random_value(32)  # just for randomization
        }
        self.include_admin_info_into_message_if_needed(domain_id, msg,
                                                       admin_info)
        self.broadcast_message_in_network(domain_id=domain_id, msg=msg)

        if domain_id == ZEROS:
            self.domain0manager.stop_all_timers()
            for dm in self.domains.keys():
                if dm != ZEROS:
                    self.domains[dm]['neighbor'].my_info.update(domain0=False)
                    self.domains[dm]['topology'].update_refresh_timer_entry(1)
        del self.domains[domain_id]
        if self.domain0manager is not None:
            self.domain0manager.update_domain_belong_to()
        self.config.remove_domain_config(domain_id)
        self.stats.update_stats_decrement("network", "num_domains", 1)
        self.logger.info("Domain %s is removed" % (domain_id.hex()))
        return True

    def save_all_static_node_list(self):
        """Save all static nodes in the config file"""
        self.logger.info("Saving the neighbor list")
        for domain_id in self.domains.keys():
            conf = self.config.get_domain_config(domain_id)
            conf['static_node'] = dict()
            for node_id, nodeinfo in self.domains[domain_id][
                    'neighbor'].nodeinfo_list.items():
                if nodeinfo.is_static:
                    nid = bbclib.convert_id_to_string(node_id)
                    info = _convert_to_string(
                        [nodeinfo.ipv4, nodeinfo.ipv6, nodeinfo.port])
                    conf['static_node'][nid] = info
        self.config.update_config()
        self.logger.info("Done...")

    def send_message_to_a_domain0_manager(self, domain_id, msg):
        """Choose one of domain0_managers and send msg to it

        Args:
            domain_id (bytes): target domain_id
            msg (bytes): message to send
        """
        if domain_id not in self.domains:
            return
        managers = tuple(
            filter(lambda nd: nd.is_domain0_node,
                   self.domains[domain_id]['neighbor'].nodeinfo_list.values()))
        if len(managers) == 0:
            return
        dst_manager = random.choice(managers)
        msg[KeyType.destination_node_id] = dst_manager.node_id
        msg[KeyType.infra_msg_type] = InfraMessageCategory.CATEGORY_DOMAIN0
        self.send_message_in_network(dst_manager, PayloadType.Type_msgpack,
                                     domain_id, msg)

    def get_domain_keypair(self, domain_id):
        """Get domain_keys (private key and public key)

        Args:
            domain_id (bytes): target domain_id
        """
        keyconfig = self.config.get_config().get('domain_key', None)
        if keyconfig is None:
            self.domains[domain_id]['keypair'] = None
            return
        if 'use' not in keyconfig or not keyconfig['use']:
            return
        if 'directory' not in keyconfig or not os.path.exists(
                keyconfig['directory']):
            self.domains[domain_id]['keypair'] = None
            return
        domain_id_str = domain_id.hex()
        keypair = bbclib.KeyPair()
        try:
            with open(
                    os.path.join(keyconfig['directory'],
                                 domain_id_str + ".pem"), "r") as f:
                keypair.mk_keyobj_from_private_key_pem(f.read())
        except:
            self.domains[domain_id]['keypair'] = None
            return
        self.domains[domain_id].setdefault('keypair', dict())
        self.domains[domain_id]['keypair'].setdefault('keys', list())
        self.domains[domain_id]['keypair']['keys'].insert(0, keypair)
        timer = self.domains[domain_id]['keypair'].setdefault('timer', None)
        if timer is None or not timer.active:
            self.domains[domain_id]['keypair'][
                'timer'] = query_management.QueryEntry(
                    expire_after=keyconfig['obsolete_timeout'],
                    data={KeyType.domain_id: domain_id},
                    callback_expire=self._delete_obsoleted_domain_keys)
        else:
            timer.update_expiration_time(keyconfig['obsolete_timeout'])
        self.domains[domain_id]['keypair']['keys'].insert(0, keypair)

    def _delete_obsoleted_domain_keys(self, query_entry):
        domain_id = query_entry.data[KeyType.domain_id]
        if self.domains[domain_id]['keypair'] is not None and len(
                self.domains[domain_id]['keypair']['keys']) > 1:
            del self.domains[domain_id]['keypair']['keys'][1:]

    def send_domain_ping(self, domain_id, ipv4, ipv6, port, is_static=False):
        """Send domain ping to the specified node

        Args:
            domain_id (bytes): target domain_id
            ipv4 (str): IPv4 address of the node
            ipv6 (str): IPv6 address of the node
            port (int): Port number
            is_static (bool): If true, the entry is treated as static one and will be saved in config.json
        Returns:
            bool: True if successful
        """
        if domain_id not in self.domains:
            return False
        if ipv4 is None and ipv6 is None:
            return False
        node_id = self.domains[domain_id]['neighbor'].my_node_id
        nodeinfo = NodeInfo(ipv4=ipv4,
                            ipv6=ipv6,
                            port=port,
                            is_static=is_static)
        query_entry = query_management.QueryEntry(
            expire_after=10,
            callback_error=self._domain_ping,
            callback_expire=self._invalidate_neighbor,
            data={
                KeyType.domain_id: domain_id,
                KeyType.node_id: node_id,
                KeyType.node_info: nodeinfo
            },
            retry_count=3)
        self._domain_ping(query_entry)
        return True

    def _domain_ping(self, query_entry):
        """Send domain ping"""
        domain_id = query_entry.data[KeyType.domain_id]
        msg = {
            KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_NETWORK,
            KeyType.domain_id: domain_id,
        }
        admin_info = {
            KeyType.node_id: query_entry.data[KeyType.node_id],
            KeyType.domain_ping: 0,
            KeyType.nonce: query_entry.nonce,
            KeyType.static_entry:
            query_entry.data[KeyType.node_info].is_static,
        }
        if self.external_ip4addr is not None:
            admin_info[KeyType.external_ip4addr] = self.external_ip4addr
        else:
            admin_info[KeyType.external_ip4addr] = self.ip_address
        if self.external_ip6addr is not None:
            admin_info[KeyType.external_ip6addr] = self.external_ip6addr
        else:
            admin_info[KeyType.external_ip6addr] = self.ip6_address
        if query_entry.data[KeyType.node_info].ipv6 is not None:
            self.logger.debug("Send domain_ping to %s:%d" %
                              (query_entry.data[KeyType.node_info].ipv6,
                               query_entry.data[KeyType.node_info].port))
        else:
            self.logger.debug("Send domain_ping to %s:%d" %
                              (query_entry.data[KeyType.node_info].ipv4,
                               query_entry.data[KeyType.node_info].port))
        query_entry.update(fire_after=1)
        self.stats.update_stats_increment("network", "domain_ping_send", 1)

        self.include_admin_info_into_message_if_needed(domain_id, msg,
                                                       admin_info)
        self.send_message_in_network(query_entry.data[KeyType.node_info],
                                     PayloadType.Type_msgpack, domain_id, msg)

    def _receive_domain_ping(self, domain_id, port, msg):
        """Process received domain_ping.

        If KeyType.domain_ping value is 1, the sender of the ping is registered as static

        Args:
            domain_id (bytes): target domain_id
            port (int): Port number
            msg (dict): received message
        """
        if KeyType.node_id not in msg:
            return
        self.stats.update_stats_increment("network", "domain_ping_receive", 1)
        node_id = msg[KeyType.node_id]
        ipv4 = msg.get(KeyType.external_ip4addr, None)
        ipv6 = msg.get(KeyType.external_ip6addr, None)
        is_static = msg.get(KeyType.static_entry, False)

        self.logger.debug("Receive domain_ping for domain %s from %s" %
                          (binascii.b2a_hex(domain_id[:4]), (ipv4, ipv6)))
        self.logger.debug(msg)
        if domain_id not in self.domains:
            self.logger.debug("no domain_id")
            return
        if self.domains[domain_id]['neighbor'].my_node_id == node_id:
            self.logger.debug("no other node_id")
            return

        self.add_neighbor(domain_id=domain_id,
                          node_id=node_id,
                          ipv4=ipv4,
                          ipv6=ipv6,
                          port=port,
                          is_static=is_static)
        self.stats.update_stats_increment("network", "domain_ping_received", 1)

        if msg[KeyType.domain_ping] == 1:
            query_entry = ticker.get_entry(msg[KeyType.nonce])
            query_entry.deactivate()
        else:
            nonce = msg[KeyType.nonce]
            msg = {
                KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_NETWORK,
                KeyType.domain_id: domain_id,
            }
            admin_info = {
                KeyType.node_id:
                self.domains[domain_id]['neighbor'].my_node_id,
                KeyType.domain_ping: 1,
                KeyType.nonce: nonce,
                KeyType.static_entry: is_static,
            }
            if self.external_ip4addr is not None:
                admin_info[KeyType.external_ip4addr] = self.external_ip4addr
            else:
                admin_info[KeyType.external_ip4addr] = self.ip_address
            if self.external_ip6addr is not None:
                admin_info[KeyType.external_ip6addr] = self.external_ip6addr
            else:
                admin_info[KeyType.external_ip6addr] = self.ip6_address
            self.include_admin_info_into_message_if_needed(
                domain_id, msg, admin_info)
            nodeinfo = NodeInfo(ipv4=ipv4, ipv6=ipv6, port=port)
            self.send_message_in_network(nodeinfo, PayloadType.Type_msgpack,
                                         domain_id, msg)

    def _invalidate_neighbor(self, query_entry):
        """Set the flag of the nodeinfo false"""
        domain_id = query_entry.data[KeyType.domain_id]
        node_id = query_entry.data[KeyType.node_id]
        try:
            self.domains[domain_id]['neighbor'].nodelist[
                node_id].is_alive = False
        except:
            pass

    def send_message_in_network(self,
                                nodeinfo=None,
                                payload_type=PayloadType.Type_any,
                                domain_id=None,
                                msg=None):
        """Send message over a domain network

        Args:
            nodeinfo (NodeInfo): NodeInfo object of the destination
            payload_type (bytes): message format type
            domain_id (bytes): target domain_id
            msg (dict): message to send
        Returns:
            bool: True if successful
        """
        if nodeinfo is None:
            if domain_id not in self.domains:
                return False
            if msg[KeyType.destination_node_id] not in self.domains[domain_id][
                    'neighbor'].nodeinfo_list:
                return False
            nodeinfo = self.domains[domain_id]['neighbor'].nodeinfo_list[msg[
                KeyType.destination_node_id]]
        msg[KeyType.
            source_node_id] = self.domains[domain_id]['neighbor'].my_node_id

        if payload_type == PayloadType.Type_any:
            if nodeinfo.key_manager is not None and nodeinfo.key_manager.key_name is not None and \
                    nodeinfo.key_manager.key_name in message_key_types.encryptors:
                payload_type = PayloadType.Type_encrypted_msgpack
            else:
                payload_type = PayloadType.Type_msgpack

        if payload_type in [PayloadType.Type_msgpack, PayloadType.Type_binary]:
            data_to_send = message_key_types.make_message(payload_type, msg)
        elif payload_type == PayloadType.Type_encrypted_msgpack:
            payload_type = PayloadType.Type_encrypted_msgpack
            data_to_send = message_key_types.make_message(
                payload_type, msg, key_name=nodeinfo.key_manager.key_name)
            if data_to_send is None:
                self.logger.error("Fail to encrypt message")
                return False
        else:
            return False

        if len(data_to_send) > TCP_THRESHOLD_SIZE:
            _send_data_by_tcp(ipv4=nodeinfo.ipv4,
                              ipv6=nodeinfo.ipv6,
                              port=nodeinfo.port,
                              msg=data_to_send)
            self.stats.update_stats_increment("network", "send_msg_by_tcp", 1)
            self.stats.update_stats_increment("network", "sent_data_size",
                                              len(data_to_send))
            return True
        if nodeinfo.ipv6 is not None and self.socket_udp6 is not None:
            self.socket_udp6.sendto(data_to_send,
                                    (nodeinfo.ipv6, nodeinfo.port))
            self.stats.update_stats_increment("network", "send_msg_by_udp6", 1)
            self.stats.update_stats_increment("network", "sent_data_size",
                                              len(data_to_send))
            return True
        if nodeinfo.ipv4 is not None and self.socket_udp is not None:
            self.socket_udp.sendto(data_to_send,
                                   (nodeinfo.ipv4, nodeinfo.port))
            self.stats.update_stats_increment("network", "send_msg_by_udp4", 1)
            self.stats.update_stats_increment("network", "sent_data_size",
                                              len(data_to_send))
            return True

    def broadcast_message_in_network(self,
                                     domain_id,
                                     payload_type=PayloadType.Type_any,
                                     msg=None):
        """Send message to all neighbor nodes

        Args:
            payload_type (bytes): message format type
            domain_id (bytes): target domain_id
            msg (dict): message to send
        Returns:
            bool: True if successful
        """
        if domain_id not in self.domains:
            return
        for node_id, nodeinfo in self.domains[domain_id][
                'neighbor'].nodeinfo_list.items():
            msg[KeyType.destination_node_id] = node_id
            #print("broadcast:", node_id.hex(), node_id)
            self.send_message_in_network(nodeinfo, payload_type, domain_id,
                                         msg)

    def add_neighbor(self,
                     domain_id,
                     node_id,
                     ipv4=None,
                     ipv6=None,
                     port=None,
                     is_static=False):
        """Add node in the neighbor list

        Args:
            domain_id (bytes): target domain_id
            node_id (bytes): target node_id
            ipv4 (str): IPv4 address of the node
            ipv6 (str): IPv6 address of the node
            port (int): Port number that the node is waiting at
            is_static (bool): If true, the entry is treated as static one and will be saved in config.json
        Returns:
            bool: True if it is a new entry, None if error.
        """
        if domain_id not in self.domains or self.domains[domain_id][
                'neighbor'].my_node_id == node_id or port is None:
            return None

        is_new = self.domains[domain_id]['neighbor'].add(node_id=node_id,
                                                         ipv4=ipv4,
                                                         ipv6=ipv6,
                                                         port=port,
                                                         is_static=is_static)
        if is_new is not None and is_new:
            nodelist = self.domains[domain_id]['neighbor'].nodeinfo_list
            self.domains[domain_id]['topology'].notify_neighbor_update(
                node_id, is_new=True)
            self.stats.update_stats("network", "neighbor_nodes", len(nodelist))
        return is_new

    def check_admin_signature(self, domain_id, msg):
        """Check admin signature in the message

        Args:
            domain_id (bytes): target domain_id
            msg (dict): received message
        Returns:
            bool: True if valid
        """
        if domain_id not in self.domains:
            return False
        if "keypair" not in self.domains[
                domain_id] or self.domains[domain_id]["keypair"] is None:
            return True
        if KeyType.nodekey_signature not in msg or KeyType.admin_info not in msg:
            return False
        digest = hashlib.sha256(msg[KeyType.admin_info]).digest()
        flag = False
        for key in self.domains[domain_id]["keypair"]['keys']:
            if key.verify(digest, msg[KeyType.nodekey_signature]):
                flag = True
                break
        if not flag:
            return False
        admin_info = message_key_types.make_dictionary_from_TLV_format(
            msg[KeyType.admin_info])
        msg.update(admin_info)
        return True

    def _process_message_base(self, domain_id, ipv4, ipv6, port, msg):
        """Process received message (common process for any kind of network module)

        Args:
            domain_id (bytes): target domain_id
            ipv4 (str): IPv4 address of the sender node
            ipv6 (str): IPv6 address of the sender node
            port (int): Port number of the sender
            msg (dict): received message
        """
        if KeyType.infra_msg_type not in msg:
            return
        self.logger.debug("[%s] process_message(type=%d)" %
                          (self.domains[domain_id]['name'],
                           int.from_bytes(msg[KeyType.infra_msg_type], 'big')))

        if msg[KeyType.
               infra_msg_type] == InfraMessageCategory.CATEGORY_NETWORK:
            self._process_message(domain_id, ipv4, ipv6, port, msg)

        elif msg[KeyType.infra_msg_type] == InfraMessageCategory.CATEGORY_USER:
            self.add_neighbor(domain_id, msg[KeyType.source_node_id], ipv4,
                              ipv6, port)
            self.domains[domain_id]['user'].process_message(msg)
        elif msg[KeyType.infra_msg_type] == InfraMessageCategory.CATEGORY_DATA:
            self.add_neighbor(domain_id, msg[KeyType.source_node_id], ipv4,
                              ipv6, port)
            self.domains[domain_id]['data'].process_message(msg)
        elif msg[KeyType.
                 infra_msg_type] == InfraMessageCategory.CATEGORY_TOPOLOGY:
            self.add_neighbor(domain_id, msg[KeyType.source_node_id], ipv4,
                              ipv6, port)
            self.domains[domain_id]['topology'].process_message(msg)
        elif msg[KeyType.
                 infra_msg_type] == InfraMessageCategory.CATEGORY_DOMAIN0:
            self.add_neighbor(domain_id, msg[KeyType.source_node_id], ipv4,
                              ipv6, port)
            self.domain0manager.process_message(msg)

    def _process_message(self, domain_id, ipv4, ipv6, port, msg):
        """Process received message

        Args:
            domain_id (bytes): target domain_id
            ipv4 (str): IPv4 address of the sender node
            ipv6 (str): IPv6 address of the sender node
            port (int): Port number of the sender
            msg (dict): received message
        """
        if not self.check_admin_signature(domain_id, msg):
            self.logger.error("Illegal access to domain %s" % domain_id.hex())
            return

        source_node_id = msg[KeyType.source_node_id]
        if source_node_id in self.domains[domain_id]["neighbor"].nodeinfo_list:
            admin_msg_seq = msg[KeyType.message_seq]
            if self.domains[domain_id]["neighbor"].nodeinfo_list[
                    source_node_id].admin_sequence_number >= admin_msg_seq:
                return
            self.domains[domain_id]["neighbor"].nodeinfo_list[
                source_node_id].admin_sequence_number = admin_msg_seq

        if KeyType.domain_ping in msg and port is not None:
            self._receive_domain_ping(domain_id, port, msg)

        elif msg[KeyType.command] == BBcNetwork.REQUEST_KEY_EXCHANGE:
            if KeyType.ecdh in msg and KeyType.hint in msg and KeyType.nonce in msg and KeyType.random in msg:
                if source_node_id not in self.domains[domain_id][
                        'neighbor'].nodeinfo_list:
                    self.add_neighbor(domain_id, source_node_id, ipv4, ipv6,
                                      port)
                nodeinfo = self.domains[domain_id]['neighbor'].nodeinfo_list[
                    source_node_id]
                if nodeinfo.key_manager is None:
                    nodeinfo.key_manager = KeyExchangeManager(
                        self, domain_id, source_node_id)
                nodeinfo.key_manager.receive_exchange_request(
                    msg[KeyType.ecdh], msg[KeyType.nonce], msg[KeyType.random],
                    msg[KeyType.hint])

        elif msg[KeyType.command] == BBcNetwork.RESPONSE_KEY_EXCHANGE:
            if KeyType.ecdh in msg and KeyType.hint in msg and KeyType.nonce in msg and KeyType.random in msg:
                nodeinfo = self.domains[domain_id]['neighbor'].nodeinfo_list[
                    source_node_id]
                nodeinfo.key_manager.receive_exchange_response(
                    msg[KeyType.ecdh], msg[KeyType.random], msg[KeyType.hint])

        elif msg[KeyType.command] == BBcNetwork.CONFIRM_KEY_EXCHANGE:
            nodeinfo = self.domains[domain_id]['neighbor'].nodeinfo_list[
                source_node_id]
            nodeinfo.key_manager.receive_confirmation()

        elif msg[KeyType.command] == BBcNetwork.NOTIFY_LEAVE:
            if KeyType.source_node_id in msg:
                self.domains[domain_id]['topology'].notify_neighbor_update(
                    source_node_id, is_new=False)
                self.domains[domain_id]['neighbor'].remove(source_node_id)

    def setup_udp_socket(self):
        """Setup UDP socket"""
        try:
            self.socket_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            self.socket_udp.bind(("0.0.0.0", self.port))
        except OSError:
            self.socket_udp = None
            self.logger.error("UDP Socket error for IPv4")
        if self.ip6_address is not None:
            try:
                self.socket_udp6 = socket.socket(socket.AF_INET6,
                                                 socket.SOCK_DGRAM)
                self.socket_udp6.setsockopt(socket.IPPROTO_IPV6,
                                            socket.IPV6_V6ONLY, 1)
                self.socket_udp6.bind(("::", self.port))
            except OSError:
                self.socket_udp6 = None
                self.logger.error("UDP Socket error for IPv6")
        if self.socket_udp is None and self.socket_udp6 is None:
            return False
        th_nw_loop = threading.Thread(target=self.udp_message_loop)
        th_nw_loop.setDaemon(True)
        th_nw_loop.start()
        return True

    def udp_message_loop(self):
        """Message loop for UDP socket"""
        self.logger.debug("Start udp_message_loop")
        msg_parser = message_key_types.Message()
        # readfds = set([self.socket_udp, self.socket_udp6])
        readfds = set()
        if self.socket_udp:
            readfds.add(self.socket_udp)
        if self.socket_udp6:
            readfds.add(self.socket_udp6)
        try:
            while True:
                rready, wready, xready = select.select(readfds, [], [])
                for sock in rready:
                    data = None
                    ipv4 = None
                    ipv6 = None
                    if sock is self.socket_udp:
                        data, (ipv4, port) = self.socket_udp.recvfrom(1500)
                    elif sock is self.socket_udp6:
                        data, (ipv6, port) = self.socket_udp6.recvfrom(1500)
                    if data is not None:
                        self.stats.update_stats_increment(
                            "network", "packets_received_by_udp", 1)
                        msg_parser.recv(data)
                        msg = msg_parser.parse()
                        #self.logger.debug("Recv_UDP from %s: data=%s" % (addr, msg))
                        if KeyType.domain_id not in msg:
                            continue
                        if msg[KeyType.domain_id] in self.domains:
                            self._process_message_base(msg[KeyType.domain_id],
                                                       ipv4, ipv6, port, msg)
        finally:
            for sock in readfds:
                sock.close()
            self.socket_udp = None
            self.socket_udp6 = None

    def setup_tcp_server(self):
        """Start tcp server"""
        try:
            self.listen_socket = socket.socket(socket.AF_INET,
                                               socket.SOCK_STREAM)
            self.listen_socket.bind(("0.0.0.0", self.port))
            self.listen_socket.listen(self.max_connections)
        except OSError:
            self.listen_socket = None
            self.logger.error("TCP Socket error for IPv4")
        if self.ip6_address is not None:
            try:
                self.listen_socket6 = socket.socket(socket.AF_INET6,
                                                    socket.SOCK_STREAM)
                self.listen_socket6.setsockopt(socket.IPPROTO_IPV6,
                                               socket.IPV6_V6ONLY, 1)
                self.listen_socket6.bind(("::", self.port))
                self.listen_socket6.listen(self.max_connections)
            except OSError:
                self.listen_socket6 = None
                self.logger.error("TCP Socket error for IPv6")
        if self.listen_socket is None and self.listen_socket6 is None:
            return False
        th_tcp_loop = threading.Thread(target=self.tcpserver_loop)
        th_tcp_loop.setDaemon(True)
        th_tcp_loop.start()
        return True

    def tcpserver_loop(self):
        """Message loop for TCP socket"""
        self.logger.debug("Start tcpserver_loop")
        msg_parsers = dict()
        readfds = set()
        if self.listen_socket:
            readfds.add(self.listen_socket)
        if self.listen_socket6:
            readfds.add(self.listen_socket6)
        try:
            while True:
                rready, wready, xready = select.select(readfds, [], [])
                for sock in rready:
                    if sock is self.listen_socket:
                        conn, address = self.listen_socket.accept()
                        conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,
                                        1)
                        #print("accept from ipv4: ", address)
                        readfds.add(conn)
                        msg_parsers[conn] = message_key_types.Message()
                    elif sock is self.listen_socket6:
                        conn, address = self.listen_socket6.accept()
                        conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,
                                        1)
                        #print("accept from ipv6: ", address)
                        readfds.add(conn)
                        msg_parsers[conn] = message_key_types.Message()
                    else:
                        buf = sock.recv(8192)
                        if len(buf) == 0:
                            del msg_parsers[sock]
                            sock.close()
                            readfds.remove(sock)
                        else:
                            msg_parsers[sock].recv(buf)
                            self.stats.update_stats_increment(
                                "network", "message_size_received_by_tcy",
                                len(buf))
                            while True:
                                msg = msg_parsers[sock].parse()
                                if msg is None:
                                    break
                                #self.logger.debug("Recv_TCP at %s: data=%s" % (sock.getsockname(), msg))
                                if KeyType.destination_node_id not in msg or KeyType.domain_id not in msg:
                                    continue
                                self._process_message_base(
                                    msg[KeyType.domain_id], None, None, None,
                                    msg)

        finally:
            for sock in readfds:
                sock.close()
            self.listen_socket = None
            self.listen_socket6 = None
Example #3
0
class UserMessageRouting:
    """Handle message for clients"""
    REFRESH_FORWARDING_LIST_INTERVAL = 300
    RESOLVE_TIMEOUT = 5
    MAX_CROSS_REF_STOCK = 10
    RESOLVE_USER_LOCATION = to_2byte(0)
    RESPONSE_USER_LOCATION = to_2byte(1)
    RESPONSE_NO_SUCH_USER = to_2byte(2)
    JOIN_MULTICAST_RECEIVER = to_2byte(3)
    LEAVE_MULTICAST_RECEIVER = to_2byte(4)
    CROSS_REF_ASSIGNMENT = to_2byte(5)

    def __init__(self, networking, domain_id, loglevel="all", logname=None):
        self.networking = networking
        self.stats = networking.core.stats
        self.domain_id = domain_id
        self.logger = logger.get_logger(key="user_message_routing",
                                        level=loglevel,
                                        logname=logname)
        self.aes_name_list = dict()
        self.cross_ref_list = list()
        self.registered_users = dict()
        self.forwarding_entries = dict()
        self.on_going_timers = set()

    def stop_all_timers(self):
        """Cancel all running timers"""
        for user_id in self.forwarding_entries.keys():
            if self.forwarding_entries[user_id]['refresh'] is not None:
                self.forwarding_entries[user_id]['refresh'].deactivate()
        for q in self.on_going_timers:
            ticker.get_entry(q).deactivate()

    def set_aes_name(self, socket, name):
        """Set name for specifying AES key for message encryption

        Args:
            socket (Socket): socket for the client
            name (bytes): name of the client (4-byte random value generated in message_key_types.get_ECDH_parameters)
        """
        self.aes_name_list[socket] = name

    def register_user(self, user_id, socket, on_multiple_nodes=False):
        """Register user to forward message

        Args:
            user_id (bytes): user_id of the client
            socket (Socket): socket for the client
            on_multiple_nodes (bool): If True, the user_id is also registered in other nodes, meaning multicasting.
        """
        self.registered_users.setdefault(user_id, set())
        self.registered_users[user_id].add(socket)
        if on_multiple_nodes:
            self.send_multicast_join(user_id)

    def unregister_user(self, user_id, socket):
        """Unregister user from the list and delete AES key if exists

        Args:
            user_id (bytes): user_id of the client
            socket (Socket): socket for the client
        """
        if user_id not in self.registered_users:
            return
        self.registered_users[user_id].remove(socket)
        if len(self.registered_users[user_id]) == 0:
            self.registered_users.pop(user_id, None)
        if socket in self.aes_name_list:
            message_key_types.unset_cipher(self.aes_name_list[socket])
            del self.aes_name_list[socket]
        self.send_multicast_leave(user_id=user_id)

    def _add_user_for_forwarding(self, user_id, node_id, permanent=False):
        """Register user to forwarding list

        Args:
            user_id (bytes): target user_id
            node_id (bytes): node_id which the client with the user_id connects to
            parmanent (bool): If True, the entry won't expire
        """
        self.forwarding_entries.setdefault(user_id, dict())
        if not permanent:
            if 'refresh' not in self.forwarding_entries[user_id]:
                query_entry = query_management.QueryEntry(
                    expire_after=UserMessageRouting.
                    REFRESH_FORWARDING_LIST_INTERVAL,
                    callback_expire=self._remove_user_from_forwarding,
                    data={
                        KeyType.user_id: user_id,
                    },
                    retry_count=0)
                self.forwarding_entries[user_id]['refresh'] = query_entry
            else:
                self.forwarding_entries[user_id]['refresh'].update(
                    fire_after=UserMessageRouting.
                    REFRESH_FORWARDING_LIST_INTERVAL)
        self.forwarding_entries[user_id].setdefault('nodes', set())
        self.forwarding_entries[user_id]['nodes'].add(node_id)
        self.stats.update_stats("user_message",
                                "registered_users_in_forwarding_list",
                                len(self.forwarding_entries))

    def _remove_user_from_forwarding(self,
                                     query_entry=None,
                                     user_id=None,
                                     node_id=None):
        """Unregister user to forwarding list"""
        if query_entry is not None:
            user_id = query_entry.data[KeyType.user_id]
            self.forwarding_entries.pop(user_id, None)
            return
        if user_id not in self.forwarding_entries:
            return
        self.forwarding_entries[user_id]['nodes'].remove(node_id)
        if len(self.forwarding_entries[user_id]['nodes']) == 0:
            if 'refresh' in self.forwarding_entries[user_id]:
                self.forwarding_entries[user_id]['refresh'].deactivate()
            self.forwarding_entries.pop(user_id, None)
        self.stats.update_stats("user_message",
                                "registered_users_in_forwarding_list",
                                len(self.forwarding_entries))

    def send_message_to_user(self, msg, direct_only=False):
        """Forward message to connecting user

        Args:
            msg (dict): message to send
            direct_only (bool): If True, _forward_message_to_another_node is not called.
        """
        if KeyType.destination_user_id not in msg:
            return True

        msg[KeyType.infra_msg_type] = InfraMessageCategory.CATEGORY_USER
        if msg.get(KeyType.is_anycast, False):
            return self._send_anycast_message(msg)

        socks = self.registered_users.get(msg[KeyType.destination_user_id],
                                          None)
        if socks is None:
            if direct_only:
                return False
            self._forward_message_to_another_node(msg)
            return True
        count = len(socks)
        for s in socks:
            if not self._send(s, msg):
                count -= 1
        return count > 0

    def _send(self, sock, msg):
        """Raw function to send a message"""
        try:
            if sock in self.aes_name_list:
                direct_send_to_user(sock, msg, name=self.aes_name_list[sock])
            else:
                direct_send_to_user(sock, msg)
            self.stats.update_stats_increment("user_message",
                                              "sent_msg_to_user", 1)
        except:
            return False
        return True

    def _send_anycast_message(self, msg):
        """Send message as anycast"""
        dst_user_id = msg[KeyType.destination_user_id]
        if dst_user_id not in self.forwarding_entries:
            return False
        ttl = msg.get(KeyType.anycast_ttl, 0)
        if ttl == 0:
            return False
        randmax = len(self.forwarding_entries[dst_user_id]['nodes'])
        if dst_user_id in self.registered_users:
            randmax += 1
        while ttl > 0:
            idx = random.randrange(randmax)
            msg[KeyType.anycast_ttl] = ttl - 1
            ttl -= 1
            if idx == randmax - 1:
                if len(self.registered_users) > 0:
                    sock = random.choice(
                        tuple(self.registered_users.get(dst_user_id, None)))
                    if sock is not None and self._send(sock, msg):
                        return True
            else:
                try:
                    msg[KeyType.destination_node_id] = random.choice(
                        tuple(self.forwarding_entries[dst_user_id]['nodes']))
                    self.networking.send_message_in_network(
                        nodeinfo=None,
                        payload_type=PayloadType.Type_any,
                        domain_id=self.domain_id,
                        msg=msg)
                except:
                    import traceback
                    traceback.print_exc()
                    continue
                return True
        return False

    def _forward_message_to_another_node(self, msg):
        """Try to forward message to another node"""
        dst_user_id = msg[KeyType.destination_user_id]
        if dst_user_id in self.forwarding_entries:
            for dst_node_id in self.forwarding_entries[dst_user_id]['nodes']:
                msg[KeyType.destination_node_id] = dst_node_id
                try:
                    self.networking.send_message_in_network(
                        nodeinfo=None,
                        payload_type=PayloadType.Type_any,
                        domain_id=self.domain_id,
                        msg=msg)
                except:
                    import traceback
                    traceback.print_exc()
                    pass
            return
        src_user_id = msg[KeyType.source_user_id]
        self._resolve_accommodating_core_node(dst_user_id, src_user_id, msg)

    def _resolve_accommodating_core_node(self,
                                         dst_user_id,
                                         src_user_id,
                                         orig_msg=None):
        """Resolve which node the user connects to

        Find the node that accommodates the user_id first, and then, send the message to the node.

        Args:
            dst_user_id (bytes): destination user_id
            src_user_id (bytes): source user_id
            orig_msg (dict): message to send
        """
        if orig_msg is not None:
            query_entry = query_management.QueryEntry(
                expire_after=UserMessageRouting.RESOLVE_TIMEOUT,
                callback_expire=self._resolve_failure,
                callback=self._resolve_success,
                data={
                    KeyType.message: orig_msg,
                },
                retry_count=0)
            self.on_going_timers.add(query_entry.nonce)
        msg = {
            KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_USER,
            KeyType.domain_id: self.domain_id,
            KeyType.infra_command: UserMessageRouting.RESOLVE_USER_LOCATION,
            KeyType.destination_user_id: dst_user_id,
        }
        if orig_msg is not None:
            msg[KeyType.nonce] = query_entry.nonce
        if src_user_id is not None:
            msg[KeyType.source_user_id] = src_user_id
        self.networking.broadcast_message_in_network(domain_id=self.domain_id,
                                                     msg=msg)

    def _resolve_success(self, query_entry):
        """Callback for successful of resolving the location"""
        self.on_going_timers.remove(query_entry.nonce)
        msg = query_entry.data[KeyType.message]
        self._forward_message_to_another_node(msg=msg)

    def _resolve_failure(self, query_entry):
        """Callback for failure of resolving the location"""
        self.on_going_timers.remove(query_entry.nonce)
        msg = query_entry.data[KeyType.message]
        msg[KeyType.destination_user_id] = msg[KeyType.source_user_id]
        msg[KeyType.result] = False
        msg[KeyType.reason] = "Cannot find such user"
        self.send_message_to_user(msg)

    def send_multicast_join(self, user_id, permanent=False):
        """Broadcast JOIN_MULTICAST_RECEIVER"""
        msg = {
            KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_USER,
            KeyType.domain_id: self.domain_id,
            KeyType.infra_command: UserMessageRouting.JOIN_MULTICAST_RECEIVER,
            KeyType.user_id: user_id,
            KeyType.static_entry: permanent,
        }
        self.stats.update_stats_increment("multicast", "join", 1)
        self.networking.broadcast_message_in_network(domain_id=self.domain_id,
                                                     msg=msg)

    def send_multicast_leave(self, user_id):
        """Broadcast LEAVE_MULTICAST_RECEIVER"""
        msg = {
            KeyType.domain_id: self.domain_id,
            KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_USER,
            KeyType.infra_command: UserMessageRouting.LEAVE_MULTICAST_RECEIVER,
            KeyType.user_id: user_id,
        }
        self.stats.update_stats_increment("multicast", "leave", 1)
        self.networking.broadcast_message_in_network(domain_id=self.domain_id,
                                                     msg=msg)

    def _distribute_cross_refs_to_clients(self):
        """Distribute cross ref assined by the domain0_manager to client"""
        if len(self.registered_users) == 0:
            return
        try:
            for i in range(len(self.cross_ref_list)):
                msg = {
                    KeyType.domain_id:
                    self.domain_id,
                    KeyType.command:
                    bbclib.MsgType.NOTIFY_CROSS_REF,
                    KeyType.destination_user_id:
                    random.choice(tuple(self.registered_users.keys())),
                    KeyType.cross_ref:
                    self.cross_ref_list.pop(0),
                }
                self.send_message_to_user(msg)
        except:
            import traceback
            traceback.print_exc()
            return

    def process_message(self, msg):
        """Process received message

        Args:
            msg (dict): received message
        """
        if KeyType.infra_command in msg:
            if msg[KeyType.
                   infra_command] == UserMessageRouting.RESOLVE_USER_LOCATION:
                self.stats.update_stats_increment("user_message",
                                                  "RESOLVE_USER_LOCATION", 1)
                user_id = msg[KeyType.destination_user_id]
                if user_id not in self.registered_users:
                    return
                self._add_user_for_forwarding(msg[KeyType.source_user_id],
                                              msg[KeyType.source_node_id])
                msg[KeyType.destination_node_id] = msg[KeyType.source_node_id]
                if KeyType.source_user_id in msg:
                    msg[KeyType.destination_user_id] = msg[
                        KeyType.source_user_id]
                msg[KeyType.source_user_id] = user_id
                msg[KeyType.
                    infra_command] = UserMessageRouting.RESPONSE_USER_LOCATION
                self.networking.send_message_in_network(
                    nodeinfo=None,
                    payload_type=PayloadType.Type_any,
                    domain_id=self.domain_id,
                    msg=msg)

            elif msg[
                    KeyType.
                    infra_command] == UserMessageRouting.RESPONSE_USER_LOCATION:
                self.stats.update_stats_increment("user_message",
                                                  "RESPONSE_USER_LOCATION", 1)
                self._add_user_for_forwarding(msg[KeyType.source_user_id],
                                              msg[KeyType.source_node_id])
                if KeyType.nonce in msg:
                    query_entry = ticker.get_entry(msg[KeyType.nonce])
                    if query_entry is not None and query_entry.active:
                        query_entry.callback()

            elif msg[
                    KeyType.
                    infra_command] == UserMessageRouting.RESPONSE_NO_SUCH_USER:
                self.stats.update_stats_increment("user_message",
                                                  "RESPONSE_NO_SUCH_USER", 1)
                self._remove_user_from_forwarding(
                    user_id=msg[KeyType.user_id],
                    node_id=msg[KeyType.source_node_id])

            elif msg[
                    KeyType.
                    infra_command] == UserMessageRouting.JOIN_MULTICAST_RECEIVER:
                self.stats.update_stats_increment("user_message",
                                                  "JOIN_MULTICAST_RECEIVER", 1)
                self._add_user_for_forwarding(msg[KeyType.user_id],
                                              msg[KeyType.source_node_id],
                                              permanent=msg.get(
                                                  KeyType.static_entry, False))

            elif msg[
                    KeyType.
                    infra_command] == UserMessageRouting.LEAVE_MULTICAST_RECEIVER:
                self.stats.update_stats_increment("user_message",
                                                  "LEAVE_MULTICAST_RECEIVER",
                                                  1)
                if msg[KeyType.user_id] in self.forwarding_entries:
                    self._remove_user_from_forwarding(
                        user_id=msg[KeyType.user_id],
                        node_id=msg[KeyType.source_node_id])

            elif msg[KeyType.
                     infra_command] == UserMessageRouting.CROSS_REF_ASSIGNMENT:
                self.stats.update_stats_increment("user_message",
                                                  "CROSS_REF_ASSIGNMENT", 1)
                if KeyType.cross_ref in msg:
                    self.cross_ref_list.append(msg[KeyType.cross_ref])
                    if len(self.cross_ref_list
                           ) > UserMessageRouting.MAX_CROSS_REF_STOCK:
                        self._distribute_cross_refs_to_clients()

            return

        src_user_id = msg[KeyType.source_user_id]
        if src_user_id in self.forwarding_entries:
            self.forwarding_entries[src_user_id]['refresh'].update(
                fire_after=UserMessageRouting.REFRESH_FORWARDING_LIST_INTERVAL)
        dst_user_id = msg[KeyType.destination_user_id]
        if dst_user_id not in self.registered_users:
            if msg.get(KeyType.is_anycast, False):
                self._send_anycast_message(msg)
                return
            retmsg = {
                KeyType.domain_id: self.domain_id,
                KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_USER,
                KeyType.destination_node_id: msg[KeyType.source_node_id],
                KeyType.infra_command:
                UserMessageRouting.RESPONSE_NO_SUCH_USER,
                KeyType.user_id: dst_user_id,
            }
            self.stats.update_stats_increment("user_message",
                                              "fail_to_find_user", 1)
            self.networking.send_message_in_network(
                nodeinfo=None,
                payload_type=PayloadType.Type_any,
                domain_id=self.domain_id,
                msg=retmsg)
            return
        if KeyType.is_anycast in msg:
            del msg[KeyType.is_anycast]
        self.stats.update_stats_increment("user_message", "send_to_user", 1)
        self.send_message_to_user(msg)
Example #4
0
class TopologyManagerBase:
    """Network topology management for a domain

    This class defines how to create topology, meaning that who should be neighbors and provides very simple topology
    management, that is full mesh topology. If P2P routing algorithm is needed, you should override this class
    to upgrade functions.
    This class does not manage the neighbor list itself (It's in BBcNetwork)
    """

    NOTIFY_NEIGHBOR_LIST = to_2byte(0)
    NEIGHBOR_LIST_REFRESH_INTERVAL = 300

    def __init__(self,
                 network=None,
                 config=None,
                 domain_id=None,
                 node_id=None,
                 loglevel="all",
                 logname=None):
        self.network = network
        self.stats = network.core.stats
        self.neighbors = network.domains[domain_id]['neighbor']
        self.config = config
        self.domain_id = domain_id
        self.logger = logger.get_logger(
            key="topology_manager:%s" %
            binascii.b2a_hex(domain_id[:4]).decode(),
            level=loglevel,
            logname=logname)
        self.my_node_id = node_id
        self.advertise_wait_entry = None
        self.neighbor_refresh_timer_entry = None
        self.update_refresh_timer_entry()

    def stop_all_timers(self):
        """Invalidate all running timers"""
        if self.advertise_wait_entry is not None:
            self.advertise_wait_entry.deactivate()
        if self.neighbor_refresh_timer_entry is not None:
            self.neighbor_refresh_timer_entry.deactivate()

    def notify_neighbor_update(self, node_id, is_new=True):
        """Update expiration timer for the notified node_id

        Args:
            node_id (bytes): target node_id
            is_new (bool): If True, this node is a new comer node
        """
        if node_id is not None:
            self.logger.debug(
                "[%s] notify_neighbor_update: node_id=%s, is_new=%s" %
                (self.my_node_id.hex()[:4], node_id.hex()[:4], is_new))
        else:
            self.logger.debug("[%s] notify_neighbor_update" %
                              self.my_node_id.hex()[:4])

        rand_time = random.uniform(
            0.5, 1) * 5 / (len(self.neighbors.nodeinfo_list) + 1)
        if self.advertise_wait_entry is None:
            self.advertise_wait_entry = query_management.QueryEntry(
                expire_after=rand_time,
                callback_expire=self._advertise_neighbor_info,
                retry_count=0)
        else:
            self.advertise_wait_entry.update_expiration_time(rand_time)

    def update_refresh_timer_entry(self,
                                   new_entry=True,
                                   force_refresh_time=None):
        """Update expiration timer"""
        if force_refresh_time is None:
            rand_interval = random.randint(
                int(TopologyManagerBase.NEIGHBOR_LIST_REFRESH_INTERVAL * 2 /
                    3),
                int(TopologyManagerBase.NEIGHBOR_LIST_REFRESH_INTERVAL * 4 /
                    3))
        else:
            rand_interval = force_refresh_time
        self.logger.debug("update_refresh_timer_entry: %d" % rand_interval)
        if new_entry:
            self.neighbor_refresh_timer_entry = query_management.QueryEntry(
                expire_after=rand_interval,
                data={"is_refresh": True},
                callback_expire=self._advertise_neighbor_info,
                retry_count=0)
        else:
            self.neighbor_refresh_timer_entry.update_expiration_time(
                rand_interval)

    def _advertise_neighbor_info(self, query_entry):
        """Broadcast nodeinfo list"""
        #print("[%s]: _advertise_neighbor_info" % self.my_node_id.hex()[:4])
        self.advertise_wait_entry = None
        msg = {
            KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_TOPOLOGY,
            KeyType.domain_id: self.domain_id,
            KeyType.command: TopologyManagerBase.NOTIFY_NEIGHBOR_LIST,
        }
        admin_info = {
            KeyType.neighbor_list: self.make_neighbor_list(),
        }
        self.network.include_admin_info_into_message_if_needed(
            self.domain_id, msg, admin_info)
        self.network.broadcast_message_in_network(
            domain_id=self.domain_id,
            payload_type=PayloadType.Type_msgpack,
            msg=msg)
        if "is_refresh" in query_entry.data:
            self.update_refresh_timer_entry()

    def make_neighbor_list(self):
        """make nodelist binary for advertising"""
        nodeinfo = bytearray()

        # the node itself
        for item in self.neighbors.my_info.get_nodeinfo():
            nodeinfo.extend(item)
        count = 1

        # neighboring node
        for nd in self.neighbors.nodeinfo_list.keys():
            if self.neighbors.nodeinfo_list[nd].is_alive:
                count += 1
                for item in self.neighbors.nodeinfo_list[nd].get_nodeinfo():
                    nodeinfo.extend(item)

        nodes = bytearray(count.to_bytes(4, 'big'))
        nodes.extend(nodeinfo)
        return bytes(nodes)

    def _update_neighbor_list(self, binary_data):
        """Parse binary data and update neighbors

        Args:
            binary_data (bytes): received data
        Returns:
            bool: True if the received nodeinfo has changed
        """
        count_originally = len(
            list(
                filter(lambda nd: nd.is_alive,
                       self.neighbors.nodeinfo_list.values())))
        count_unchanged = 0
        count = int.from_bytes(binary_data[:4], 'big')
        for i in range(count):
            base = 4 + i * (32 + 4 + 16 + 2 + 1 + 8)
            node_id = binary_data[base:base + 32]
            if node_id == self.my_node_id:
                continue
            ipv4 = socket.inet_ntop(socket.AF_INET,
                                    binary_data[base + 32:base + 36])
            ipv6 = socket.inet_ntop(socket.AF_INET6,
                                    binary_data[base + 36:base + 52])
            port = socket.ntohs(
                int.from_bytes(binary_data[base + 52:base + 54], 'big'))
            domain0 = True if binary_data[base + 54] == 0x01 else False
            updated_at = int.from_bytes(binary_data[base + 55:base + 63],
                                        'big')
            if not self.neighbors.add(node_id=node_id,
                                      ipv4=ipv4,
                                      ipv6=ipv6,
                                      port=port,
                                      domain0=domain0):
                count_unchanged += 1
        self.logger.debug(
            "[%s] update_neighbor_list: orig=%d, unchanged=%d, recv=%d, need_advertise=%s"
            % (self.my_node_id.hex()[:4], count_originally, count_unchanged,
               count, count_originally != count_unchanged))
        if count_originally == count_unchanged:
            return False
        else:
            return True

    def process_message(self, msg):
        """Process received message

        Args:
            msg (dict): received message
        """
        if KeyType.destination_node_id not in msg or KeyType.command not in msg:
            return
        if "keypair" in self.network.domains[
                self.domain_id] and self.network.domains[
                    self.domain_id]["keypair"] is not None:
            if not self.network.check_admin_signature(self.domain_id, msg):
                self.logger.error("Illegal access to domain %s" %
                                  self.domain_id.hex())
                return

        if msg[KeyType.command] == TopologyManagerBase.NOTIFY_NEIGHBOR_LIST:
            self.stats.update_stats_increment("topology_manager",
                                              "NOTIFY_NEIGHBOR_LIST", 1)
            self.update_refresh_timer_entry(new_entry=False)
            diff_flag = self._update_neighbor_list(msg[KeyType.neighbor_list])
            if diff_flag:
                if self.advertise_wait_entry is None:
                    self.notify_neighbor_update(None)
            else:
                if self.advertise_wait_entry is not None:
                    self.advertise_wait_entry.deactivate()
                    self.advertise_wait_entry = None
Example #5
0
class DataHandler:
    """DB and storage handler"""
    REPLICATION_ALL = 0
    REPLICATION_P2P = 1
    REPLICATION_EXT = 2
    REQUEST_REPLICATION_INSERT = to_2byte(0)
    RESPONSE_REPLICATION_INSERT = to_2byte(1)
    REQUEST_SEARCH = to_2byte(2)
    RESPONSE_SEARCH = to_2byte(3)
    NOTIFY_INSERTED = to_2byte(4)
    REPAIR_TRANSACTION_DATA = to_2byte(5)
    REPLICATION_CROSS_REF = to_2byte(6)

    def __init__(self,
                 networking=None,
                 config=None,
                 workingdir=None,
                 domain_id=None,
                 loglevel="all",
                 logname=None):
        self.networking = networking
        self.core = networking.core
        self.stats = networking.core.stats
        self.logger = logger.get_logger(key="data_handler",
                                        level=loglevel,
                                        logname=logname)
        self.domain_id = domain_id
        self.domain_id_str = bbclib.convert_id_to_string(domain_id)
        self.config = config
        self.working_dir = workingdir
        self.storage_root = os.path.join(self.working_dir, self.domain_id_str)
        if not os.path.exists(self.storage_root):
            os.makedirs(self.storage_root, exist_ok=True)
        self.use_external_storage = self._storage_setup()
        self.replication_strategy = DataHandler.REPLICATION_ALL
        self.upgraded_from = DB_VERSION
        self.db_adaptors = list()
        self._db_setup()

    def _db_setup(self):
        """Setup DB"""
        dbconf = self.config['db']
        if dbconf['replication_strategy'] == 'all':
            self.replication_strategy = DataHandler.REPLICATION_ALL
        elif dbconf['replication_strategy'] == 'p2p':
            self.replication_strategy = DataHandler.REPLICATION_P2P
        else:
            self.replication_strategy = DataHandler.REPLICATION_EXT
        db_type = dbconf.get("db_type", "sqlite")
        db_name = dbconf.get("db_name", "bbc1_db.sqlite")
        if db_type == "sqlite":
            self.db_adaptors.append(
                SqliteAdaptor(self,
                              db_name=os.path.join(self.storage_root,
                                                   db_name)))
        elif db_type == "mysql":
            count = 0
            for c in dbconf['db_servers']:
                db_addr = c.get("db_addr", "127.0.0.1")
                db_port = c.get("db_port", 3306)
                db_user = c.get("db_user", "user")
                db_pass = c.get("db_pass", "password")
                self.db_adaptors.append(
                    MysqlAdaptor(self,
                                 db_name=db_name,
                                 db_num=count,
                                 server_info=(db_addr, db_port, db_user,
                                              db_pass)))
                count += 1

        for db in self.db_adaptors:
            db.open_db()
            flag_created = db.create_table('transaction_table',
                                           transaction_tbl_definition,
                                           primary_key=0,
                                           indices=[0])
            db.create_table('asset_info_table',
                            asset_info_definition,
                            primary_key=0,
                            indices=[0, 1, 2, 3, 4, 5])
            db.create_table('topology_table',
                            topology_info_definition,
                            primary_key=0,
                            indices=[0, 1, 2])
            db.create_table('cross_ref_table',
                            cross_ref_tbl_definition,
                            primary_key=0,
                            indices=[1])
            db.create_table('merkle_branch_table',
                            merkle_branch_db_definition,
                            primary_key=0,
                            indices=[1, 2])
            db.create_table('merkle_leaf_table',
                            merkle_leaf_db_definition,
                            primary_key=0,
                            indices=[1, 2])
            db.create_table('merkle_root_table',
                            merkle_root_db_definition,
                            primary_key=0,
                            indices=[0])
            ver = db.get_version()
            if ver != DB_VERSION:
                if not flag_created:
                    self.logger.fatal(
                        "*** DB meta table is upgraded. Run db_migration_tool.py"
                    )
                    self.upgraded_from = ver
                db.update_table_def(ver)

    def _storage_setup(self):
        """Setup storage"""
        if self.config['storage']['type'] == "external":
            return True
        if 'root' in self.config['storage'] and self.config['storage'][
                'root'].startswith("/"):
            self.storage_root = os.path.join(self.config['storage']['root'],
                                             self.domain_id_str)
        else:
            self.storage_root = os.path.join(self.working_dir,
                                             self.domain_id_str)
        os.makedirs(self.storage_root, exist_ok=True)
        return False

    def exec_sql(self,
                 db_num=0,
                 sql=None,
                 args=(),
                 commit=False,
                 fetch_one=False,
                 return_cursor=False):
        """Execute sql sentence

        Args:
            db_num (int): index of DB if multiple DBs are used
            sql (str): SQL string
            args (list): Args for the SQL
            commit (bool): If True, commit is performed
            fetch_one (bool): If True, fetch just one record
            return_cursor (bool): If True (and fetch_one is False), return db_cur (iterator)
        Returns:
            list: list of records
        """
        self.stats.update_stats_increment("data_handler", "exec_sql", 1)
        #print("sql=", sql)
        #if len(args) > 0:
        #    print("args=", args)
        try:
            db_num = 0 if db_num >= len(self.db_adaptors) else db_num
            if len(args) > 0:
                self.db_adaptors[db_num].db_cur.execute(sql, args)
            else:
                self.db_adaptors[db_num].db_cur.execute(sql)
            self.db_adaptors[db_num].db.commit(
            )  # commit is mandatory (even if read access) in that case that multiple client connect to a single mysql server
            if commit:
                ret = None
            else:
                if fetch_one:
                    ret = self.db_adaptors[db_num].db_cur.fetchone()
                    self.db_adaptors[db_num].db.commit()
                else:
                    if return_cursor:
                        return self.db_adaptors[db_num].db_cur
                    ret = self.db_adaptors[db_num].db_cur.fetchall()
        except:
            if commit:
                self.db_adaptors[db_num].db.rollback()
            self.logger.error(traceback.format_exc())
            traceback.print_exc()
            self.stats.update_stats_increment("data_handler", "fail_exec_sql",
                                              1)
            if self.db_adaptors[db_num] is not None and self.db_adaptors[
                    db_num].db_cur is not None:
                self.db_adaptors[db_num].db_cur.close()
            if self.db_adaptors[db_num] is not None and self.db_adaptors[
                    db_num].db is not None:
                self.db_adaptors[db_num].db.close()
            self.db_adaptors[db_num].open_db()
            return None

        if ret is None:
            return []
        else:
            return list(ret)

    def get_asset_info(self, txobj):
        """Retrieve asset information from transaction object

        Args:
            txobj (BBcTransaction): transaction object to analyze
        Returns:
            list: list of list [asset_group_id, asset_id, user_id, file_size, file_digest]
        """
        info = list()
        for idx, evt in enumerate(txobj.events):
            ast = evt.asset
            if ast is not None:
                info.append((evt.asset_group_id, ast.asset_id, ast.user_id,
                             ast.asset_file_size > 0, ast.asset_file_digest))
        for idx, rtn in enumerate(txobj.relations):
            ast = rtn.asset
            if rtn.asset is not None:
                info.append((rtn.asset_group_id, ast.asset_id, ast.user_id,
                             ast.asset_file_size > 0, ast.asset_file_digest))
        return info

    def _get_topology_info(self, txobj):
        """Retrieve topology information from transaction object

        This method returns (from, to) list that describe the topology of transactions

        Args:
            txobj (BBcTransaction): transaction object to analyze
        Returns:
            list: list of tuple (base transaction_id, pointing transaction_id)
        """
        info = list()
        for reference in txobj.references:
            info.append((txobj.transaction_id,
                         reference.transaction_id))  # (base, point_to)
        for idx, rtn in enumerate(txobj.relations):
            for pt in rtn.pointers:
                info.append((txobj.transaction_id,
                             pt.transaction_id))  # (base, point_to)
        return info

    def insert_transaction(self,
                           txdata,
                           txobj=None,
                           fmt_type=bbclib_config.DEFAULT_BBC_FORMAT,
                           asset_files=None,
                           no_replication=False):
        """Insert transaction data and its asset files

        Either txdata or txobj must be given to insert the transaction.

        Args:
            txdata (bytes): serialized transaction data
            txobj (BBcTransaction): transaction object to insert
            fmt_type (int): 2-byte value of BBcFormat type
            asset_files (dict): asset files in the transaction
        Returns:
            set: set of asset_group_ids in the transaction
        """
        self.stats.update_stats_increment("data_handler", "insert_transaction",
                                          1)
        if txobj is None:
            txobj, fmt_type = self.core.validate_transaction(
                txdata, asset_files=asset_files)
            if txobj is None:
                return None

        inserted_count = 0
        for i in range(len(self.db_adaptors)):
            if self._insert_transaction_into_a_db(i, txobj, fmt_type):
                inserted_count += 1
        if inserted_count == 0:
            return None

        asset_group_ids = self._store_asset_files(txobj, asset_files)

        if not no_replication and self.replication_strategy != DataHandler.REPLICATION_EXT:
            self._send_replication_to_other_cores(txdata, asset_files)

        if self.networking.domain0manager is not None:
            self.networking.domain0manager.distribute_cross_ref_in_domain0(
                domain_id=self.domain_id, transaction_id=txobj.transaction_id)
            if txobj.cross_ref is not None:
                self.networking.domain0manager.cross_ref_registered(
                    domain_id=self.domain_id,
                    transaction_id=txobj.transaction_id,
                    cross_ref=(txobj.cross_ref.domain_id,
                               txobj.cross_ref.transaction_id))

        return asset_group_ids

    def _insert_transaction_into_a_db(self,
                                      db_num,
                                      txobj,
                                      fmt_type=bbclib_config.DEFAULT_BBC_FORMAT
                                      ):
        """Insert transaction data into the transaction table of the specified DB

        Args:
            db_num (int): index of DB if multiple DBs are used
            txobj (BBcTransaction): transaction object to insert
            fmt_type (int): 2-byte value of BBcFormat type
        Returns:
            bool: True if successful
        """
        #print("_insert_transaction_into_a_db: for txid =", txobj.transaction_id.hex())
        txdata = bbclib.serialize(txobj, format_type=fmt_type)
        ret = self.exec_sql(
            db_num=db_num,
            sql="INSERT INTO transaction_table VALUES (%s,%s)" %
            (self.db_adaptors[0].placeholder, self.db_adaptors[0].placeholder),
            args=(txobj.transaction_id, txdata),
            commit=True)
        if ret is None:
            return False

        ts = txobj.timestamp
        for asset_group_id, asset_id, user_id, fileflag, filedigest in self.get_asset_info(
                txobj):
            self.exec_sql(
                db_num=db_num,
                sql=
                "INSERT INTO asset_info_table(transaction_id, asset_group_id, asset_id, user_id, timestamp) "
                "VALUES (%s, %s, %s, %s, %s)" %
                (self.db_adaptors[0].placeholder,
                 self.db_adaptors[0].placeholder,
                 self.db_adaptors[0].placeholder,
                 self.db_adaptors[0].placeholder,
                 self.db_adaptors[0].placeholder),
                args=(txobj.transaction_id, asset_group_id, asset_id, user_id,
                      ts),
                commit=True)
        for base, point_to in self._get_topology_info(txobj):
            self.exec_sql(
                db_num=db_num,
                sql="INSERT INTO topology_table(base, point_to) VALUES (%s, %s)"
                % (self.db_adaptors[0].placeholder,
                   self.db_adaptors[0].placeholder),
                args=(base, point_to),
                commit=True)
            #print("topology: base:%s, point_to:%s" % (base.hex(), point_to.hex()))
        return True

    def insert_cross_ref(self,
                         transaction_id,
                         outer_domain_id,
                         txid_having_cross_ref,
                         no_replication=False):
        """Insert cross_ref information into cross_ref_table

        Args:
            transaction_id (bytes): target transaction_id
            outer_domain_id (bytes): domain_id that holds cross_ref about the transaction_id
            txid_having_cross_ref (bytes): transaction_id in the outer_domain that includes the cross_ref
            no_replication (bool): If False, the replication is sent to other nodes in the domain
        """
        self.stats.update_stats_increment("data_handler", "insert_cross_ref",
                                          1)
        sql = "INSERT INTO cross_ref_table (transaction_id, outer_domain_id, txid_having_cross_ref) " + \
              "VALUES (%s, %s, %s)" % (self.db_adaptors[0].placeholder, self.db_adaptors[0].placeholder,
                                       self.db_adaptors[0].placeholder)
        for i in range(len(self.db_adaptors)):
            self.exec_sql(db_num=i,
                          sql=sql,
                          args=(transaction_id, outer_domain_id,
                                txid_having_cross_ref),
                          commit=True)

        if not no_replication:
            self._send_cross_ref_replication_to_other_cores(
                transaction_id, outer_domain_id, txid_having_cross_ref)

    def count_domain_in_cross_ref(self, outer_domain_id):
        """Count the number of domains in the cross_ref table"""
        # TODO: need to consider registered_time
        sql = "SELECT count(*) FROM cross_ref_table WHERE outer_domain = %s" % self.db_adaptors[
            0].placeholder
        ret = self.exec_sql(sql=sql, args=(outer_domain_id, ))
        return ret

    def search_domain_having_cross_ref(self, transaction_id=None):
        """Search domain_id that holds cross_ref about the specified transaction_id

        Args:
            transaction_id (bytes): target transaction_id
        Returns:
            list: records of cross_ref_tables ["id","transaction_id", "outer_domain_id", "txid_having_cross_ref"]
        """
        if transaction_id is not None:
            sql = "SELECT * FROM cross_ref_table WHERE transaction_id = %s" % self.db_adaptors[
                0].placeholder
            return self.exec_sql(sql=sql, args=(transaction_id, ))
        else:
            return self.exec_sql(sql="SELECT * FROM cross_ref_table")

    def _store_asset_files(self, txobj, asset_files):
        """Store all asset_files related to the transaction_object

        Args:
            txobj (BBcTransaction): transaction object to insert
            asset_files (dict): dictionary of {asset_id: content} for the transaction
        Returns:
            set: set of asset_group_ids in the transaction
        """
        #print("_store_asset_files: for txid =", txobj.transaction_id.hex())
        asset_group_ids = set()
        for asset_group_id, asset_id, user_id, fileflag, filedigest in self.get_asset_info(
                txobj):
            asset_group_ids.add(asset_group_id)
            if not self.use_external_storage and asset_files is not None and asset_id in asset_files:
                self.store_in_storage(asset_group_id, asset_id,
                                      asset_files[asset_id])
        return asset_group_ids

    def restore_transaction_data(self, db_num, transaction_id, txobj):
        """Remove and insert a transaction"""
        if txobj is not None:
            self.remove(transaction_id, txobj=txobj, db_num=db_num)
            self._insert_transaction_into_a_db(db_num=db_num, txobj=txobj)

    def _send_replication_to_other_cores(self, txdata, asset_files=None):
        """Broadcast replication of transaction data"""
        msg = {
            KeyType.domain_id: self.domain_id,
            KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_DATA,
            KeyType.infra_command: DataHandler.REQUEST_REPLICATION_INSERT,
            KeyType.transaction_data: txdata,
        }
        if asset_files is not None:
            msg[KeyType.all_asset_files] = asset_files
        if self.replication_strategy == DataHandler.REPLICATION_ALL:
            self.networking.broadcast_message_in_network(
                domain_id=self.domain_id,
                payload_type=PayloadType.Type_any,
                msg=msg)
        elif self.replication_strategy == DataHandler.REPLICATION_P2P:
            pass  # TODO: implement (destinations determined by TopologyManager)

    def _send_cross_ref_replication_to_other_cores(self, transaction_id,
                                                   outer_domain_id,
                                                   txid_having_cross_ref):
        """Broadcast replication of cross_ref

        Args:
            transaction_id (bytes): target transaction_id
            outer_domain_id (bytes): domain_id that holds cross_ref about the transaction_id
            txid_having_cross_ref (bytes): transaction_id in the outer_domain that includes the cross_ref
        """
        msg = {
            KeyType.domain_id: self.domain_id,
            KeyType.infra_msg_type: InfraMessageCategory.CATEGORY_DATA,
            KeyType.infra_command: DataHandler.REPLICATION_CROSS_REF,
            KeyType.transaction_id: transaction_id,
            KeyType.outer_domain_id: outer_domain_id,
            KeyType.txid_having_cross_ref: txid_having_cross_ref,
        }
        if self.replication_strategy == DataHandler.REPLICATION_ALL:
            self.networking.broadcast_message_in_network(
                domain_id=self.domain_id,
                payload_type=PayloadType.Type_any,
                msg=msg)
        elif self.replication_strategy == DataHandler.REPLICATION_P2P:
            pass  # TODO: implement (destinations determined by TopologyManager)

    def remove(self, transaction_id, txobj=None, db_num=-1):
        """Delete all data regarding the specified transaction_id

        This method requires either transaction_id or txobj.

        Args:
            transaction_id (bytes): target transaction_id
            txobj (BBcTransaction): transaction object to remove
            db_num (int): index of DB if multiple DBs are used
        """
        if transaction_id is None:
            return
        if txobj is None:
            txdata = self.exec_sql(
                sql="SELECT * FROM transaction_table WHERE transaction_id = %s"
                % self.db_adaptors[0].placeholder,
                args=(transaction_id, ))
            txobj, fmt_type = bbclib.deserialize(txdata[0][1])
        elif txobj.transaction_id != transaction_id:
            return

        if db_num == -1 or db_num >= len(self.db_adaptors):
            for i in range(len(self.db_adaptors)):
                self._remove_transaction(txobj, i)
        else:
            self._remove_transaction(txobj, db_num)

    def _remove_transaction(self, txobj, db_num):
        """Remove transaction from DB"""
        #print("_remove_transaction: for txid =", txobj.transaction_id.hex())
        self.exec_sql(
            db_num=db_num,
            sql="DELETE FROM transaction_table WHERE transaction_id = %s" %
            self.db_adaptors[0].placeholder,
            args=(txobj.transaction_id, ),
            commit=True)
        for base, point_to in self._get_topology_info(txobj):
            self.exec_sql(
                db_num=db_num,
                sql=
                "DELETE FROM topology_table WHERE base = %s AND point_to = %s"
                % (self.db_adaptors[0].placeholder,
                   self.db_adaptors[0].placeholder),
                args=(base, point_to),
                commit=True)

    def _remove_asset_files(self, txobj, asset_files=None):
        """Remove all asset files related to the transaction

        If asset_files is given, only asset files in given param are removed

        Args:
            txobj (BBcTransaction): transaction object that includes the asset to be removed
            asset_files (dict): dictionary of {asset_id: content} for the transaction
        """
        #print("_remove_asset_files: for txid =", txobj.transaction_id.hex())
        if self.use_external_storage:
            return
        for asset_group_id, asset_id, user_id, fileflag, filedigest in self.get_asset_info(
                txobj):
            if asset_files is not None:
                if asset_id in asset_files:
                    self._remove_in_storage(asset_group_id, asset_id)
            else:
                self._remove_in_storage(asset_group_id, asset_id)

    def search_transaction(self,
                           transaction_id=None,
                           asset_group_id=None,
                           asset_id=None,
                           user_id=None,
                           start_from=None,
                           until=None,
                           direction=0,
                           count=1,
                           db_num=0):
        """Search transaction data

        When Multiple conditions are given, they are considered as AND condition.

        Args:
            transaction_id (bytes): target transaction_id
            asset_group_id (bytes): asset_group_id that target transactions should have
            asset_id (bytes): asset_id that target transactions should have
            user_id (bytes): user_id that target transactions should have
            start_from (int): the starting timestamp to search
            until (int): the end timestamp to search
            direction (int): 0: descend, 1: ascend
            count (int): The maximum number of transactions to retrieve
            db_num (int): index of DB if multiple DBs are used
        Returns:
            dict: mapping from transaction_id to serialized transaction data
            dict: dictionary of {asset_id: content} for the transaction
        """
        if transaction_id is not None:
            txinfo = self.exec_sql(
                db_num=db_num,
                sql="SELECT * FROM transaction_table WHERE transaction_id = %s"
                % self.db_adaptors[0].placeholder,
                args=(transaction_id, ))
            if len(txinfo) == 0:
                return None, None
        else:
            dire = "DESC"
            if direction == 1:
                dire = "ASC"
            sql = "SELECT * from asset_info_table WHERE "
            conditions = list()
            if asset_group_id is not None:
                conditions.append("asset_group_id = %s " %
                                  self.db_adaptors[0].placeholder)
            if asset_id is not None:
                conditions.append("asset_id = %s " %
                                  self.db_adaptors[0].placeholder)
            if user_id is not None:
                conditions.append("user_id = %s " %
                                  self.db_adaptors[0].placeholder)
            if start_from is not None:
                conditions.append("timestamp >= %s " %
                                  self.db_adaptors[0].placeholder)
            if until is not None:
                conditions.append("timestamp <= %s " %
                                  self.db_adaptors[0].placeholder)
            sql += "AND ".join(conditions) + "ORDER BY id %s" % dire
            if count > 0:
                sql += " limit %d" % count
            sql += ";"
            args = list(
                filter(lambda a: a is not None,
                       (asset_group_id, asset_id, user_id, start_from, until)))
            ret = self.exec_sql(db_num=db_num, sql=sql, args=args)
            txinfo = list()
            for record in ret:
                tx = self.exec_sql(
                    db_num=db_num,
                    sql=
                    "SELECT * FROM transaction_table WHERE transaction_id = %s"
                    % self.db_adaptors[0].placeholder,
                    args=(record[1], ))
                if tx is not None and len(tx) == 1:
                    txinfo.append(tx[0])

        result_txobj = dict()
        result_asset_files = dict()
        for txid, txdata in txinfo:
            txobj, fmt_type = bbclib.deserialize(txdata)
            result_txobj[txid] = txobj
            for asset_group_id, asset_id, user_id, fileflag, filedigest in self.get_asset_info(
                    txobj):
                if fileflag:
                    result_asset_files[asset_id] = self.get_in_storage(
                        asset_group_id, asset_id)
        return result_txobj, result_asset_files

    def count_transactions(self,
                           asset_group_id=None,
                           asset_id=None,
                           user_id=None,
                           start_from=None,
                           until=None,
                           db_num=0):
        """Count transactions that matches the given conditions

        When Multiple conditions are given, they are considered as AND condition.

        Args:
            asset_group_id (bytes): asset_group_id that target transactions should have
            asset_id (bytes): asset_id that target transactions should have
            user_id (bytes): user_id that target transactions should have
            start_from (int): the starting timestamp to search
            until (int): the end timestamp to search
            db_num (int): index of DB if multiple DBs are used
        Returns:
            int: the number of transactions
        """
        sql = "SELECT count( DISTINCT transaction_id ) from asset_info_table WHERE "
        conditions = list()
        if asset_group_id is not None:
            conditions.append("asset_group_id = %s " %
                              self.db_adaptors[0].placeholder)
        if asset_id is not None:
            conditions.append("asset_id = %s " %
                              self.db_adaptors[0].placeholder)
        if user_id is not None:
            conditions.append("user_id = %s " %
                              self.db_adaptors[0].placeholder)
        if start_from is not None:
            conditions.append("timestamp >= %s " %
                              self.db_adaptors[0].placeholder)
        if until is not None:
            conditions.append("timestamp <= %s " %
                              self.db_adaptors[0].placeholder)
        sql += "AND ".join(conditions)
        args = list(
            filter(lambda a: a is not None,
                   (asset_group_id, asset_id, user_id, start_from, until)))
        ret = self.exec_sql(db_num=db_num, sql=sql, args=args)
        return ret[0][0]

    def search_transaction_topology(self,
                                    transaction_id,
                                    traverse_to_past=True):
        """Search in topology info

        Args:
            transaction_id (bytes): base transaction_id
            traverse_to_past (bool): True: search backward (to past), False: search forward (to future)
        Returns:
            list: list of records of topology table
        """
        if transaction_id is None:
            return None
        if traverse_to_past:
            return self.exec_sql(
                sql="SELECT * FROM topology_table WHERE base = %s" %
                self.db_adaptors[0].placeholder,
                args=(transaction_id, ))

        else:
            return self.exec_sql(
                sql="SELECT * FROM topology_table WHERE point_to = %s" %
                self.db_adaptors[0].placeholder,
                args=(transaction_id, ))

    def store_in_storage(self,
                         asset_group_id,
                         asset_id,
                         content,
                         do_overwrite=False):
        """Store asset file in local storage

        Args:
            asset_group_id (bytes): asset_group_id of the asset
            asset_id (bytes): asset_id of the asset
            content (bytes): the content of the asset file
            do_overwrite (bool): If True, file is overwritten
        Returns:
            bool: True if successful
        """
        #print("store_in_storage: for asset_id =", asset_id.hex())
        self.stats.update_stats_increment("data_handler", "store_in_storage",
                                          1)
        asset_group_id_str = binascii.b2a_hex(asset_group_id).decode('utf-8')
        storage_path = os.path.join(self.storage_root, asset_group_id_str)
        if not os.path.exists(storage_path):
            os.makedirs(storage_path, exist_ok=True)
        path = os.path.join(storage_path,
                            binascii.b2a_hex(asset_id).decode('utf-8'))
        if not do_overwrite and os.path.exists(path):
            return False
        with open(path, 'wb') as f:
            try:
                f.write(content)
            except:
                return False
        return os.path.exists(path)

    def get_in_storage(self, asset_group_id, asset_id):
        """Get the asset file with the asset_id from local storage

        Args:
            asset_group_id (bytes): asset_group_id of the asset
            asset_id (bytes): asset_id of the asset
        Returns:
            bytes or None: the file content
        """
        asset_group_id_str = binascii.b2a_hex(asset_group_id).decode('utf-8')
        storage_path = os.path.join(self.storage_root, asset_group_id_str)
        if not os.path.exists(storage_path):
            return None
        path = os.path.join(storage_path,
                            binascii.b2a_hex(asset_id).decode('utf-8'))
        if not os.path.exists(path):
            return None
        try:
            with open(path, 'rb') as f:
                content = f.read()
            return content
        except:
            self.logger.error(traceback.format_exc())
            return None

    def _remove_in_storage(self, asset_group_id, asset_id):
        """Delete asset file

        Args:
            asset_group_id (bytes): asset_group_id of the asset
            asset_id (bytes): asset_id of the asset
        """
        #print("_remove_in_storage: for asset_id =", asset_id.hex())
        asset_group_id_str = binascii.b2a_hex(asset_group_id).decode('utf-8')
        storage_path = os.path.join(self.storage_root, asset_group_id_str)
        if not os.path.exists(storage_path):
            return
        path = os.path.join(storage_path,
                            binascii.b2a_hex(asset_id).decode('utf-8'))
        if not os.path.exists(path):
            return
        os.remove(path)

    def process_message(self, msg):
        """Process received message

        Args:
            msg (dict): received message
        """
        if KeyType.infra_command not in msg:
            return

        if msg[KeyType.
               infra_command] == DataHandler.REQUEST_REPLICATION_INSERT:
            self.stats.update_stats_increment("data_handler",
                                              "REQUEST_REPLICATION_INSERT", 1)
            self.insert_transaction(msg[KeyType.transaction_data],
                                    asset_files=msg.get(
                                        KeyType.all_asset_files, None),
                                    no_replication=True)

        elif msg[KeyType.
                 infra_command] == DataHandler.RESPONSE_REPLICATION_INSERT:
            self.stats.update_stats_increment("data_handler",
                                              "RESPONSE_REPLICATION_INSERT", 1)
            pass

        elif msg[KeyType.infra_command] == DataHandler.REQUEST_SEARCH:
            self.stats.update_stats_increment("data_handler", "REQUEST_SEARCH",
                                              1)
            ret = self.search_transaction(msg[KeyType.transaction_id])
            msg[KeyType.infra_command] = DataHandler.RESPONSE_SEARCH
            if ret is None or len(ret) == 0:
                msg[KeyType.result] = False
                msg[KeyType.reason] = "Not found"
            else:
                msg[KeyType.result] = True
                msg[KeyType.transaction_data] = ret[0][1]
            self.networking.send_message_in_network(
                nodeinfo=None,
                payload_type=PayloadType.Type_any,
                domain_id=self.domain_id,
                msg=msg)

        elif msg[KeyType.infra_command] == DataHandler.RESPONSE_SEARCH:
            self.stats.update_stats_increment("data_handler",
                                              "RESPONSE_SEARCH", 1)
            if msg[KeyType.result]:
                self.insert_transaction(msg[KeyType.transaction_data])

        elif msg[KeyType.infra_command] == DataHandler.NOTIFY_INSERTED:
            self.stats.update_stats_increment("data_handler",
                                              "NOTIFY_INSERTED", 1)
            if KeyType.transaction_id not in msg or KeyType.asset_group_ids not in msg:
                return
            transaction_id = msg[KeyType.transaction_id]
            asset_group_ids = msg[KeyType.asset_group_ids]
            self.core.send_inserted_notification(self.domain_id,
                                                 asset_group_ids,
                                                 transaction_id,
                                                 only_registered_user=True)

        elif msg[KeyType.infra_command] == DataHandler.REPAIR_TRANSACTION_DATA:
            self.networking.domains[self.domain_id]['repair'].put_message(msg)

        elif msg[KeyType.infra_command] == DataHandler.REPLICATION_CROSS_REF:
            transaction_id = msg[KeyType.transaction_id]
            outer_domain_id = msg[KeyType.outer_domain_id]
            txid_having_cross_ref = msg[KeyType.txid_having_cross_ref]
            self.insert_cross_ref(transaction_id,
                                  outer_domain_id,
                                  txid_having_cross_ref,
                                  no_replication=True)