class TxnVersionController(TVController): def __init__(self) -> None: self._versions = SortedDict() self._f = 0 self._votes_for_new_version = SortedDict() @property def version(self): return self._versions.peekitem(-1)[1] if self._versions else None def get_pool_version(self, timestamp): if timestamp is None: return self.version last_version = None for upgrade_tm, version in self._versions.items(): if timestamp < upgrade_tm: return last_version last_version = version return last_version def update_version(self, txn): if get_type(txn) == POOL_UPGRADE and get_payload_data(txn).get(ACTION) == START: N = len(get_payload_data(txn).get(SCHEDULE, {})) self._f = (N - 1) // 3 elif get_type(txn) == NODE_UPGRADE and get_payload_data(txn)[DATA][ACTION] == COMPLETE: version = get_payload_data(txn)[DATA][VERSION] self._votes_for_new_version.setdefault(version, set()) self._votes_for_new_version[version].add(get_from(txn)) if len(self._votes_for_new_version[version]) > self._f: self._versions[get_txn_time(txn)] = version self._votes_for_new_version = SortedDict({v: senders for v, senders in self._votes_for_new_version.items() if v > version})
class NodeRegHandler(BatchRequestHandler, WriteRequestHandler): def __init__(self, database_manager: DatabaseManager): BatchRequestHandler.__init__(self, database_manager, POOL_LEDGER_ID) WriteRequestHandler.__init__(self, database_manager, NODE, POOL_LEDGER_ID) self.uncommitted_node_reg = [] self.committed_node_reg = [] # committed node reg at the beginning of view # matches the committed node reg BEFORE the first txn in a view is applied (that is according to the last txn in the last view) self.committed_node_reg_at_beginning_of_view = SortedDict() # uncommitted node reg at the beginning of view # matches the uncommittednode reg BEFORE the first txn in a view is applied (that is according to the last txn in the last view) self.uncommitted_node_reg_at_beginning_of_view = SortedDict() self._uncommitted = deque() # type: deque[UncommittedNodeReg] self._uncommitted_view_no = 0 self._committed_view_no = 0 self.internal_bus = None # type: InternalBus def set_internal_bus(self, internal_bus: InternalBus): self.internal_bus = internal_bus @property def active_node_reg(self): if not self.uncommitted_node_reg_at_beginning_of_view: return [] return self.uncommitted_node_reg_at_beginning_of_view.peekitem(-1)[1] def on_catchup_finished(self): self._load_current_node_reg() # we must have node regs for at least last two views self._load_last_view_node_reg() self.uncommitted_node_reg_at_beginning_of_view = copy.deepcopy( self.committed_node_reg_at_beginning_of_view) logger.info("Loaded current node registry from the ledger: {}".format( self.uncommitted_node_reg)) logger.info( "Current committed node registry for previous views: {}".format( sorted(self.committed_node_reg_at_beginning_of_view.items()))) logger.info( "Current uncommitted node registry for previous views: {}".format( sorted( self.uncommitted_node_reg_at_beginning_of_view.items()))) logger.info("Current active node registry: {}".format( self.active_node_reg)) def post_batch_applied(self, three_pc_batch: ThreePcBatch, prev_handler_result=None): # Observer case: if not self.uncommitted_node_reg and three_pc_batch.node_reg: self.uncommitted_node_reg = list(three_pc_batch.node_reg) view_no = three_pc_batch.view_no if three_pc_batch.original_view_no is None else three_pc_batch.original_view_no # Update active_node_reg to point to node_reg at the end of last view if view_no > self._uncommitted_view_no: self.uncommitted_node_reg_at_beginning_of_view[view_no] = list( self._uncommitted[-1].uncommitted_node_reg) if len( self._uncommitted) > 0 else list(self.committed_node_reg) self._uncommitted_view_no = view_no self._uncommitted.append( UncommittedNodeReg(list(self.uncommitted_node_reg), view_no)) three_pc_batch.node_reg = list(self.uncommitted_node_reg) logger.debug("Applied uncommitted node registry: {}".format( self.uncommitted_node_reg)) logger.debug( "Current committed node registry for previous views: {}".format( sorted(self.committed_node_reg_at_beginning_of_view.items()))) logger.debug( "Current uncommitted node registry for previous views: {}".format( sorted( self.uncommitted_node_reg_at_beginning_of_view.items()))) logger.debug("Current active node registry: {}".format( self.active_node_reg)) def post_batch_rejected(self, ledger_id, prev_handler_result=None): reverted = self._uncommitted.pop() if len(self._uncommitted) == 0: self.uncommitted_node_reg = list(self.committed_node_reg) self._uncommitted_view_no = self._committed_view_no else: last_uncommitted = self._uncommitted[-1] self.uncommitted_node_reg = last_uncommitted.uncommitted_node_reg self._uncommitted_view_no = last_uncommitted.view_no # find the uncommitted node reg at the beginning of view if self._uncommitted_view_no < reverted.view_no: self.uncommitted_node_reg_at_beginning_of_view.pop( reverted.view_no) logger.debug("Reverted uncommitted node registry from {} to {}".format( reverted.uncommitted_node_reg, self.uncommitted_node_reg)) logger.debug( "Current committed node registry for previous views: {}".format( sorted(self.committed_node_reg_at_beginning_of_view.items()))) logger.debug( "Current uncommitted node registry for previous views: {}".format( sorted( self.uncommitted_node_reg_at_beginning_of_view.items()))) logger.debug("Current active node registry: {}".format( self.active_node_reg)) def commit_batch(self, three_pc_batch: ThreePcBatch, prev_handler_result=None): # 1. Update node_reg_at_beginning_of_view first (to match the node reg at the end of last view) three_pc_batch_view_no = three_pc_batch.view_no if three_pc_batch.original_view_no is None else three_pc_batch.original_view_no if three_pc_batch_view_no > self._committed_view_no: self.committed_node_reg_at_beginning_of_view[ three_pc_batch_view_no] = list(self.committed_node_reg) self._committed_view_no = three_pc_batch_view_no self._gc_node_reg_at_beginning_of_view( self.committed_node_reg_at_beginning_of_view) self._gc_node_reg_at_beginning_of_view( self.uncommitted_node_reg_at_beginning_of_view) # 2. update committed node reg prev_committed = self.committed_node_reg self.committed_node_reg = self._uncommitted.popleft( ).uncommitted_node_reg # trigger view change if nodes count changed # TODO: create a new message to pass Suspicious events and make ViewChangeTriggerService the only place for # view change triggering if self.internal_bus and len(prev_committed) != len( self.committed_node_reg): self.internal_bus.send( VoteForViewChange(Suspicions.NODE_COUNT_CHANGED, three_pc_batch_view_no + 1)) if prev_committed != self.committed_node_reg: logger.info("Committed node registry: {}".format( self.committed_node_reg)) logger.info( "Current committed node registry for previous views: {}". format( sorted( self.committed_node_reg_at_beginning_of_view.items()))) logger.info( "Current uncommitted node registry for previous views: {}". format( sorted(self.uncommitted_node_reg_at_beginning_of_view. items()))) logger.info("Current active node registry: {}".format( self.active_node_reg)) else: logger.debug("Committed node registry: {}".format( self.committed_node_reg)) logger.debug( "Current committed node registry for previous views: {}". format( sorted( self.committed_node_reg_at_beginning_of_view.items()))) logger.debug( "Current uncommitted node registry for previous views: {}". format( sorted(self.uncommitted_node_reg_at_beginning_of_view. items()))) logger.debug("Current active node registry: {}".format( self.active_node_reg)) def _gc_node_reg_at_beginning_of_view(self, node_reg): # make sure that we have node reg for the current and previous view (which can be less than the current for more than 1) # Ex.: node_reg_at_beginning_of_view has views {0, 3, 5, 7, 11, 13), committed is now 7, so we need to keep all uncommitted (11, 13), # and keep the one from the previous view (5). Views 0 and 3 needs to be deleted. committed_view_nos = list(node_reg.keys()) prev_committed_index = max(committed_view_nos.index(self._committed_view_no) - 1, 0) \ if self._committed_view_no in node_reg else 0 for view_no in committed_view_nos[:prev_committed_index]: node_reg.pop(view_no, None) def apply_request(self, request: Request, batch_ts, prev_result): if request.operation.get(TYPE) != NODE: return None, None, None node_name = request.operation[DATA][ALIAS] services = request.operation[DATA].get(SERVICES) if services is None: return None, None, None if node_name not in self.uncommitted_node_reg and VALIDATOR in services: # new node added or old one promoted self.uncommitted_node_reg.append(node_name) logger.info("Changed uncommitted node registry to: {}".format( self.uncommitted_node_reg)) elif node_name in self.uncommitted_node_reg and VALIDATOR not in services: # existing node demoted self.uncommitted_node_reg.remove(node_name) logger.info("Changed uncommitted node registry to: {}".format( self.uncommitted_node_reg)) return None, None, None def update_state(self, txn, prev_result, request, is_committed=False): pass def static_validation(self, request): pass def additional_dynamic_validation(self, request, req_pp_time): pass def gen_state_key(self, txn): pass def _load_current_node_reg(self): node_reg = self.__load_current_node_reg_from_audit_ledger() if node_reg is None: node_reg = self.__load_node_reg_from_pool_ledger() self.uncommitted_node_reg = list(node_reg) self.committed_node_reg = list(node_reg) def _load_last_view_node_reg(self): self.committed_node_reg_at_beginning_of_view.clear() # 1. check if we have audit ledger at all audit_ledger = self.database_manager.get_ledger(AUDIT_LEDGER_ID) if not audit_ledger: # don't have audit ledger yet, so get aleady loaded values from the pool ledger self.committed_node_reg_at_beginning_of_view[0] = list( self.uncommitted_node_reg) self._committed_view_no = 0 self._uncommitted_view_no = 0 return # 2. get the first txn in the current view and last txn in the last view first_txn_in_this_view, last_txn_in_prev_view = self.__get_first_txn_in_view_from_audit( audit_ledger, audit_ledger.get_last_committed_txn()) # 3. set view_no self._committed_view_no = get_payload_data( first_txn_in_this_view)[AUDIT_TXN_VIEW_NO] self._uncommitted_view_no = self._committed_view_no # 4. Use last txn in last view to get the node reg # get from pool ledger if there is no txns for last view in audit if last_txn_in_prev_view is None: node_reg_this_view = self.__load_node_reg_for_first_audit_txn( first_txn_in_this_view) else: node_reg_this_view = list( self.__load_node_reg_from_audit_txn(audit_ledger, last_txn_in_prev_view)) self.committed_node_reg_at_beginning_of_view[ self._committed_view_no] = node_reg_this_view # 5. Check if audit ledger has information about 0 view only if self._committed_view_no == 0: return # 5. If audit has just 1 txn for the current view (and this view >0), then # get the last view from the pool ledger if last_txn_in_prev_view is None: # assume last view=0 if we don't know it self.committed_node_reg_at_beginning_of_view[0] = list( self.__load_node_reg_for_first_audit_txn( first_txn_in_this_view)) return # 6. Get the first audit txn for the last view first_txn_in_last_view, last_txn_in_pre_last_view = self.__get_first_txn_in_view_from_audit( audit_ledger, last_txn_in_prev_view) # 7. Use last txn in the view before the last one to get the node reg # get from pool ledger if there is no txns for view before the last one in audit if last_txn_in_pre_last_view is None: node_reg_last_view = self.__load_node_reg_for_first_audit_txn( first_txn_in_last_view) else: node_reg_last_view = list( self.__load_node_reg_from_audit_txn(audit_ledger, last_txn_in_pre_last_view)) last_view_no = get_payload_data( first_txn_in_last_view)[AUDIT_TXN_VIEW_NO] self.committed_node_reg_at_beginning_of_view[ last_view_no] = node_reg_last_view def __load_node_reg_from_pool_ledger(self, to=None): node_reg = [] for _, txn in self.ledger.getAllTxn(to=to): if get_type(txn) != NODE: continue txn_data = get_payload_data(txn) node_name = txn_data[DATA][ALIAS] services = txn_data[DATA].get(SERVICES) if services is None: continue if node_name not in node_reg and VALIDATOR in services: # new node added or old one promoted node_reg.append(node_name) elif node_name in node_reg and VALIDATOR not in services: # existing node demoted node_reg.remove(node_name) return node_reg # TODO: create a helper class to get data from Audit Ledger def __load_current_node_reg_from_audit_ledger(self): audit_ledger = self.database_manager.get_ledger(AUDIT_LEDGER_ID) if not audit_ledger: return None last_txn = audit_ledger.get_last_committed_txn() last_txn_node_reg = get_payload_data(last_txn).get(AUDIT_TXN_NODE_REG) if last_txn_node_reg is None: return None if isinstance(last_txn_node_reg, int): seq_no = get_seq_no(last_txn) - last_txn_node_reg audit_txn_for_seq_no = audit_ledger.getBySeqNo(seq_no) last_txn_node_reg = get_payload_data(audit_txn_for_seq_no).get( AUDIT_TXN_NODE_REG) if last_txn_node_reg is None: return None return last_txn_node_reg def __load_node_reg_from_audit_txn(self, audit_ledger, audit_txn): audit_txn_data = get_payload_data(audit_txn) # Get the node reg from audit txn node_reg = audit_txn_data.get(AUDIT_TXN_NODE_REG) if node_reg is None: return self.__load_node_reg_for_first_audit_txn(audit_txn) if isinstance(node_reg, int): seq_no = get_seq_no(audit_txn) - node_reg prev_audit_txn = audit_ledger.getBySeqNo(seq_no) node_reg = get_payload_data(prev_audit_txn).get(AUDIT_TXN_NODE_REG) if node_reg is None: return self.__load_node_reg_for_first_audit_txn(audit_txn) return node_reg def __get_first_txn_in_view_from_audit(self, audit_ledger, this_view_first_txn): ''' :param audit_ledger: audit ledger :param this_view_first_txn: a txn from the current view :return: the first txn in this view and the last txn in the previous view (if amy, otherwise None) ''' this_txn_view_no = get_payload_data(this_view_first_txn).get( AUDIT_TXN_VIEW_NO) prev_view_last_txn = None while True: txn_primaries = get_payload_data(this_view_first_txn).get( AUDIT_TXN_PRIMARIES) if isinstance(txn_primaries, int): seq_no = get_seq_no(this_view_first_txn) - txn_primaries this_view_first_txn = audit_ledger.getBySeqNo(seq_no) this_txn_seqno = get_seq_no(this_view_first_txn) if this_txn_seqno <= 1: break prev_view_last_txn = audit_ledger.getBySeqNo(this_txn_seqno - 1) prev_txn_view_no = get_payload_data(prev_view_last_txn).get( AUDIT_TXN_VIEW_NO) if this_txn_view_no != prev_txn_view_no: break this_view_first_txn = prev_view_last_txn prev_view_last_txn = None return this_view_first_txn, prev_view_last_txn def __load_node_reg_for_first_audit_txn(self, first_audit_txn): # If this is the first txn in the audit ledger, so that we don't know a full history, # then get node reg from the pool ledger audit_txn_data = get_payload_data(first_audit_txn) genesis_pool_ledger_size = audit_txn_data[AUDIT_TXN_LEDGERS_SIZE][ POOL_LEDGER_ID] return self.__load_node_reg_from_pool_ledger( to=genesis_pool_ledger_size)