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)
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
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)
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
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)