예제 #1
0
class BlockValidator(object):
    """
    Responsible for validating a block, handles both chain extensions and fork
    will determine if the new block should be the head of the chain and return
    the information necessary to do the switch if necessary.
    """

    def __init__(self,
                 block_cache,
                 state_view_factory,
                 transaction_executor,
                 identity_signer,
                 data_dir,
                 config_dir,
                 permission_verifier,
                 thread_pool=None):
        """Initialize the BlockValidator
        Args:
            block_cache: The cache of all recent blocks and the processing
                state associated with them.
            state_view_factory: A factory that can be used to create read-
                only views of state for a particular merkle root, in
                particular the state as it existed when a particular block
                was the chain head.
            transaction_executor: The transaction executor used to
                process transactions.
            identity_signer: A cryptographic signer for signing blocks.
            data_dir: Path to location where persistent data for the
                consensus module can be stored.
            config_dir: Path to location where config data for the
                consensus module can be found.
            permission_verifier: The delegate for handling permission
                validation on blocks.
            thread_pool: (Optional) Executor pool used to submit block
                validation jobs. If not specified, a default will be created.
        Returns:
            None
        """
        self._block_cache = block_cache
        self._state_view_factory = state_view_factory
        self._transaction_executor = transaction_executor
        self._identity_signer = identity_signer
        self._data_dir = data_dir
        self._config_dir = config_dir
        self._permission_verifier = permission_verifier

        self._settings_view_factory = SettingsViewFactory(state_view_factory)

        self._thread_pool = InstrumentedThreadPoolExecutor(1) \
            if thread_pool is None else thread_pool

        self._moved_to_fork_count = COLLECTOR.counter(
            'chain_head_moved_to_fork_count', instance=self)

        # Blocks that are currently being processed
        self._blocks_processing = ConcurrentSet()

        # Descendant blocks that are waiting for an in process block
        # to complete
        self._blocks_pending = ConcurrentMultiMap()

    def stop(self):
        self._thread_pool.shutdown(wait=True)

    def _get_previous_block_state_root(self, blkw):
        if blkw.previous_block_id == NULL_BLOCK_IDENTIFIER:
            return INIT_ROOT_KEY

        return self._block_cache[blkw.previous_block_id].state_root_hash

    def _validate_batches_in_block(self, blkw, prev_state_root):
        """
        Validate all batches in the block. This includes:
            - Validating all transaction dependencies are met
            - Validating there are no duplicate batches or transactions
            - Validating execution of all batches in the block produces the
              correct state root hash

        Args:
            blkw: the block of batches to validate
            prev_state_root: the state root to execute transactions on top of

        Raises:
            BlockValidationFailure:
                If validation fails, raises this error with the reason.
            MissingDependency:
                Validation failed because of a missing dependency.
            DuplicateTransaction:
                Validation failed because of a duplicate transaction.
            DuplicateBatch:
                Validation failed because of a duplicate batch.
        """
        if not blkw.block.batches:
            return

        try:
            chain_commit_state = ChainCommitState(
                blkw.previous_block_id,
                self._block_cache,
                self._block_cache.block_store)

            scheduler = self._transaction_executor.create_scheduler(
                prev_state_root)

            chain_commit_state.check_for_duplicate_batches(
                blkw.block.batches)

            transactions = []
            for batch in blkw.block.batches:
                transactions.extend(batch.transactions)

            chain_commit_state.check_for_duplicate_transactions(
                transactions)

            chain_commit_state.check_for_transaction_dependencies(
                transactions)

            for batch, has_more in look_ahead(blkw.block.batches):
                if has_more:
                    scheduler.add_batch(batch)
                else:
                    scheduler.add_batch(batch, blkw.state_root_hash)

        except (DuplicateBatch,
                DuplicateTransaction,
                MissingDependency) as err:
            scheduler.cancel()
            raise BlockValidationFailure(
                "Block {} failed validation: {}".format(blkw, err))

        except Exception:
            scheduler.cancel()
            raise

        scheduler.finalize()
        scheduler.complete(block=True)
        state_hash = None

        for batch in blkw.batches:
            batch_result = scheduler.get_batch_execution_result(
                batch.header_signature)
            if batch_result is not None and batch_result.is_valid:
                txn_results = \
                    scheduler.get_transaction_execution_results(
                        batch.header_signature)
                blkw.execution_results.extend(txn_results)
                state_hash = batch_result.state_hash
                blkw.num_transactions += len(batch.transactions)
            else:
                raise BlockValidationFailure(
                    "Block {} failed validation: Invalid batch "
                    "{}".format(blkw, batch))

        if blkw.state_root_hash != state_hash:
            raise BlockValidationFailure(
                "Block {} failed state root hash validation. Expected {}"
                " but got {}".format(
                    blkw, blkw.state_root_hash, state_hash))

    def _validate_permissions(self, blkw, prev_state_root):
        """
        Validate that all of the batch signers and transaction signer for the
        batches in the block are permitted by the transactor permissioning
        roles stored in state as of the previous block. If a transactor is
        found to not be permitted, the block is invalid.
        """
        if blkw.block_num != 0:
            for batch in blkw.batches:
                if not self._permission_verifier.is_batch_signer_authorized(
                        batch, prev_state_root, from_state=True):
                    return False
        return True

    def _validate_on_chain_rules(self, blkw, prev_state_root):
        """
        Validate that the block conforms to all validation rules stored in
        state. If the block breaks any of the stored rules, the block is
        invalid.
        """
        if blkw.block_num != 0:
            return enforce_validation_rules(
                self._settings_view_factory.create_settings_view(
                    prev_state_root),
                blkw.header.signer_public_key,
                blkw.batches)
        return True

    def validate_block(self, blkw, chain_head=None):
        if blkw.status == BlockStatus.Valid:
            return
        elif blkw.status == BlockStatus.Invalid:
            raise BlockValidationFailure(
                'Block {} is already invalid'.format(blkw))

        # pylint: disable=broad-except
        try:
            if chain_head is None:
                # Try to get the chain head from the block store; note that the
                # block store may also return None for the chain head if a
                # genesis block hasn't been committed yet.
                chain_head = self._block_cache.block_store.chain_head

            try:
                prev_state_root = self._get_previous_block_state_root(blkw)
            except KeyError:
                raise BlockValidationError(
                    'Block {} rejected due to missing predecessor'.format(
                        blkw))

            if not self._validate_permissions(blkw, prev_state_root):
                raise BlockValidationFailure(
                    'Block {} failed permission validation'.format(blkw))

            try:
                prev_block = self._block_cache[blkw.previous_block_id]
            except KeyError:
                prev_block = None

            consensus = self._load_consensus(prev_block)
            public_key = \
                self._identity_signer.get_public_key().as_hex()
            consensus_block_verifier = consensus.BlockVerifier(
                block_cache=self._block_cache,
                state_view_factory=self._state_view_factory,
                data_dir=self._data_dir,
                config_dir=self._config_dir,
                validator_id=public_key)

            if not consensus_block_verifier.verify_block(blkw):
                raise BlockValidationFailure(
                    'Block {} failed {} consensus validation'.format(
                        blkw, consensus))

            if not self._validate_on_chain_rules(blkw, prev_state_root):
                raise BlockValidationFailure(
                    'Block {} failed on-chain validation rules'.format(
                        blkw))

            self._validate_batches_in_block(blkw, prev_state_root)

            # since changes to the chain-head can change the state of the
            # blocks in BlockStore we have to revalidate this block.
            block_store = self._block_cache.block_store

            # The chain_head is None when this is the genesis block or if the
            # block store has no chain_head.
            if chain_head is not None:
                if chain_head.identifier != block_store.chain_head.identifier:
                    raise ChainHeadUpdated()

            blkw.status = BlockStatus.Valid

        except BlockValidationFailure as err:
            blkw.status = BlockStatus.Invalid
            raise err

        except BlockValidationError as err:
            blkw.status = BlockStatus.Unknown
            raise err

        except ChainHeadUpdated as e:
            raise e

        except Exception as e:
            LOGGER.exception(
                "Unhandled exception BlockValidator.validate_block()")
            raise e

    @staticmethod
    def _compare_chain_height(head_a, head_b):
        """Returns True if head_a is taller, False if head_b is taller, and
        True if the heights are the same."""
        return head_a.block_num - head_b.block_num >= 0

    def _build_fork_diff_to_common_height(self, head_long, head_short):
        """Returns a list of blocks on the longer chain since the greatest
        common height between the two chains. Note that the chains may not
        have the same block id at the greatest common height.

        Args:
            head_long (BlockWrapper)
            head_short (BlockWrapper)

        Returns:
            (list of BlockWrapper) All blocks in the longer chain since the
            last block in the shorter chain. Ordered newest to oldest.

        Raises:
            BlockValidationError
                The block is missing a predecessor. Note that normally this
                shouldn't happen because of the completer."""
        fork_diff = []

        last = head_short.block_num
        blk = head_long

        while blk.block_num > last:
            if blk.previous_block_id == NULL_BLOCK_IDENTIFIER:
                break

            fork_diff.append(blk)
            try:
                blk = self._block_cache[blk.previous_block_id]
            except KeyError:
                raise BlockValidationError(
                    'Failed to build fork diff: block {} missing predecessor'
                    .format(blk))

        return blk, fork_diff

    def _extend_fork_diff_to_common_ancestor(
        self, new_blkw, cur_blkw, new_chain, cur_chain
    ):
        """ Finds a common ancestor of the two chains. new_blkw and cur_blkw
        must be at the same height, or this will always fail.
        """
        while cur_blkw.identifier != new_blkw.identifier:
            if (cur_blkw.previous_block_id == NULL_BLOCK_IDENTIFIER
                    or new_blkw.previous_block_id == NULL_BLOCK_IDENTIFIER):
                # We are at a genesis block and the blocks are not the same
                for b in new_chain:
                    b.status = BlockStatus.Invalid
                raise BlockValidationFailure(
                    'Block {} rejected due to wrong genesis {}'.format(
                        cur_blkw, new_blkw))

            new_chain.append(new_blkw)
            try:
                new_blkw = self._block_cache[new_blkw.previous_block_id]
            except KeyError:
                raise BlockValidationError(
                    'Block {} rejected due to missing predecessor {}'.format(
                        new_blkw, new_blkw.previous_block_id))

            cur_chain.append(cur_blkw)
            cur_blkw = self._block_cache[cur_blkw.previous_block_id]

    def _compare_forks_consensus(self, chain_head, new_block):
        """Ask the consensus module which fork to choose.
        """
        public_key = self._identity_signer.get_public_key().as_hex()
        consensus = self._load_consensus(chain_head)
        fork_resolver = consensus.ForkResolver(
            block_cache=self._block_cache,
            state_view_factory=self._state_view_factory,
            data_dir=self._data_dir,
            config_dir=self._config_dir,
            validator_id=public_key)

        return fork_resolver.compare_forks(chain_head, new_block)

    def _load_consensus(self, block):
        """Load the consensus module using the state as of the given block."""
        if block is not None:
            return ConsensusFactory.get_configured_consensus_module(
                block.header_signature,
                BlockWrapper.state_view_for_block(
                    block,
                    self._state_view_factory))
        return ConsensusFactory.get_consensus_module('genesis')

    @staticmethod
    def _get_batch_commit_changes(new_chain, cur_chain):
        """
        Get all the batches that should be committed from the new chain and
        all the batches that should be uncommitted from the current chain.
        """
        committed_batches = []
        for blkw in new_chain:
            for batch in blkw.batches:
                committed_batches.append(batch)

        uncommitted_batches = []
        for blkw in cur_chain:
            for batch in blkw.batches:
                uncommitted_batches.append(batch)

        return (committed_batches, uncommitted_batches)

    def submit_blocks_for_verification(self, blocks, callback):
        for block in blocks:
            if self.in_process(block.header_signature):
                LOGGER.debug("Block already in process: %s", block)
                continue

            if self.in_process(block.previous_block_id):
                LOGGER.debug(
                    "Previous block '%s' in process,"
                    " adding '%s' pending",
                    block.previous_block_id, block)
                self._add_block_to_pending(block)
                continue

            if self.in_pending(block.previous_block_id):
                LOGGER.debug(
                    "Previous block '%s' is pending,"
                    " adding '%s' pending",
                    block.previous_block_id, block)
                self._add_block_to_pending(block)
                continue

            LOGGER.debug(
                "Adding block %s for processing", block.identifier)

            # Add the block to the set of blocks being processed
            self._blocks_processing.add(block.identifier)

            # Schedule the block for processing
            self._thread_pool.submit(
                self.process_block_verification, block,
                self._wrap_callback(block, callback))

    def _wrap_callback(self, block, callback):
        # Internal cleanup after verification
        def wrapper(commit_new_block, result, chain_head_updated=False):
            block = result.block
            LOGGER.debug("Removing block from processing %s", block.identifier)
            try:
                self._blocks_processing.remove(block.identifier)
            except KeyError:
                LOGGER.warning(
                    "Tried to remove block from in process but it"
                    " wasn't in processes: %s",
                    block.identifier)

            # If the block was valid, submit all pending blocks for validation
            if block.status == BlockStatus.Valid:
                blocks_now_ready = self._blocks_pending.pop(
                    block.identifier, [])
                self.submit_blocks_for_verification(blocks_now_ready, callback)

            elif block.status == BlockStatus.Invalid:
                # If the block was invalid, mark all pending blocks as invalid
                blocks_now_invalid = self._blocks_pending.pop(
                    block.identifier, [])

                while blocks_now_invalid:
                    invalid_block = blocks_now_invalid.pop()
                    invalid_block.status = BlockStatus.Invalid

                    LOGGER.debug(
                        'Marking descendant block invalid: %s',
                        invalid_block)

                    # Get descendants of the descendant
                    blocks_now_invalid.extend(
                        self._blocks_pending.pop(invalid_block.identifier, []))

            elif not chain_head_updated:
                # If an error occured during validation, something is wrong
                # internally and we need to abort validation of this block
                # and all its children without marking them as invalid.
                blocks_to_remove = self._blocks_pending.pop(
                    block.identifier, [])

                while blocks_to_remove:
                    block = blocks_to_remove.pop()

                    LOGGER.error(
                        'Removing block from cache and pending due to error '
                        'during validation: %s; status: %s',
                        block, block.status)

                    del self._block_cache[block.identifier]

                    # Get descendants of the descendant
                    blocks_to_remove.extend(
                        self._blocks_pending.pop(block.identifier, []))

            callback(commit_new_block, result)

        return wrapper

    def in_process(self, block_id):
        return block_id in self._blocks_processing

    def in_pending(self, block_id):
        return block_id in self._blocks_pending

    def _add_block_to_pending(self, block):
        previous = block.previous_block_id
        self._blocks_pending.append(previous, block)

    def process_block_verification(self, block, callback):
        """
        Main entry for Block Validation, Take a given candidate block
        and decide if it is valid then if it is valid determine if it should
        be the new head block. Returns the results to the ChainController
        so that the change over can be made if necessary.
        """
        try:
            result = BlockValidationResult(block)
            LOGGER.info("Starting block validation of : %s", block)

            # Get the current chain_head and store it in the result
            chain_head = self._block_cache.block_store.chain_head
            result.chain_head = chain_head

            # Create new local variables for current and new block, since
            # these variables get modified later
            current_block = chain_head
            new_block = block

            try:
                # Get all the blocks since the greatest common height from the
                # longer chain.
                if self._compare_chain_height(current_block, new_block):
                    current_block, result.current_chain =\
                        self._build_fork_diff_to_common_height(
                            current_block, new_block)
                else:
                    new_block, result.new_chain =\
                        self._build_fork_diff_to_common_height(
                            new_block, current_block)

                # Add blocks to the two chains until a common ancestor is found
                # or raise an exception if no common ancestor is found
                self._extend_fork_diff_to_common_ancestor(
                    new_block, current_block,
                    result.new_chain, result.current_chain)
            except BlockValidationFailure as err:
                LOGGER.warning(
                    'Block %s failed validation: %s',
                    block, err)
                block.status = BlockStatus.Invalid
            except BlockValidationError as err:
                LOGGER.error(
                    'Encountered an error while validating %s: %s',
                    block, err)
                callback(False, result)
                return

            valid = True
            for blk in reversed(result.new_chain):
                if valid:
                    try:
                        self.validate_block(
                            blk, chain_head)
                    except BlockValidationFailure as err:
                        LOGGER.warning(
                            'Block %s failed validation: %s',
                            blk, err)
                        valid = False
                    except BlockValidationError as err:
                        LOGGER.error(
                            'Encountered an error while validating %s: %s',
                            blk, err)
                        callback(False, result)
                    result.transaction_count += block.num_transactions
                else:
                    LOGGER.info(
                        "Block marked invalid (invalid predecessor): %s", blk)
                    blk.status = BlockStatus.Invalid

            if not valid:
                callback(False, result)
                return

            # Ask consensus if the new chain should be committed
            LOGGER.info(
                "Comparing current chain head '%s' against new block '%s'",
                chain_head, new_block)
            for i in range(max(
                len(result.new_chain), len(result.current_chain)
            )):
                cur = new = num = "-"
                if i < len(result.current_chain):
                    cur = result.current_chain[i].header_signature[:8]
                    num = result.current_chain[i].block_num
                if i < len(result.new_chain):
                    new = result.new_chain[i].header_signature[:8]
                    num = result.new_chain[i].block_num
                LOGGER.info(
                    "Fork comparison at height %s is between %s and %s",
                    num, cur, new)

            commit_new_chain = self._compare_forks_consensus(chain_head, block)

            # If committing the new chain, get the list of committed batches
            # from the current chain that need to be uncommitted and the list
            # of uncommitted batches from the new chain that need to be
            # committed.
            if commit_new_chain:
                commit, uncommit =\
                    self._get_batch_commit_changes(
                        result.new_chain, result.current_chain)
                result.committed_batches = commit
                result.uncommitted_batches = uncommit

                if result.new_chain[0].previous_block_id \
                        != chain_head.identifier:
                    self._moved_to_fork_count.inc()

            # Pass the results to the callback function
            callback(commit_new_chain, result)
            LOGGER.info("Finished block validation of: %s", block)

        except ChainHeadUpdated:
            LOGGER.debug(
                "Block validation failed due to chain head update: %s", block)
            callback(False, result, chain_head_updated=True)
            return
        except Exception:  # pylint: disable=broad-except
            LOGGER.exception(
                "Block validation failed with unexpected error: %s", block)
            # callback to clean up the block out of the processing list.
            callback(False, result)
예제 #2
0
class TransactionExecutor:
    def __init__(self,
                 service,
                 context_manager,
                 settings_view_factory,
                 scheduler_type,
                 invalid_observers=None):
        """
        Args:
            service (Interconnect): The zmq internal interface
            context_manager (ContextManager): Cache of state for tps
            settings_view_factory (SettingsViewFactory): Read-only view of
                setting state.
        Attributes:
            processor_manager (ProcessorManager): All of the registered
                transaction processors and a way to find the next one to send
                to.
        """
        self._service = service
        self._context_manager = context_manager
        self.processor_manager = ProcessorManager(RoundRobinProcessorIterator)
        self._settings_view_factory = settings_view_factory
        self._executing_threadpool = \
            InstrumentedThreadPoolExecutor(max_workers=5, name='Executing')
        self._alive_threads = []
        self._lock = threading.Lock()

        self._invalid_observers = ([] if invalid_observers is None
                                   else invalid_observers)

        self._scheduler_type = scheduler_type

    def create_scheduler(self,
                         first_state_root,
                         always_persist=False):

        # Useful for a logical first state root of ""
        if not first_state_root:
            first_state_root = self._context_manager.get_first_root()

        if self._scheduler_type == "serial":
            scheduler = SerialScheduler(
                squash_handler=self._context_manager.get_squash_handler(),
                first_state_hash=first_state_root,
                always_persist=always_persist)
        elif self._scheduler_type == "parallel":
            scheduler = ParallelScheduler(
                squash_handler=self._context_manager.get_squash_handler(),
                first_state_hash=first_state_root,
                always_persist=always_persist)

        else:
            raise AssertionError(
                "Scheduler type must be either serial or parallel. Current"
                " scheduler type is {}.".format(self._scheduler_type))

        self.execute(scheduler=scheduler)
        return scheduler

    def check_connections(self):
        self._executing_threadpool.submit(self._check_connections)

    def _remove_done_threads(self):
        for t in self._alive_threads.copy():
            if t.is_done():
                with self._lock:
                    self._alive_threads.remove(t)

    def _cancel_threads(self):
        for t in self._alive_threads:
            if not t.is_done():
                t.cancel()

    def _check_connections(self):
        # This is not ideal, because it locks up the current thread while
        # waiting for the results.
        try:
            with self._lock:
                futures = {}
                for connection_id in \
                        self.processor_manager.get_all_processors():
                    fut = self._service.send(
                        validator_pb2.Message.PING_REQUEST,
                        network_pb2.PingRequest().SerializeToString(),
                        connection_id=connection_id)
                    futures[fut] = connection_id
                for fut in futures:
                    try:
                        fut.result(timeout=10)
                    except FutureTimeoutError:
                        LOGGER.warning(
                            "%s did not respond to the Ping, removing "
                            "transaction processor.", futures[fut])
                        self._remove_broken_connection(futures[fut])
        except Exception:  # pylint: disable=broad-except
            LOGGER.exception('Unhandled exception while checking connections')

    def _remove_broken_connection(self, connection_id):
        for t in self._alive_threads:
            t.remove_broken_connection(connection_id)

    def execute(self, scheduler):
        self._remove_done_threads()
        t = TransactionExecutorThread(
            service=self._service,
            context_manager=self._context_manager,
            scheduler=scheduler,
            processor_manager=self.processor_manager,
            settings_view_factory=self._settings_view_factory,
            invalid_observers=self._invalid_observers)
        self._executing_threadpool.submit(t.execute_thread)
        with self._lock:
            self._alive_threads.append(t)

    def stop(self):
        self._cancel_threads()
        self._executing_threadpool.shutdown(wait=True)
예제 #3
0
def verify_state(global_state_db, blockstore, bind_component, scheduler_type):
    """
    Verify the state root hash of all blocks is in state and if not,
    reconstruct the missing state. Assumes that there are no "holes" in
    state, ie starting from genesis, state is present for all blocks up to some
    point and then not at all. If persist is False, this recomputes state in
    memory for all blocks in the blockstore and verifies the state root
    hashes.

    Raises:
        InvalidChainError: The chain in the blockstore is not valid.
        ExecutionError: An unrecoverable error was encountered during batch
            execution.
    """
    state_view_factory = StateViewFactory(global_state_db)

    # Check if we should do state verification
    start_block, prev_state_root = search_for_present_state_root(
        blockstore, state_view_factory)

    if start_block is None:
        LOGGER.info(
            "Skipping state verification: chain head's state root is present")
        return

    LOGGER.info(
        "Recomputing missing state from block %s with %s scheduler",
        start_block, scheduler_type)

    component_thread_pool = InstrumentedThreadPoolExecutor(
        max_workers=10,
        name='Component')

    component_dispatcher = Dispatcher()
    component_service = Interconnect(
        bind_component,
        component_dispatcher,
        secured=False,
        heartbeat=False,
        max_incoming_connections=20,
        monitor=True,
        max_future_callback_workers=10)

    context_manager = ContextManager(global_state_db)

    transaction_executor = TransactionExecutor(
        service=component_service,
        context_manager=context_manager,
        settings_view_factory=SettingsViewFactory(state_view_factory),
        scheduler_type=scheduler_type,
        invalid_observers=[])

    component_service.set_check_connections(
        transaction_executor.check_connections)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_RECEIPT_ADD_DATA_REQUEST,
        tp_state_handlers.TpReceiptAddDataHandler(context_manager),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_EVENT_ADD_REQUEST,
        tp_state_handlers.TpEventAddHandler(context_manager),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_STATE_DELETE_REQUEST,
        tp_state_handlers.TpStateDeleteHandler(context_manager),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_STATE_GET_REQUEST,
        tp_state_handlers.TpStateGetHandler(context_manager),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_STATE_SET_REQUEST,
        tp_state_handlers.TpStateSetHandler(context_manager),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_REGISTER_REQUEST,
        processor_handlers.ProcessorRegisterValidationHandler(),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_REGISTER_REQUEST,
        processor_handlers.ProcessorRegisterHandler(
            transaction_executor.processor_manager),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_UNREGISTER_REQUEST,
        processor_handlers.ProcessorUnRegisterHandler(
            transaction_executor.processor_manager),
        component_thread_pool)

    component_dispatcher.start()
    component_service.start()

    process_blocks(
        initial_state_root=prev_state_root,
        blocks=blockstore.get_block_iter(
            start_block=start_block, reverse=False),
        transaction_executor=transaction_executor,
        context_manager=context_manager,
        state_view_factory=state_view_factory)

    component_dispatcher.stop()
    component_service.stop()
    component_thread_pool.shutdown(wait=True)
    transaction_executor.stop()
    context_manager.stop()
예제 #4
0
class BlockValidator(object):
    """
    Responsible for validating a block, handles both chain extensions and fork
    will determine if the new block should be the head of the chain and return
    the information necessary to do the switch if necessary.
    """
    def __init__(self,
                 block_cache,
                 state_view_factory,
                 transaction_executor,
                 squash_handler,
                 identity_signer,
                 data_dir,
                 config_dir,
                 permission_verifier,
                 metrics_registry=None,
                 thread_pool=None):
        """Initialize the BlockValidator
        Args:
            block_cache: The cache of all recent blocks and the processing
                state associated with them.
            state_view_factory: A factory that can be used to create read-
                only views of state for a particular merkle root, in
                particular the state as it existed when a particular block
                was the chain head.
            transaction_executor: The transaction executor used to
                process transactions.
            squash_handler: A parameter passed when creating transaction
                schedulers.
            identity_signer: A cryptographic signer for signing blocks.
            data_dir: Path to location where persistent data for the
                consensus module can be stored.
            config_dir: Path to location where config data for the
                consensus module can be found.
            permission_verifier: The delegate for handling permission
                validation on blocks.
            metrics_registry: (Optional) Pyformance metrics registry handle for
                creating new metrics.
            thread_pool: (Optional) Executor pool used to submit block
                validation jobs. If not specified, a default will be created.
        Returns:
            None
        """
        self._block_cache = block_cache
        self._state_view_factory = state_view_factory
        self._transaction_executor = transaction_executor
        self._squash_handler = squash_handler
        self._identity_signer = identity_signer
        self._data_dir = data_dir
        self._config_dir = config_dir
        self._permission_verifier = permission_verifier

        self._validation_rule_enforcer = ValidationRuleEnforcer(
            SettingsViewFactory(state_view_factory))

        self._thread_pool = InstrumentedThreadPoolExecutor(1) \
            if thread_pool is None else thread_pool

        if metrics_registry:
            self._moved_to_fork_count = CounterWrapper(
                metrics_registry.counter('chain_head_moved_to_fork_count'))
        else:
            self._moved_to_fork_count = CounterWrapper()

        # Blocks that are currently being processed
        self._blocks_processing = ConcurrentSet()

        # Descendant blocks that are waiting for an in process block
        # to complete
        self._blocks_pending = ConcurrentMultiMap()

    def stop(self):
        self._thread_pool.shutdown(wait=True)

    def _get_previous_block_state_root(self, blkw):
        if blkw.previous_block_id == NULL_BLOCK_IDENTIFIER:
            return INIT_ROOT_KEY

        return self._block_cache[blkw.previous_block_id].state_root_hash

    def _validate_batches_in_block(self, blkw, prev_state_root):
        """
        Validate all batches in the block. This includes:
            - Validating all transaction dependencies are met
            - Validating there are no duplicate batches or transactions
            - Validating execution of all batches in the block produces the
              correct state root hash

        Args:
            blkw: the block of batches to validate
            prev_state_root: the state root to execute transactions on top of

        Raises:
            BlockValidationError:
                If validation fails, raises this error with the reason.
            MissingDependency:
                Validation failed because of a missing dependency.
            DuplicateTransaction:
                Validation failed because of a duplicate transaction.
            DuplicateBatch:
                Validation failed because of a duplicate batch.
        """
        if not blkw.block.batches:
            return

        try:
            chain_commit_state = ChainCommitState(
                blkw.previous_block_id, self._block_cache,
                self._block_cache.block_store)

            scheduler = self._transaction_executor.create_scheduler(
                self._squash_handler, prev_state_root)
            self._transaction_executor.execute(scheduler)

            chain_commit_state.check_for_duplicate_batches(blkw.block.batches)

            transactions = []
            for batch in blkw.block.batches:
                transactions.extend(batch.transactions)

            chain_commit_state.check_for_duplicate_transactions(transactions)

            chain_commit_state.check_for_transaction_dependencies(transactions)

            for batch, has_more in look_ahead(blkw.block.batches):
                if has_more:
                    scheduler.add_batch(batch)
                else:
                    scheduler.add_batch(batch, blkw.state_root_hash)

        except (DuplicateBatch, DuplicateTransaction,
                MissingDependency) as err:
            scheduler.cancel()
            raise BlockValidationError("Block {} failed validation: {}".format(
                blkw, err))

        except Exception:
            scheduler.cancel()
            raise

        scheduler.finalize()
        scheduler.complete(block=True)
        state_hash = None

        for batch in blkw.batches:
            batch_result = scheduler.get_batch_execution_result(
                batch.header_signature)
            if batch_result is not None and batch_result.is_valid:
                txn_results = \
                    scheduler.get_transaction_execution_results(
                        batch.header_signature)
                blkw.execution_results.extend(txn_results)
                state_hash = batch_result.state_hash
                blkw.num_transactions += len(batch.transactions)
            else:
                raise BlockValidationError(
                    "Block {} failed validation: Invalid batch "
                    "{}".format(blkw, batch))

        if blkw.state_root_hash != state_hash:
            raise BlockValidationError(
                "Block {} failed state root hash validation. Expected {}"
                " but got {}".format(blkw, blkw.state_root_hash, state_hash))

    def _validate_permissions(self, blkw, prev_state_root):
        """
        Validate that all of the batch signers and transaction signer for the
        batches in the block are permitted by the transactor permissioning
        roles stored in state as of the previous block. If a transactor is
        found to not be permitted, the block is invalid.
        """
        if blkw.block_num != 0:
            for batch in blkw.batches:
                if not self._permission_verifier.is_batch_signer_authorized(
                        batch, prev_state_root, from_state=True):
                    return False
        return True

    def _validate_on_chain_rules(self, blkw, prev_state_root):
        """
        Validate that the block conforms to all validation rules stored in
        state. If the block breaks any of the stored rules, the block is
        invalid.
        """
        if blkw.block_num != 0:
            return self._validation_rule_enforcer.validate(
                blkw, prev_state_root)
        return True

    def validate_block(self, blkw, chain_head=None):
        if blkw.status == BlockStatus.Valid:
            return
        elif blkw.status == BlockStatus.Invalid:
            raise BlockValidationError(
                'Block {} is already invalid'.format(blkw))

        # pylint: disable=broad-except
        try:
            if chain_head is None:
                # Try to get the chain head from the block store; note that the
                # block store may also return None for the chain head if a
                # genesis block hasn't been committed yet.
                chain_head = self._block_cache.block_store.chain_head

            try:
                prev_state_root = self._get_previous_block_state_root(blkw)
            except KeyError:
                raise BlockValidationError(
                    'Block {} rejected due to missing predecessor'.format(
                        blkw))

            if not self._validate_permissions(blkw, prev_state_root):
                raise BlockValidationError(
                    'Block {} failed permission validation'.format(blkw))

            try:
                prev_block = self._block_cache[blkw.previous_block_id]
            except KeyError:
                prev_block = None

            consensus = self._load_consensus(prev_block)
            public_key = \
                self._identity_signer.get_public_key().as_hex()
            consensus_block_verifier = consensus.BlockVerifier(
                block_cache=self._block_cache,
                state_view_factory=self._state_view_factory,
                data_dir=self._data_dir,
                config_dir=self._config_dir,
                validator_id=public_key)

            if not consensus_block_verifier.verify_block(blkw):
                raise BlockValidationError(
                    'Block {} failed {} consensus validation'.format(
                        blkw, consensus))

            if not self._validate_on_chain_rules(blkw, prev_state_root):
                raise BlockValidationError(
                    'Block {} failed on-chain validation rules'.format(blkw))

            self._validate_batches_in_block(blkw, prev_state_root)

            # since changes to the chain-head can change the state of the
            # blocks in BlockStore we have to revalidate this block.
            block_store = self._block_cache.block_store

            # The chain_head is None when this is the genesis block or if the
            # block store has no chain_head.
            if chain_head is not None:
                if chain_head.identifier != block_store.chain_head.identifier:
                    raise ChainHeadUpdated()

            blkw.status = BlockStatus.Valid

        except BlockValidationError as err:
            blkw.status = BlockStatus.Invalid
            raise err

        except ChainHeadUpdated as e:
            raise e

        except Exception as e:
            LOGGER.exception(
                "Unhandled exception BlockValidator.validate_block()")
            raise e

    @staticmethod
    def _compare_chain_height(head_a, head_b):
        """Returns True if head_a is taller, False if head_b is taller, and
        True if the heights are the same."""
        return head_a.block_num - head_b.block_num >= 0

    def _build_fork_diff_to_common_height(self, head_long, head_short):
        """Returns a list of blocks on the longer chain since the greatest
        common height between the two chains. Note that the chains may not
        have the same block id at the greatest common height.

        Args:
            head_long (BlockWrapper)
            head_short (BlockWrapper)

        Returns:
            (list of BlockWrapper) All blocks in the longer chain since the
            last block in the shorter chain. Ordered newest to oldest.

        Raises:
            BlockValidationError
                The block is missing a predecessor. Note that normally this
                shouldn't happen because of the completer."""
        fork_diff = []

        last = head_short.block_num
        blk = head_long

        while blk.block_num > last:
            if blk.previous_block_id == NULL_BLOCK_IDENTIFIER:
                break

            fork_diff.append(blk)
            try:
                blk = self._block_cache[blk.previous_block_id]
            except KeyError:
                LOGGER.debug(
                    "Failed to build fork diff due to missing predecessor: %s",
                    blk)

                # Mark all blocks in the longer chain since the invalid block
                # as invalid.
                for blk in fork_diff:
                    blk.status = BlockStatus.Invalid
                raise BlockValidationError(
                    'Failed to build fork diff: block {} missing predecessor'.
                    format(blk))

        return blk, fork_diff

    def _extend_fork_diff_to_common_ancestor(self, new_blkw, cur_blkw,
                                             new_chain, cur_chain):
        """ Finds a common ancestor of the two chains. new_blkw and cur_blkw
        must be at the same height, or this will always fail.
        """
        while cur_blkw.identifier != new_blkw.identifier:
            if (cur_blkw.previous_block_id == NULL_BLOCK_IDENTIFIER
                    or new_blkw.previous_block_id == NULL_BLOCK_IDENTIFIER):
                # We are at a genesis block and the blocks are not the same
                for b in new_chain:
                    b.status = BlockStatus.Invalid
                raise BlockValidationError(
                    'Block {} rejected due to wrong genesis {}'.format(
                        cur_blkw, new_blkw))

            new_chain.append(new_blkw)
            try:
                new_blkw = self._block_cache[new_blkw.previous_block_id]
            except KeyError:
                for b in new_chain:
                    b.status = BlockStatus.Invalid
                raise BlockValidationError(
                    'Block {} rejected due to missing predecessor {}'.format(
                        new_blkw, new_blkw.previous_block_id))

            cur_chain.append(cur_blkw)
            cur_blkw = self._block_cache[cur_blkw.previous_block_id]

    def _compare_forks_consensus(self, chain_head, new_block):
        """Ask the consensus module which fork to choose.
        """
        public_key = self._identity_signer.get_public_key().as_hex()
        consensus = self._load_consensus(chain_head)
        fork_resolver = consensus.ForkResolver(
            block_cache=self._block_cache,
            state_view_factory=self._state_view_factory,
            data_dir=self._data_dir,
            config_dir=self._config_dir,
            validator_id=public_key)

        return fork_resolver.compare_forks(chain_head, new_block)

    def _load_consensus(self, block):
        """Load the consensus module using the state as of the given block."""
        if block is not None:
            return ConsensusFactory.get_configured_consensus_module(
                block.header_signature,
                BlockWrapper.state_view_for_block(block,
                                                  self._state_view_factory))
        return ConsensusFactory.get_consensus_module('genesis')

    @staticmethod
    def _get_batch_commit_changes(new_chain, cur_chain):
        """
        Get all the batches that should be committed from the new chain and
        all the batches that should be uncommitted from the current chain.
        """
        committed_batches = []
        for blkw in new_chain:
            for batch in blkw.batches:
                committed_batches.append(batch)

        uncommitted_batches = []
        for blkw in cur_chain:
            for batch in blkw.batches:
                uncommitted_batches.append(batch)

        return (committed_batches, uncommitted_batches)

    def submit_blocks_for_verification(self, blocks, callback):
        for block in blocks:
            if self.in_process(block.header_signature):
                LOGGER.debug("Block already in process: %s", block)
                continue

            if self.in_process(block.previous_block_id):
                LOGGER.debug(
                    "Previous block '%s' in process,"
                    " adding '%s' pending", block.previous_block_id, block)
                self._add_block_to_pending(block)
                continue

            if self.in_pending(block.previous_block_id):
                LOGGER.debug(
                    "Previous block '%s' is pending,"
                    " adding '%s' pending", block.previous_block_id, block)
                self._add_block_to_pending(block)
                continue

            LOGGER.debug("Adding block %s for processing", block.identifier)

            # Add the block to the set of blocks being processed
            self._blocks_processing.add(block.identifier)

            # Schedule the block for processing
            self._thread_pool.submit(self.process_block_verification, block,
                                     self._wrap_callback(block, callback))

    def _wrap_callback(self, block, callback):
        # Internal cleanup after verification
        def wrapper(commit_new_block, result):
            LOGGER.debug("Removing block from processing %s",
                         block.identifier[:6])
            try:
                self._blocks_processing.remove(block.identifier)
            except KeyError:
                LOGGER.warning(
                    "Tried to remove block from in process but it"
                    " wasn't in processes: %s", block.identifier)

            # If the block is invalid, mark all descendant blocks as invalid
            # and remove from pending.
            if block.status == BlockStatus.Valid:
                blocks_now_ready = self._blocks_pending.pop(
                    block.identifier, [])
                self.submit_blocks_for_verification(blocks_now_ready, callback)

            else:
                # Get all the pending blocks that can now be processed
                blocks_now_invalid = self._blocks_pending.pop(
                    block.identifier, [])

                while blocks_now_invalid:
                    invalid_block = blocks_now_invalid.pop()
                    invalid_block.status = BlockStatus.Invalid

                    LOGGER.debug('Marking descendant block invalid: %s',
                                 invalid_block)

                    # Get descendants of the descendant
                    blocks_now_invalid.extend(
                        self._blocks_pending.pop(invalid_block.identifier, []))

            callback(commit_new_block, result)

        return wrapper

    def in_process(self, block_id):
        return block_id in self._blocks_processing

    def in_pending(self, block_id):
        return block_id in self._blocks_pending

    def _add_block_to_pending(self, block):
        previous = block.previous_block_id
        self._blocks_pending.append(previous, block)

    def process_block_verification(self, block, callback):
        """
        Main entry for Block Validation, Take a given candidate block
        and decide if it is valid then if it is valid determine if it should
        be the new head block. Returns the results to the ChainController
        so that the change over can be made if necessary.
        """
        try:
            result = BlockValidationResult(block)
            LOGGER.info("Starting block validation of : %s", block)

            # Get the current chain_head and store it in the result
            chain_head = self._block_cache.block_store.chain_head
            result.chain_head = chain_head

            # Create new local variables for current and new block, since
            # these variables get modified later
            current_block = chain_head
            new_block = block

            try:
                # Get all the blocks since the greatest common height from the
                # longer chain.
                if self._compare_chain_height(current_block, new_block):
                    current_block, result.current_chain =\
                        self._build_fork_diff_to_common_height(
                            current_block, new_block)
                else:
                    new_block, result.new_chain =\
                        self._build_fork_diff_to_common_height(
                            new_block, current_block)

                # Add blocks to the two chains until a common ancestor is found
                # or raise an exception if no common ancestor is found
                self._extend_fork_diff_to_common_ancestor(
                    new_block, current_block, result.new_chain,
                    result.current_chain)
            except BlockValidationError as err:
                LOGGER.warning('%s', err)
                callback(False, result)
                return

            valid = True
            for blk in reversed(result.new_chain):
                if valid:
                    try:
                        self.validate_block(blk, chain_head)
                    except BlockValidationError as err:
                        LOGGER.warning('Block %s failed validation: %s', blk,
                                       err)
                        valid = False
                    result.transaction_count += block.num_transactions
                else:
                    LOGGER.info(
                        "Block marked invalid(invalid predecessor): %s", blk)
                    blk.status = BlockStatus.Invalid

            if not valid:
                callback(False, result)
                return

            # Ask consensus if the new chain should be committed
            LOGGER.info(
                "Comparing current chain head '%s' against new block '%s'",
                chain_head, new_block)
            for i in range(
                    max(len(result.new_chain), len(result.current_chain))):
                cur = new = num = "-"
                if i < len(result.current_chain):
                    cur = result.current_chain[i].header_signature[:8]
                    num = result.current_chain[i].block_num
                if i < len(result.new_chain):
                    new = result.new_chain[i].header_signature[:8]
                    num = result.new_chain[i].block_num
                LOGGER.info(
                    "Fork comparison at height %s is between %s and %s", num,
                    cur, new)

            commit_new_chain = self._compare_forks_consensus(chain_head, block)

            # If committing the new chain, get the list of committed batches
            # from the current chain that need to be uncommitted and the list
            # of uncommitted batches from the new chain that need to be
            # committed.
            if commit_new_chain:
                commit, uncommit =\
                    self._get_batch_commit_changes(
                        result.new_chain, result.current_chain)
                result.committed_batches = commit
                result.uncommitted_batches = uncommit

                if result.new_chain[0].previous_block_id \
                        != chain_head.identifier:
                    self._moved_to_fork_count.inc()

            # Pass the results to the callback function
            callback(commit_new_chain, result)
            LOGGER.info("Finished block validation of: %s", block)

        except ChainHeadUpdated:
            callback(False, result)
            return
        except Exception:  # pylint: disable=broad-except
            LOGGER.exception(
                "Block validation failed with unexpected error: %s", block)
            # callback to clean up the block out of the processing list.
            callback(False, result)
예제 #5
0
class TransactionExecutor(object):
    def __init__(self,
                 service,
                 context_manager,
                 settings_view_factory,
                 scheduler_type,
                 invalid_observers=None,
                 metrics_registry=None):
        """
        Args:
            service (Interconnect): The zmq internal interface
            context_manager (ContextManager): Cache of state for tps
            settings_view_factory (SettingsViewFactory): Read-only view of
                setting state.
        Attributes:
            processors (ProcessorIteratorCollection): All of the registered
                transaction processors and a way to find the next one to send
                to.
            _waiting_threadpool (ThreadPoolExecutor): A threadpool to run
                waiting to process transactions functions in.
            _waiters_by_type (_WaitersByType): Threadsafe map of ProcessorType
                to _Waiter that is waiting on a processor of that type.
        """
        self._service = service
        self._context_manager = context_manager
        self.processors = processor_iterator.ProcessorIteratorCollection(
            processor_iterator.RoundRobinProcessorIterator)
        self._settings_view_factory = settings_view_factory
        self._waiting_threadpool = InstrumentedThreadPoolExecutor(
            max_workers=MAX_WORKERS_WAIT,
            name='Waiting',
            metrics_registry=metrics_registry)
        self._executing_threadpool = InstrumentedThreadPoolExecutor(
            max_workers=MAX_WORKERS_EXEC,
            name='Executing',
            metrics_registry=metrics_registry)
        self._alive_threads = []
        self._lock = threading.Lock()

        self._invalid_observers = ([] if invalid_observers is None else
                                   invalid_observers)

        self._scheduler_type = scheduler_type
        self._metrics_registry = metrics_registry
        self._malicious = 0

    def set_malicious(self, mode=0):
        self._malicious = mode
        LOGGER.debug("set MALICIOUS=%s", mode)

    def create_scheduler(self,
                         squash_handler,
                         first_state_root,
                         context_handlers=None,
                         always_persist=False):
        LOGGER.debug("create scheduler_type=%s", self._scheduler_type)
        if self._scheduler_type == "serial":
            return SerialScheduler(squash_handler=squash_handler,
                                   first_state_hash=first_state_root,
                                   always_persist=always_persist,
                                   context_handlers=context_handlers)
        elif self._scheduler_type == "parallel":
            return ParallelScheduler(squash_handler=squash_handler,
                                     first_state_hash=first_state_root,
                                     always_persist=always_persist,
                                     context_handlers=context_handlers)

        else:
            raise AssertionError(
                "Scheduler type must be either serial or parallel. Current"
                " scheduler type is {}.".format(self._scheduler_type))

    def check_connections(self):
        self._executing_threadpool.submit(self._check_connections)

    def _remove_done_threads(self):
        for t in self._alive_threads.copy():
            if t.is_done():
                with self._lock:
                    self._alive_threads.remove(t)

    def _cancel_threads(self):
        for t in self._alive_threads:
            if not t.is_done():
                t.cancel()

    def _check_connections(self):
        # This is not ideal, because it locks up the current thread while
        # waiting for the results.
        try:
            with self._lock:
                futures = {}
                for connection_id in self.processors.get_all_processors():
                    fut = self._service.send(
                        validator_pb2.Message.PING_REQUEST,
                        network_pb2.PingRequest().SerializeToString(),
                        connection_id=connection_id)
                    futures[fut] = connection_id
                for fut in futures:
                    try:
                        fut.result(timeout=10)
                    except FutureTimeoutError:
                        LOGGER.info(
                            "%s did not respond to the Ping, removing transaction processor.",
                            futures[fut])
                        self._remove_broken_connection(futures[fut])
        except Exception:  # pylint: disable=broad-except
            LOGGER.exception('Unhandled exception while checking connections')

    def _remove_broken_connection(self, connection_id):
        for t in self._alive_threads:
            if not t.is_done():
                t.remove_broken_connection(connection_id)

    def execute(self, scheduler):
        self._remove_done_threads()
        t = TransactionExecutorThread(
            service=self._service,
            context_manager=self._context_manager,
            scheduler=scheduler,
            processors=self.processors,
            waiting_threadpool=self._waiting_threadpool,
            settings_view_factory=self._settings_view_factory,
            invalid_observers=self._invalid_observers,
            metrics_registry=self._metrics_registry,
            malicious=self._malicious)
        self._executing_threadpool.submit(t.execute_thread)
        with self._lock:
            LOGGER.debug('execute:append new thread num=%s\n',
                         len(self._alive_threads))
            self._alive_threads.append(t)

    def stop(self):
        self._cancel_threads()
        self._waiting_threadpool.shutdown(wait=True)
        self._executing_threadpool.shutdown(wait=True)
예제 #6
0
class ChainController(object):
    """
    To evaluating new blocks to determine if they should extend or replace
    the current chain. If they are valid extend the chain.
    """
    def __init__(self,
                 block_cache,
                 block_sender,
                 state_view_factory,
                 transaction_executor,
                 chain_head_lock,
                 on_chain_updated,
                 squash_handler,
                 chain_id_manager,
                 identity_signer,
                 data_dir,
                 config_dir,
                 permission_verifier,
                 chain_observers,
                 thread_pool=None,
                 metrics_registry=None):
        """Initialize the ChainController
        Args:
            block_cache: The cache of all recent blocks and the processing
                state associated with them.
            block_sender: an interface object used to send blocks to the
                network.
            state_view_factory: The factory object to create
            transaction_executor: The TransactionExecutor used to produce
                schedulers for batch validation.
            chain_head_lock: Lock to hold while the chain head is being
                updated, this prevents other components that depend on the
                chain head and the BlockStore from having the BlockStore change
                under them. This lock is only for core Journal components
                (BlockPublisher and ChainController), other components should
                handle block not found errors from the BlockStore explicitly.
            on_chain_updated: The callback to call to notify the rest of the
                 system the head block in the chain has been changed.
                 squash_handler: a parameter passed when creating transaction
                 schedulers.
            chain_id_manager: The ChainIdManager instance.
            identity_signer: Private key for signing blocks.
            data_dir: path to location where persistent data for the
                consensus module can be stored.
            config_dir: path to location where config data for the
                consensus module can be found.
            chain_observers (list of :obj:`ChainObserver`): A list of chain
                observers.
        Returns:
            None
        """
        self._lock = RLock()
        self._chain_head_lock = chain_head_lock
        self._block_cache = block_cache
        self._block_store = block_cache.block_store
        self._state_view_factory = state_view_factory
        self._block_sender = block_sender
        self._transaction_executor = transaction_executor
        self._notify_on_chain_updated = on_chain_updated
        self._squash_handler = squash_handler
        self._identity_signer = identity_signer
        self._data_dir = data_dir
        self._config_dir = config_dir

        self._blocks_processing = {}  # a set of blocks that are
        # currently being processed.
        self._blocks_pending = {}  # set of blocks that the previous block
        # is being processed. Once that completes this block will be
        # scheduled for validation.
        self._chain_id_manager = chain_id_manager

        self._chain_head = None

        self._permission_verifier = permission_verifier
        self._chain_observers = chain_observers

        if metrics_registry:
            self._chain_head_gauge = GaugeWrapper(
                metrics_registry.gauge('chain_head', default='no chain head'))
            self._committed_transactions_count = CounterWrapper(
                metrics_registry.counter('committed_transactions_count'))
            self._block_num_gauge = GaugeWrapper(
                metrics_registry.gauge('block_num'))
        else:
            self._chain_head_gauge = GaugeWrapper()
            self._committed_transactions_count = CounterWrapper()
            self._block_num_gauge = GaugeWrapper()

        self._block_queue = queue.Queue()
        self._thread_pool = \
            InstrumentedThreadPoolExecutor(1) \
            if thread_pool is None else thread_pool
        self._chain_thread = None

        # Only run this after all member variables have been bound
        self._set_chain_head_from_block_store()

    def _set_chain_head_from_block_store(self):
        try:
            self._chain_head = self._block_store.chain_head
            if self._chain_head is not None:
                LOGGER.info("Chain controller initialized with chain head: %s",
                            self._chain_head)
                self._chain_head_gauge.set_value(
                    self._chain_head.identifier[:8])
        except Exception:
            LOGGER.exception(
                "Invalid block store. Head of the block chain cannot be"
                " determined")
            raise

    def start(self):
        self._set_chain_head_from_block_store()
        self._notify_on_chain_updated(self._chain_head)

        self._chain_thread = _ChainThread(chain_controller=self,
                                          block_queue=self._block_queue,
                                          block_cache=self._block_cache)
        self._chain_thread.start()

    def stop(self):
        if self._chain_thread is not None:
            self._chain_thread.stop()
            self._chain_thread = None

        if self._thread_pool is not None:
            self._thread_pool.shutdown(wait=True)

    def queue_block(self, block):
        """
        New block has been received, queue it with the chain controller
        for processing.
        """
        self._block_queue.put(block)

    @property
    def chain_head(self):
        return self._chain_head

    def _submit_blocks_for_verification(self, blocks):
        for blkw in blocks:
            state_view = BlockWrapper.state_view_for_block(
                self.chain_head, self._state_view_factory)
            consensus_module = \
                ConsensusFactory.get_configured_consensus_module(
                    self.chain_head.header_signature,
                    state_view)

            validator = BlockValidator(
                consensus_module=consensus_module,
                new_block=blkw,
                block_cache=self._block_cache,
                state_view_factory=self._state_view_factory,
                done_cb=self.on_block_validated,
                executor=self._transaction_executor,
                squash_handler=self._squash_handler,
                identity_signer=self._identity_signer,
                data_dir=self._data_dir,
                config_dir=self._config_dir,
                permission_verifier=self._permission_verifier)
            self._blocks_processing[blkw.block.header_signature] = validator
            self._thread_pool.submit(validator.run)

    def on_block_validated(self, commit_new_block, result):
        """Message back from the block validator, that the validation is
        complete
        Args:
        commit_new_block (Boolean): whether the new block should become the
        chain head or not.
        result (Dict): Map of the results of the fork resolution.
        Returns:
            None
        """
        try:
            with self._lock:
                new_block = result["new_block"]
                LOGGER.info("on_block_validated: %s", new_block)

                # remove from the processing list
                del self._blocks_processing[new_block.identifier]

                # Remove this block from the pending queue, obtaining any
                # immediate descendants of this block in the process.
                descendant_blocks = \
                    self._blocks_pending.pop(new_block.identifier, [])

                # if the head has changed, since we started the work.
                if result["chain_head"].identifier !=\
                        self._chain_head.identifier:
                    LOGGER.info(
                        'Chain head updated from %s to %s while processing '
                        'block: %s', result["chain_head"], self._chain_head,
                        new_block)

                    # If any immediate descendant blocks arrived while this
                    # block was being processed, then submit them for
                    # verification.  Otherwise, add this block back to the
                    # pending queue and resubmit it for verification.
                    if descendant_blocks:
                        LOGGER.debug('Verify descendant blocks: %s (%s)',
                                     new_block, [
                                         block.identifier[:8]
                                         for block in descendant_blocks
                                     ])
                        self._submit_blocks_for_verification(descendant_blocks)
                    else:
                        LOGGER.debug('Verify block again: %s ', new_block)
                        self._blocks_pending[new_block.identifier] = []
                        self._submit_blocks_for_verification([new_block])

                # If the head is to be updated to the new block.
                elif commit_new_block:
                    with self._chain_head_lock:
                        self._chain_head = new_block

                        # update the the block store to have the new chain
                        self._block_store.update_chain(result["new_chain"],
                                                       result["cur_chain"])

                        # make sure old chain is in the block_caches
                        for block in result["cur_chain"]:
                            if block.header_signature not in self._block_cache:
                                self._block_cache[block.header_signature] = \
                                    block

                        LOGGER.info("Chain head updated to: %s",
                                    self._chain_head)

                        self._chain_head_gauge.set_value(
                            self._chain_head.identifier[:8])

                        self._committed_transactions_count.inc(
                            result["num_transactions"])

                        self._block_num_gauge.set_value(
                            self._chain_head.block_num)

                        # tell the BlockPublisher else the chain is updated
                        self._notify_on_chain_updated(
                            self._chain_head, result["committed_batches"],
                            result["uncommitted_batches"])

                        for batch in new_block.batches:
                            if batch.trace:
                                LOGGER.debug("TRACE %s: %s",
                                             batch.header_signature,
                                             self.__class__.__name__)

                    # Submit any immediate descendant blocks for verification
                    LOGGER.debug(
                        'Verify descendant blocks: %s (%s)', new_block,
                        [block.identifier[:8] for block in descendant_blocks])
                    self._submit_blocks_for_verification(descendant_blocks)

                    receipts = self._make_receipts(result["execution_results"])
                    # Update all chain observers
                    for observer in self._chain_observers:
                        observer.chain_update(new_block, receipts)

                # If the block was determine to be invalid.
                elif new_block.status == BlockStatus.Invalid:
                    # Since the block is invalid, we will never accept any
                    # blocks that are descendants of this block.  We are going
                    # to go through the pending blocks and remove all
                    # descendants we find and mark the corresponding block
                    # as invalid.
                    while descendant_blocks:
                        pending_block = descendant_blocks.pop()
                        pending_block.status = BlockStatus.Invalid

                        LOGGER.debug('Marking descendant block invalid: %s',
                                     pending_block)

                        descendant_blocks.extend(
                            self._blocks_pending.pop(pending_block.identifier,
                                                     []))

                # The block is otherwise valid, but we have determined we
                # don't want it as the chain head.
                else:
                    LOGGER.info('Rejected new chain head: %s', new_block)

                    # Submit for verification any immediate descendant blocks
                    # that arrived while we were processing this block.
                    LOGGER.debug(
                        'Verify descendant blocks: %s (%s)', new_block,
                        [block.identifier[:8] for block in descendant_blocks])
                    self._submit_blocks_for_verification(descendant_blocks)

        # pylint: disable=broad-except
        except Exception:
            LOGGER.exception(
                "Unhandled exception in ChainController.on_block_validated()")

    def on_block_received(self, block):
        try:
            with self._lock:
                if self.has_block(block.header_signature):
                    # do we already have this block
                    return

                if self.chain_head is None:
                    self._set_genesis(block)
                    return

                # If we are already currently processing this block, then
                # don't bother trying to schedule it again.
                if block.identifier in self._blocks_processing:
                    return

                self._block_cache[block.identifier] = block
                self._blocks_pending[block.identifier] = []
                LOGGER.debug("Block received: %s", block)
                if block.previous_block_id in self._blocks_processing or \
                        block.previous_block_id in self._blocks_pending:
                    LOGGER.debug('Block pending: %s', block)
                    # if the previous block is being processed, put it in a
                    # wait queue, Also need to check if previous block is
                    # in the wait queue.
                    pending_blocks = \
                        self._blocks_pending.get(block.previous_block_id,
                                                 [])
                    # Though rare, the block may already be in the
                    # pending_block list and should not be re-added.
                    if block not in pending_blocks:
                        pending_blocks.append(block)

                    self._blocks_pending[block.previous_block_id] = \
                        pending_blocks
                else:
                    # schedule this block for validation.
                    self._submit_blocks_for_verification([block])
        # pylint: disable=broad-except
        except Exception:
            LOGGER.exception(
                "Unhandled exception in ChainController.on_block_received()")

    def has_block(self, block_id):
        with self._lock:
            if block_id in self._block_cache:
                return True

            if block_id in self._blocks_processing:
                return True

            if block_id in self._blocks_pending:
                return True

            return False

    def _set_genesis(self, block):
        # This is used by a non-genesis journal when it has received the
        # genesis block from the genesis validator
        if block.previous_block_id == NULL_BLOCK_IDENTIFIER:
            chain_id = self._chain_id_manager.get_block_chain_id()
            if chain_id is not None and chain_id != block.identifier:
                LOGGER.warning(
                    "Block id does not match block chain id %s. "
                    "Cannot set initial chain head.: %s", chain_id[:8],
                    block.identifier[:8])
            else:
                state_view = self._state_view_factory.create_view()
                consensus_module = \
                    ConsensusFactory.get_configured_consensus_module(
                        NULL_BLOCK_IDENTIFIER,
                        state_view)

                validator = BlockValidator(
                    consensus_module=consensus_module,
                    new_block=block,
                    block_cache=self._block_cache,
                    state_view_factory=self._state_view_factory,
                    done_cb=self.on_block_validated,
                    executor=self._transaction_executor,
                    squash_handler=self._squash_handler,
                    identity_signer=self._identity_signer,
                    data_dir=self._data_dir,
                    config_dir=self._config_dir,
                    permission_verifier=self._permission_verifier)

                valid = validator.validate_block(block)
                if valid:
                    if chain_id is None:
                        self._chain_id_manager.save_block_chain_id(
                            block.identifier)
                    self._block_store.update_chain([block])
                    self._chain_head = block
                    self._notify_on_chain_updated(self._chain_head)
                else:
                    LOGGER.warning(
                        "The genesis block is not valid. Cannot "
                        "set chain head: %s", block)

        else:
            LOGGER.warning(
                "Cannot set initial chain head, this is not a "
                "genesis block: %s", block)

    def _make_receipts(self, results):
        receipts = []
        for result in results:
            receipt = TransactionReceipt()
            receipt.data.extend([data for data in result.data])
            receipt.state_changes.extend(result.state_changes)
            receipt.events.extend(result.events)
            receipt.transaction_id = result.signature
            receipts.append(receipt)
        return receipts
예제 #7
0
class BlockValidator:
    """
    Responsible for validating a block.
    """
    def __init__(self,
                 block_manager,
                 block_store,
                 state_view_factory,
                 transaction_executor,
                 identity_signer,
                 data_dir,
                 config_dir,
                 permission_verifier,
                 thread_pool=None):
        """Initialize the BlockValidator
        Args:
            block_manager: The component that stores blocks and maintains
                integrity of the predecessor relationship.
            state_view_factory: A factory that can be used to create read-
                only views of state for a particular merkle root, in
                particular the state as it existed when a particular block
                was the chain head.
            transaction_executor: The transaction executor used to
                process transactions.
            identity_signer: A cryptographic signer for signing blocks.
            data_dir: Path to location where persistent data for the
                consensus module can be stored.
            config_dir: Path to location where config data for the
                consensus module can be found.
            permission_verifier: The delegate for handling permission
                validation on blocks.
            thread_pool: (Optional) Executor pool used to submit block
                validation jobs. If not specified, a default will be created.
        Returns:
            None
        """

        self._block_manager = block_manager
        self._block_store = block_store
        self._block_validity_fn = None
        self._block_scheduler = BlockScheduler(block_manager)
        self._state_view_factory = state_view_factory
        self._transaction_executor = transaction_executor
        self._identity_signer = identity_signer
        self._data_dir = data_dir
        self._config_dir = config_dir
        self._permission_verifier = permission_verifier

        self._settings_view_factory = SettingsViewFactory(state_view_factory)

        self._thread_pool = InstrumentedThreadPoolExecutor(1) \
            if thread_pool is None else thread_pool

    def stop(self):
        self._thread_pool.shutdown(wait=True)

    def set_block_validity_fn(self, func):
        self._block_validity_fn = func
        self._block_scheduler.set_block_validity_fn(func=func)

    def _block_validity(self, block_id):
        return self._block_validity_fn(block_id)

    def _get_previous_block_state_root(self, block):
        block_header = BlockHeader()
        block_header.ParseFromString(block.header)

        if block_header.previous_block_id == NULL_BLOCK_IDENTIFIER:
            return INIT_ROOT_KEY
        try:
            block = next(
                self._block_manager.get([block_header.previous_block_id]))
        except StopIteration:
            return None
        block_header = BlockHeader()
        block_header.ParseFromString(block.header)
        return block_header.state_root_hash

    def _validate_batches_in_block(self, block, prev_state_root):
        """
        Validate all batches in the block. This includes:
            - Validating all transaction dependencies are met
            - Validating there are no duplicate batches or transactions
            - Validating execution of all batches in the block produces the
              correct state root hash

        Args:
            block: the block of batches to validate
            prev_state_root: the state root to execute transactions on top of

        Raises:
            BlockValidationFailure:
                If validation fails, raises this error with the reason.
            MissingDependency:
                Validation failed because of a missing dependency.
            DuplicateTransaction:
                Validation failed because of a duplicate transaction.
            DuplicateBatch:
                Validation failed because of a duplicate batch.
        """
        if not block.batches:
            return

        scheduler = None
        try:
            while True:
                try:
                    chain_head = self._block_store.chain_head

                    block_header = BlockHeader()
                    block_header.ParseFromString(block.header)

                    chain_commit_state = ChainCommitState(
                        block_header.previous_block_id, self._block_manager,
                        self._block_store)

                    chain_commit_state.check_for_duplicate_batches(
                        block.batches)

                    transactions = []
                    for batch in block.batches:
                        transactions.extend(batch.transactions)

                    chain_commit_state.check_for_duplicate_transactions(
                        transactions)

                    chain_commit_state.check_for_transaction_dependencies(
                        transactions)

                    if not self._check_chain_head_updated(chain_head, block):
                        break

                except (DuplicateBatch, DuplicateTransaction,
                        MissingDependency, BlockStoreUpdated) as err:
                    if not self._check_chain_head_updated(chain_head, block):
                        raise BlockValidationFailure(
                            "Block {} failed validation: {}".format(
                                block.header_signature, err))

            scheduler = self._transaction_executor.create_scheduler(
                prev_state_root)

            for batch, has_more in look_ahead(block.batches):
                if has_more:
                    scheduler.add_batch(batch)
                else:
                    scheduler.add_batch(batch, block_header.state_root_hash)

        except Exception:
            if scheduler is not None:
                scheduler.cancel()
            raise

        scheduler.finalize()
        scheduler.complete(block=True)
        state_hash = None

        execution_results = []
        num_transactions = 0

        for batch in block.batches:
            batch_result = scheduler.get_batch_execution_result(
                batch.header_signature)
            if batch_result is not None and batch_result.is_valid:
                txn_results = \
                    scheduler.get_transaction_execution_results(
                        batch.header_signature)
                execution_results.extend(txn_results)
                state_hash = batch_result.state_hash
                num_transactions += len(batch.transactions)
            else:
                raise BlockValidationFailure(
                    "Block {} failed validation: Invalid batch "
                    "{}".format(block.header_signature, batch))

        if block_header.state_root_hash != state_hash:
            raise BlockValidationFailure(
                "Block {} failed state root hash validation. Expected {}"
                " but got {}".format(block.header_signature,
                                     block_header.state_root_hash, state_hash))

        return execution_results, num_transactions

    def _check_chain_head_updated(self, chain_head, block):
        # The validity of blocks depends partially on whether or not
        # there are any duplicate transactions or batches in the block.
        # This can only be checked accurately if the block store does
        # not update during validation. The current practice is the
        # assume this will not happen and, if it does, to reprocess the
        # validation. This has been experimentally proven to be more
        # performant than locking the chain head and block store around
        # duplicate checking.
        if chain_head is None:
            return False

        current_chain_head = self._block_store.chain_head
        if chain_head.header_signature != current_chain_head.header_signature:
            LOGGER.warning(
                "Chain head updated from %s to %s while checking "
                "duplicates and dependencies in block %s. "
                "Reprocessing validation.", chain_head, current_chain_head,
                block)
            return True

        return False

    def _validate_permissions(self, block, prev_state_root):
        """
        Validate that all of the batch signers and transaction signer for the
        batches in the block are permitted by the transactor permissioning
        roles stored in state as of the previous block. If a transactor is
        found to not be permitted, the block is invalid.
        """

        block_header = BlockHeader()
        block_header.ParseFromString(block.header)

        if block_header.block_num != 0:
            for batch in block.batches:
                if not self._permission_verifier.is_batch_signer_authorized(
                        batch, prev_state_root, from_state=True):
                    return False
        return True

    def _validate_on_chain_rules(self, block, prev_state_root):
        """
        Validate that the block conforms to all validation rules stored in
        state. If the block breaks any of the stored rules, the block is
        invalid.
        """

        block_header = BlockHeader()
        block_header.ParseFromString(block.header)

        if block_header.block_num != 0:
            return enforce_validation_rules(
                self._settings_view_factory.create_settings_view(
                    prev_state_root), block_header.signer_public_key,
                block.batches)
        return True

    def validate_block(self, block):

        block_header = BlockHeader()
        block_header.ParseFromString(block.header)

        # pylint: disable=broad-except
        try:
            try:
                prev_block = next(
                    self._block_manager.get([block_header.previous_block_id]))
            except StopIteration:
                if block_header.previous_block_id != NULL_BLOCK_IDENTIFIER:
                    raise
            else:
                if self._block_validity(prev_block.header_signature) == \
                        BlockStatus.Invalid:
                    raise BlockValidationFailure(
                        "Block {} rejected due to invalid predecessor"
                        " {}".format(block.header_signature,
                                     prev_block.header_signature))
                elif self._block_validity(prev_block.header_signature) == \
                        BlockStatus.Unknown:
                    raise BlockValidationError(
                        "Attempted to validate block {} before its predecessor"
                        " {}".format(block.header_signature,
                                     prev_block.header_signature))

            try:
                prev_state_root = self._get_previous_block_state_root(block)
            except KeyError:
                raise BlockValidationError(
                    'Block {} rejected due to missing predecessor'.format(
                        block.header_signature))

            if not self._validate_permissions(block, prev_state_root):
                raise BlockValidationFailure(
                    'Block {} failed permission validation'.format(
                        block.header_signature))

            if not self._validate_on_chain_rules(block, prev_state_root):
                raise BlockValidationFailure(
                    'Block {} failed on-chain validation rules'.format(block))

            execution_results, num_txns = \
                self._validate_batches_in_block(block, prev_state_root)

            return BlockValidationResult(execution_results=execution_results,
                                         block_id=block.header_signature,
                                         num_transactions=num_txns,
                                         status=BlockStatus.Valid)

        except BlockValidationFailure as err:
            raise err

        except BlockValidationError as err:
            raise err

        except Exception as e:
            LOGGER.exception(
                "Unhandled exception BlockValidator.validate_block()")
            raise e

    def submit_blocks_for_verification(self, blocks, callback):
        ready = self._block_scheduler.schedule(blocks)
        for block in ready:
            # Schedule the block for processing
            self._thread_pool.submit(self.process_block_verification, block,
                                     callback)

    def process_pending(self, block, callback):
        """Removes the block from processing if it is already valid, and
        returns pending blocks that were waiting on the valid block.
        """

        ready = []
        block_id = block.header_signature
        if self._block_validity(block_id) == BlockStatus.Valid:
            ready.extend(self._block_scheduler.done(block))

        self.submit_blocks_for_verification(ready, callback)

    def has_block(self, block_id):
        return block_id in self._block_scheduler

    def return_block_failure(self, block):
        return BlockValidationResult(execution_results=[],
                                     num_transactions=0,
                                     status=BlockStatus.Invalid,
                                     block_id=block.header_signature)

    def process_block_verification(self, block, callback):
        """
        Main entry for Block Validation, Take a given candidate block
        and decide if it is valid then if it is valid determine if it should
        be the new head block. Returns the results to the ChainController
        so that the change over can be made if necessary.
        """

        result = None

        try:
            result = self.validate_block(block)
            LOGGER.info('Block %s passed validation', block.header_signature)
        except BlockValidationFailure as err:
            LOGGER.warning('Block %s failed validation: %s',
                           block.header_signature, err)
            result = self.return_block_failure(block)

        except BlockValidationError as err:
            LOGGER.error('Encountered an error while validating %s: %s',
                         block.header_signature, err)

        except Exception:  # pylint: disable=broad-except
            LOGGER.exception(
                "Block validation failed with unexpected error: %s",
                block.header_signature)

        if result:
            callback(result)
예제 #8
0
class BlockValidator(object):
    """
    Responsible for validating a block, handles both chain extensions and fork
    will determine if the new block should be the head of the chain and return
    the information necessary to do the switch if necessary.
    """

    def __init__(self,
                 block_cache,
                 state_view_factory,
                 transaction_executor,
                 squash_handler,
                 identity_signer,
                 data_dir,
                 config_dir,
                 permission_verifier,
                 metrics_registry=None,
                 thread_pool=None):
        """Initialize the BlockValidator
        Args:
            block_cache: The cache of all recent blocks and the processing
                state associated with them.
            state_view_factory: A factory that can be used to create read-
                only views of state for a particular merkle root, in
                particular the state as it existed when a particular block
                was the chain head.
            transaction_executor: The transaction executor used to
                process transactions.
            squash_handler: A parameter passed when creating transaction
                schedulers.
            identity_signer: A cryptographic signer for signing blocks.
            data_dir: Path to location where persistent data for the
                consensus module can be stored.
            config_dir: Path to location where config data for the
                consensus module can be found.
            permission_verifier: The delegate for handling permission
                validation on blocks.
            metrics_registry: (Optional) Pyformance metrics registry handle for
                creating new metrics.
            thread_pool: (Optional) Executor pool used to submit block
                validation jobs. If not specified, a default will be created.
        Returns:
            None
        """
        self._block_cache = block_cache
        self._state_view_factory = state_view_factory
        self._transaction_executor = transaction_executor
        self._squash_handler = squash_handler
        self._identity_signer = identity_signer
        self._data_dir = data_dir
        self._config_dir = config_dir
        self._permission_verifier = permission_verifier

        self._settings_view_factory = SettingsViewFactory(state_view_factory)

        self._thread_pool = InstrumentedThreadPoolExecutor(1) \
            if thread_pool is None else thread_pool

        if metrics_registry:
            self._moved_to_fork_count = CounterWrapper(
                metrics_registry.counter('chain_head_moved_to_fork_count'))
        else:
            self._moved_to_fork_count = CounterWrapper()

    def stop(self):
        self._thread_pool.shutdown(wait=True)

    def _get_previous_block_state_root(self, blkw):
        if blkw.previous_block_id == NULL_BLOCK_IDENTIFIER:
            return INIT_ROOT_KEY

        return self._block_cache[blkw.previous_block_id].state_root_hash

    @staticmethod
    def _validate_transactions_in_batch(batch, chain_commit_state):
        """Verify that all transactions in this batch are unique and that all
        transaction dependencies in this batch have been satisfied.

        :param batch: the batch to verify
        :param chain_commit_state: the current chain commit state to verify the
            batch against
        :return:
        Boolean: True if all dependencies are present and all transactions
        are unique.
        """
        for txn in batch.transactions:
            txn_hdr = TransactionHeader()
            txn_hdr.ParseFromString(txn.header)
            if chain_commit_state.has_transaction(txn.header_signature):
                LOGGER.debug(
                    "Batch invalid due to duplicate transaction: %s",
                    txn.header_signature[:8])
                return False
            for dep in txn_hdr.dependencies:
                if not chain_commit_state.has_transaction(dep):
                    LOGGER.debug(
                        "Batch invalid due to missing transaction dependency;"
                        " transaction %s depends on %s",
                        txn.header_signature[:8], dep[:8])
                    return False
        return True

    def _validate_batches_in_block(
        self, blkw, prev_state_root, chain_commit_state
    ):
        if blkw.block.batches:
            scheduler = self._transaction_executor.create_scheduler(
                self._squash_handler, prev_state_root)
            self._transaction_executor.execute(scheduler)
            try:
                for batch, has_more in look_ahead(blkw.block.batches):
                    if chain_commit_state.has_batch(
                            batch.header_signature):
                        LOGGER.debug("Block(%s) rejected due to duplicate "
                                     "batch, batch: %s", blkw,
                                     batch.header_signature[:8])
                        raise InvalidBatch()

                    # Verify dependencies and uniqueness
                    if self._validate_transactions_in_batch(
                        batch, chain_commit_state
                    ):
                        # Only add transactions to commit state if all
                        # transactions in the batch are good.
                        chain_commit_state.add_batch(
                            batch, add_transactions=True)
                    else:
                        raise InvalidBatch()

                    if has_more:
                        scheduler.add_batch(batch)
                    else:
                        scheduler.add_batch(batch, blkw.state_root_hash)
            except InvalidBatch:
                LOGGER.debug("Invalid batch %s encountered during "
                             "verification of block %s",
                             batch.header_signature[:8],
                             blkw)
                scheduler.cancel()
                return False
            except Exception:
                scheduler.cancel()
                raise

            scheduler.finalize()
            scheduler.complete(block=True)
            state_hash = None

            for batch in blkw.batches:
                batch_result = scheduler.get_batch_execution_result(
                    batch.header_signature)
                if batch_result is not None and batch_result.is_valid:
                    txn_results = \
                        scheduler.get_transaction_execution_results(
                            batch.header_signature)
                    blkw.execution_results.extend(txn_results)
                    state_hash = batch_result.state_hash
                    blkw.num_transactions += len(batch.transactions)
                else:
                    return False
            if blkw.state_root_hash != state_hash:
                LOGGER.debug("Block(%s) rejected due to state root hash "
                             "mismatch: %s != %s", blkw, blkw.state_root_hash,
                             state_hash)
                return False
        return True

    def _validate_permissions(self, blkw, prev_state_root):
        """
        Validate that all of the batch signers and transaction signer for the
        batches in the block are permitted by the transactor permissioning
        roles stored in state as of the previous block. If a transactor is
        found to not be permitted, the block is invalid.
        """
        if blkw.block_num != 0:
            for batch in blkw.batches:
                if not self._permission_verifier.is_batch_signer_authorized(
                        batch, prev_state_root, from_state=True):
                    return False
        return True

    def _validate_on_chain_rules(self, blkw, prev_state_root):
        """
        Validate that the block conforms to all validation rules stored in
        state. If the block breaks any of the stored rules, the block is
        invalid.
        """
        if blkw.block_num != 0:
            return enforce_validation_rules(
                self._settings_view_factory.create_settings_view(
                    prev_state_root),
                blkw.header.signer_public_key,
                blkw.batches)
        return True

    def validate_block(self, blkw, consensus, chain_head=None, chain=None):
        if blkw.status == BlockStatus.Valid:
            return True
        elif blkw.status == BlockStatus.Invalid:
            return False

        # pylint: disable=broad-except
        try:
            if chain_head is None:
                # Try to get the chain head from the block store; note that the
                # block store may also return None for the chain head if a
                # genesis block hasn't been committed yet.
                chain_head = self._block_cache.block_store.chain_head

            if chain is None:
                chain = []
            chain_commit_state = ChainCommitState(
                self._block_cache.block_store, chain)

            try:
                prev_state_root = self._get_previous_block_state_root(blkw)
            except KeyError:
                LOGGER.debug(
                    "Block rejected due to missing predecessor: %s", blkw)
                return False

            if not self._validate_permissions(blkw, prev_state_root):
                blkw.status = BlockStatus.Invalid
                return False

            public_key = \
                self._identity_signer.get_public_key().as_hex()
            consensus_block_verifier = consensus.BlockVerifier(
                block_cache=self._block_cache,
                state_view_factory=self._state_view_factory,
                data_dir=self._data_dir,
                config_dir=self._config_dir,
                validator_id=public_key)

            if not consensus_block_verifier.verify_block(blkw):
                blkw.status = BlockStatus.Invalid
                return False

            if not self._validate_on_chain_rules(blkw, prev_state_root):
                blkw.status = BlockStatus.Invalid
                return False

            if not self._validate_batches_in_block(
                blkw, prev_state_root, chain_commit_state
            ):
                blkw.status = BlockStatus.Invalid
                return False

            # since changes to the chain-head can change the state of the
            # blocks in BlockStore we have to revalidate this block.
            block_store = self._block_cache.block_store

            # The chain_head is None when this is the genesis block or if the
            # block store has no chain_head.
            if chain_head is not None:
                if chain_head.identifier != block_store.chain_head.identifier:
                    raise ChainHeadUpdated()

            blkw.status = BlockStatus.Valid
            return True

        except ChainHeadUpdated as e:
            raise e

        except Exception:
            LOGGER.exception(
                "Unhandled exception BlockPublisher.validate_block()")
            return False

    @staticmethod
    def _compare_chain_height(head_a, head_b):
        """Returns True if head_a is taller, False if head_b is taller, and
        True if the heights are the same."""
        return head_a.block_num - head_b.block_num >= 0

    def _build_fork_diff_to_common_height(self, head_long, head_short):
        """Returns a list of blocks on the longer chain since the greatest
        common height between the two chains. Note that the chains may not
        have the same block id at the greatest common height.

        Args:
            head_long (BlockWrapper)
            head_short (BlockWrapper)

        Returns:
            (list of BlockWrapper) All blocks in the longer chain since the
            last block in the shorter chain. Ordered newest to oldest.

        Raises:
            BlockValidationAborted
                The block is missing a predecessor. Note that normally this
                shouldn't happen because of the completer."""
        fork_diff = []

        last = head_short.block_num
        blk = head_long

        while blk.block_num > last:
            if blk.previous_block_id == NULL_BLOCK_IDENTIFIER:
                break

            fork_diff.append(blk)
            try:
                blk = self._block_cache[blk.previous_block_id]
            except KeyError:
                LOGGER.debug(
                    "Failed to build fork diff due to missing predecessor: %s",
                    blk)

                # Mark all blocks in the longer chain since the invalid block
                # as invalid.
                for blk in fork_diff:
                    blk.status = BlockStatus.Invalid
                raise BlockValidationAborted()

        return blk, fork_diff

    def _extend_fork_diff_to_common_ancestor(
        self, new_blkw, cur_blkw, new_chain, cur_chain
    ):
        """ Finds a common ancestor of the two chains. new_blkw and cur_blkw
        must be at the same height, or this will always fail.
        """
        while cur_blkw.identifier != new_blkw.identifier:
            if (cur_blkw.previous_block_id == NULL_BLOCK_IDENTIFIER
                    or new_blkw.previous_block_id == NULL_BLOCK_IDENTIFIER):
                # We are at a genesis block and the blocks are not the same
                LOGGER.info(
                    "Block rejected due to wrong genesis: %s %s",
                    cur_blkw, new_blkw)
                for b in new_chain:
                    b.status = BlockStatus.Invalid
                raise BlockValidationAborted()

            new_chain.append(new_blkw)
            try:
                new_blkw = self._block_cache[new_blkw.previous_block_id]
            except KeyError:
                LOGGER.info(
                    "Block %s rejected due to missing predecessor %s",
                    new_blkw,
                    new_blkw.previous_block_id)
                for b in new_chain:
                    b.status = BlockStatus.Invalid
                raise BlockValidationAborted()

            cur_chain.append(cur_blkw)
            cur_blkw = self._block_cache[cur_blkw.previous_block_id]

    def _compare_forks_consensus(self, consensus, chain_head, new_block):
        """Ask the consensus module which fork to choose.
        """
        public_key = self._identity_signer.get_public_key().as_hex()
        fork_resolver = consensus.ForkResolver(
            block_cache=self._block_cache,
            state_view_factory=self._state_view_factory,
            data_dir=self._data_dir,
            config_dir=self._config_dir,
            validator_id=public_key)

        return fork_resolver.compare_forks(chain_head, new_block)

    @staticmethod
    def _get_batch_commit_changes(new_chain, cur_chain):
        """
        Get all the batches that should be committed from the new chain and
        all the batches that should be uncommitted from the current chain.
        """
        committed_batches = []
        for blkw in new_chain:
            for batch in blkw.batches:
                committed_batches.append(batch)

        uncommitted_batches = []
        for blkw in cur_chain:
            for batch in blkw.batches:
                uncommitted_batches.append(batch)

        return (committed_batches, uncommitted_batches)

    def submit_blocks_for_verification(
        self, blocks, consensus, callback
    ):
        for block in blocks:
            self._thread_pool.submit(
                self.process_block_verification,
                block, consensus, callback)

    def process_block_verification(self, block, consensus, callback):
        """
        Main entry for Block Validation, Take a given candidate block
        and decide if it is valid then if it is valid determine if it should
        be the new head block. Returns the results to the ChainController
        so that the change over can be made if necessary.
        """
        try:
            result = BlockValidationResult(block)
            LOGGER.info("Starting block validation of : %s", block)

            # Get the current chain_head and store it in the result
            chain_head = self._block_cache.block_store.chain_head
            result.chain_head = chain_head

            # Create new local variables for current and new block, since
            # these variables get modified later
            current_block = chain_head
            new_block = block

            # Get all the blocks since the greatest common height from the
            # longer chain.
            if self._compare_chain_height(current_block, new_block):
                current_block, result.current_chain =\
                    self._build_fork_diff_to_common_height(
                        current_block, new_block)
            else:
                new_block, result.new_chain =\
                    self._build_fork_diff_to_common_height(
                        new_block, current_block)

            # Add blocks to the two chains until a common ancestor is found
            # or raise an exception if no common ancestor is found
            self._extend_fork_diff_to_common_ancestor(
                new_block, current_block,
                result.new_chain, result.current_chain)

            valid = True
            for blk in reversed(result.new_chain):
                if valid:
                    if not self.validate_block(
                        blk, consensus, chain_head,
                        result.current_chain
                    ):
                        LOGGER.info("Block validation failed: %s", blk)
                        valid = False
                    result.transaction_count += block.num_transactions
                else:
                    LOGGER.info(
                        "Block marked invalid(invalid predecessor): %s", blk)
                    blk.status = BlockStatus.Invalid

            if not valid:
                callback(False, result)
                return

            # Ask consensus if the new chain should be committed
            LOGGER.info(
                "Comparing current chain head '%s' against new block '%s'",
                chain_head, new_block)
            for i in range(max(
                len(result.new_chain), len(result.current_chain)
            )):
                cur = new = num = "-"
                if i < len(result.current_chain):
                    cur = result.current_chain[i].header_signature[:8]
                    num = result.current_chain[i].block_num
                if i < len(result.new_chain):
                    new = result.new_chain[i].header_signature[:8]
                    num = result.new_chain[i].block_num
                LOGGER.info(
                    "Fork comparison at height %s is between %s and %s",
                    num, cur, new)

            commit_new_chain = self._compare_forks_consensus(
                consensus, chain_head, block)

            # If committing the new chain, get the list of committed batches
            # from the current chain that need to be uncommitted and the list
            # of uncommitted batches from the new chain that need to be
            # committed.
            if commit_new_chain:
                commit, uncommit =\
                    self._get_batch_commit_changes(
                        result.new_chain, result.current_chain)
                result.committed_batches = commit
                result.uncommitted_batches = uncommit

                if result.new_chain[0].previous_block_id \
                        != chain_head.identifier:
                    self._moved_to_fork_count.inc()

            # Pass the results to the callback function
            callback(commit_new_chain, result)
            LOGGER.info("Finished block validation of: %s", block)

        except BlockValidationAborted:
            callback(False, result)
            return
        except ChainHeadUpdated:
            callback(False, result)
            return
        except Exception:  # pylint: disable=broad-except
            LOGGER.exception(
                "Block validation failed with unexpected error: %s", block)
            # callback to clean up the block out of the processing list.
            callback(False, result)
예제 #9
0
class Interconnect(object):
    def __init__(self,
                 endpoint,
                 dispatcher,
                 zmq_identity=None,
                 secured=False,
                 server_public_key=None,
                 server_private_key=None,
                 heartbeat=False,
                 public_endpoint=None,
                 connection_timeout=60,
                 max_incoming_connections=100,
                 monitor=False,
                 max_future_callback_workers=10,
                 roles=None,
                 authorize=False,
                 public_key=None,
                 priv_key=None,
                 metrics_registry=None):
        """
        Constructor for Interconnect.

        Args:
            secured (bool): Whether or not to start the 'server' socket
                and associated Connection sockets in secure mode --
                using zmq auth.
            server_public_key (bytes): A public key to use in verifying
                server identity as part of the zmq auth handshake.
            server_private_key (bytes): A private key corresponding to
                server_public_key used by the server socket to sign
                messages are part of the zmq auth handshake.
            heartbeat (bool): Whether or not to send ping messages.
            max_future_callback_workers (int): max number of workers for future
                callbacks, defaults to 10
        """
        self._endpoint = endpoint
        self._public_endpoint = public_endpoint
        self._future_callback_threadpool = InstrumentedThreadPoolExecutor(
            max_workers=max_future_callback_workers, name='FutureCallback')
        self._futures = future.FutureCollection(
            resolving_threadpool=self._future_callback_threadpool)
        self._dispatcher = dispatcher
        self._zmq_identity = zmq_identity
        self._secured = secured
        self._server_public_key = server_public_key
        self._server_private_key = server_private_key
        self._heartbeat = heartbeat
        self._connection_timeout = connection_timeout
        self._connections_lock = Lock()
        self._connections = {}
        self.outbound_connections = {}
        self._max_incoming_connections = max_incoming_connections
        self._roles = {}
        # if roles are None, default to AuthorizationType.TRUST
        if roles is None:
            self._roles["network"] = AuthorizationType.TRUST
        else:
            self._roles = {}
            for role in roles:
                if roles[role] == "challenge":
                    self._roles[role] = AuthorizationType.CHALLENGE
                else:
                    self._roles[role] = AuthorizationType.TRUST

        self._authorize = authorize
        self._public_key = public_key
        self._priv_key = priv_key

        self._send_receive_thread = _SendReceive(
            "ServerThread",
            connections=self._connections,
            address=endpoint,
            dispatcher=dispatcher,
            futures=self._futures,
            secured=secured,
            server_public_key=server_public_key,
            server_private_key=server_private_key,
            heartbeat=heartbeat,
            connection_timeout=connection_timeout,
            monitor=monitor,
            metrics_registry=metrics_registry)

        self._thread = None

        self._metrics_registry = metrics_registry
        self._send_response_timers = {}

    @property
    def roles(self):
        return self._roles

    @property
    def endpoint(self):
        return self._endpoint

    def _get_send_response_timer(self, tag):
        if tag not in self._send_response_timers:
            if self._metrics_registry:
                self._send_response_timers[tag] = TimerWrapper(
                    self._metrics_registry.timer(
                        'interconnect_send_response_time',
                        tags=['message_type={}'.format(tag)]))
            else:
                self._send_response_timers[tag] = TimerWrapper()
        return self._send_response_timers[tag]

    def connection_id_to_public_key(self, connection_id):
        """
        Get stored public key for a connection.
        """
        if connection_id in self._connections:
            connection_info = self._connections[connection_id]
            return connection_info.public_key
        return None

    def connection_id_to_endpoint(self, connection_id):
        """
        Get stored public key for a connection.
        """
        if connection_id in self._connections:
            connection_info = self._connections[connection_id]
            return connection_info.uri
        return None

    def get_connection_status(self, connection_id):
        """
        Get status of the connection during Role enforcement.
        """
        if connection_id in self._connections:
            connection_info = self._connections[connection_id]
            return connection_info.status
        return None

    def set_check_connections(self, function):
        self._send_receive_thread.set_check_connections(function)

    def allow_inbound_connection(self):
        """Determines if an additional incoming network connection
        should be permitted.

        Returns:
            bool
        """
        LOGGER.debug(
            "Determining whether inbound connection should "
            "be allowed. num connections: %s max %s", len(self._connections),
            self._max_incoming_connections)
        return self._max_incoming_connections >= len(self._connections)

    def add_outbound_connection(self, uri):
        """Adds an outbound connection to the network.

        Args:
            uri (str): The zmq-style (e.g. tcp://hostname:port) uri
                to attempt to connect to.
        """
        LOGGER.debug("Adding connection to %s", uri)
        conn = OutboundConnection(
            connections=self._connections,
            endpoint=uri,
            dispatcher=self._dispatcher,
            zmq_identity=self._zmq_identity,
            secured=self._secured,
            server_public_key=self._server_public_key,
            server_private_key=self._server_private_key,
            future_callback_threadpool=self._future_callback_threadpool,
            heartbeat=True,
            connection_timeout=self._connection_timeout,
            metrics_registry=self._metrics_registry)

        self.outbound_connections[uri] = conn
        conn.start()

        self._add_connection(conn, uri)

        connect_message = ConnectionRequest(endpoint=self._public_endpoint)
        conn.send(validator_pb2.Message.NETWORK_CONNECT,
                  connect_message.SerializeToString(),
                  callback=partial(
                      self._connect_callback,
                      connection=conn,
                  ))

        return conn

    def send_connect_request(self, connection_id):
        """
        Send ConnectionRequest to an inbound connection. This allows
        the validator to be authorized by the incoming connection.
        """
        connect_message = ConnectionRequest(endpoint=self._public_endpoint)
        self.send(validator_pb2.Message.NETWORK_CONNECT,
                  connect_message.SerializeToString(),
                  connection_id,
                  callback=partial(self._inbound_connection_request_callback,
                                   connection_id=connection_id))

    def _connect_callback(self, request, result, connection=None):
        connection_response = ConnectionResponse()
        connection_response.ParseFromString(result.content)

        if connection_response.status == connection_response.ERROR:
            LOGGER.debug(
                "Received an error response to the NETWORK_CONNECT "
                "we sent. Removing connection: %s", connection.connection_id)
            self.remove_connection(connection.connection_id)
        elif connection_response.status == connection_response.OK:

            LOGGER.debug("Connection to %s was acknowledged",
                         connection.connection_id)
            if self._authorize:
                # Send correct Authorization Request for network role
                auth_type = {"trust": [], "challenge": []}
                for role_entry in connection_response.roles:
                    if role_entry.auth_type == connection_response.TRUST:
                        auth_type["trust"].append(role_entry.role)
                    elif role_entry.auth_type == connection_response.CHALLENGE:
                        auth_type["challenge"].append(role_entry.role)

                if auth_type["trust"]:
                    auth_trust_request = AuthorizationTrustRequest(
                        roles=auth_type["trust"], public_key=self._public_key)
                    connection.send(
                        validator_pb2.Message.AUTHORIZATION_TRUST_REQUEST,
                        auth_trust_request.SerializeToString())

                if auth_type["challenge"]:
                    auth_challenge_request = AuthorizationChallengeRequest()
                    connection.send(
                        validator_pb2.Message.AUTHORIZATION_CHALLENGE_REQUEST,
                        auth_challenge_request.SerializeToString(),
                        callback=partial(
                            self._challenge_authorization_callback,
                            connection=connection))

    def _inbound_connection_request_callback(self,
                                             request,
                                             result,
                                             connection_id=None):
        connection_response = ConnectionResponse()
        connection_response.ParseFromString(result.content)
        if connection_response.status == connection_response.ERROR:
            LOGGER.debug(
                "Received an error response to the NETWORK_CONNECT "
                "we sent. Removing connection: %s", connection_id)
            self.remove_connection(connection_id)

        # Send correct Authorization Request for network role
        auth_type = {"trust": [], "challenge": []}
        for role_entry in connection_response.roles:
            if role_entry.auth_type == connection_response.TRUST:
                auth_type["trust"].append(role_entry.role)
            elif role_entry.auth_type == connection_response.CHALLENGE:
                auth_type["challenge"].append(role_entry.role)

        if auth_type["trust"]:
            auth_trust_request = AuthorizationTrustRequest(
                roles=[RoleType.Value("NETWORK")], public_key=self._public_key)
            self.send(validator_pb2.Message.AUTHORIZATION_TRUST_REQUEST,
                      auth_trust_request.SerializeToString(), connection_id)

        if auth_type["challenge"]:
            auth_challenge_request = AuthorizationChallengeRequest()
            self.send(validator_pb2.Message.AUTHORIZATION_CHALLENGE_REQUEST,
                      auth_challenge_request.SerializeToString(),
                      connection_id,
                      callback=partial(
                          self._inbound_challenge_authorization_callback,
                          connection_id=connection_id))

    def _challenge_authorization_callback(
        self,
        request,
        result,
        connection=None,
    ):
        if result.message_type != \
                validator_pb2.Message.AUTHORIZATION_CHALLENGE_RESPONSE:
            LOGGER.debug("Unable to complete Challenge Authorization.")
            return

        auth_challenge_response = AuthorizationChallengeResponse()
        auth_challenge_response.ParseFromString(result.content)
        payload = auth_challenge_response.payload
        signature = signing.sign(payload, self._priv_key)

        auth_challenge_submit = AuthorizationChallengeSubmit(
            public_key=self._public_key,
            payload=payload,
            signature=signature,
            roles=[RoleType.Value("NETWORK")])

        connection.send(validator_pb2.Message.AUTHORIZATION_CHALLENGE_SUBMIT,
                        auth_challenge_submit.SerializeToString())

    def _inbound_challenge_authorization_callback(self,
                                                  request,
                                                  result,
                                                  connection_id=None):
        if result.message_type != \
                validator_pb2.Message.AUTHORIZATION_CHALLENGE_RESPONSE:
            LOGGER.debug("Unable to complete Challenge Authorization.")
            return

        auth_challenge_response = AuthorizationChallengeResponse()
        auth_challenge_response.ParseFromString(result.content)
        payload = auth_challenge_response.payload
        signature = signing.sign(payload, self._priv_key)

        auth_challenge_submit = AuthorizationChallengeSubmit(
            public_key=self._public_key,
            payload=payload,
            signature=signature,
            roles=[RoleType.Value("NETWORK")])

        self.send(validator_pb2.Message.AUTHORIZATION_CHALLENGE_SUBMIT,
                  auth_challenge_submit.SerializeToString(), connection_id)

    def send(self, message_type, data, connection_id, callback=None):
        """
        Send a message of message_type
        :param connection_id: the identity for the connection to send to
        :param message_type: validator_pb2.Message.* enum value
        :param data: bytes serialized protobuf
        :return: future.Future
        """
        if connection_id not in self._connections:
            raise ValueError("Unknown connection id: %s", connection_id)
        connection_info = self._connections.get(connection_id)
        if connection_info.connection_type == \
                ConnectionType.ZMQ_IDENTITY:
            message = validator_pb2.Message(correlation_id=_generate_id(),
                                            content=data,
                                            message_type=message_type)

            timer_tag = get_enum_name(message.message_type)
            timer_ctx = self._get_send_response_timer(timer_tag).time()
            fut = future.Future(message.correlation_id,
                                message.content,
                                callback,
                                timer_ctx=timer_ctx)

            self._futures.put(fut)

            self._send_receive_thread.send_message(msg=message,
                                                   connection_id=connection_id)
            return fut

        return connection_info.connection.send(message_type,
                                               data,
                                               callback=callback)

    def start(self):
        complete_or_error_queue = queue.Queue()
        self._thread = InstrumentedThread(
            target=self._send_receive_thread.setup,
            args=(zmq.ROUTER, complete_or_error_queue))
        self._thread.name = self.__class__.__name__ + self._thread.name
        self._thread.start()
        # Blocking in startup until the background thread has made it to
        # running the event loop or error.
        err = complete_or_error_queue.get(block=True)
        if err != _STARTUP_COMPLETE_SENTINEL:
            raise err

    def stop(self):
        self._send_receive_thread.shutdown()
        for conn in self.outbound_connections.values():
            conn.stop()
        self._future_callback_threadpool.shutdown(wait=True)

    def get_connection_id_by_endpoint(self, endpoint):
        """Returns the connection id associated with a publically
        reachable endpoint or raises KeyError if the endpoint is not
        found.

        Args:
            endpoint (str): A zmq-style uri which identifies a publically
                reachable endpoint.
        """
        for connection_id in self._connections:
            connection_info = self._connections[connection_id]
            if connection_info.uri == endpoint:
                return connection_id
        raise KeyError()

    def update_connection_endpoint(self, connection_id, endpoint):
        """Adds the endpoint to the connection definition. When the
        connection is created by the send/receive thread, we do not
        yet have the endpoint of the remote node. That is not known
        until we process the incoming ConnectRequest.

        Args:
            connection_id (str): The identifier for the connection.
            endpoint (str): A zmq-style uri which identifies a publically
                reachable endpoint.
        """
        if connection_id in self._connections:
            connection_info = self._connections[connection_id]
            self._connections[connection_id] = \
                ConnectionInfo(connection_info.connection_type,
                               connection_info.connection,
                               endpoint,
                               connection_info.status,
                               connection_info.public_key)
        else:
            LOGGER.debug(
                "Could not update the endpoint %s for "
                "connection_id %s. The connection does not "
                "exist.", endpoint, connection_id)

    def update_connection_public_key(self, connection_id, public_key):
        """Adds the public_key to the connection definition.

        Args:
            connection_id (str): The identifier for the connection.
            public_key (str): The public key used to enforce permissions on
                connections.

        """
        if connection_id in self._connections:
            connection_info = self._connections[connection_id]
            self._connections[connection_id] = \
                ConnectionInfo(connection_info.connection_type,
                               connection_info.connection,
                               connection_info.uri,
                               connection_info.status,
                               public_key)
        else:
            LOGGER.debug(
                "Could not update the public key %s for "
                "connection_id %s. The connection does not "
                "exist.", public_key, connection_id)

    def update_connection_status(self, connection_id, status):
        """Adds a status to the connection definition. This allows the handlers
        to ensure that the connection is following the steps in order for
        authorization enforement.

        Args:
            connection_id (str): The identifier for the connection.
            status (ConnectionStatus): An enum value for the stage of
                authortization the connection is at.

        """
        if connection_id in self._connections:
            connection_info = self._connections[connection_id]
            self._connections[connection_id] = \
                ConnectionInfo(connection_info.connection_type,
                               connection_info.connection,
                               connection_info.uri,
                               status,
                               connection_info.public_key)
        else:
            LOGGER.debug(
                "Could not update the status to %s for "
                "connection_id %s. The connection does not "
                "exist.", status, connection_id)

    def _add_connection(self, connection, uri=None):
        with self._connections_lock:
            connection_id = connection.connection_id
            if connection_id not in self._connections:
                self._connections[connection_id] = \
                    ConnectionInfo(ConnectionType.OUTBOUND_CONNECTION,
                                   connection,
                                   uri,
                                   None,
                                   None)

    def remove_connection(self, connection_id):
        with self._connections_lock:
            LOGGER.debug("Removing connection: %s", connection_id)
            if connection_id in self._connections:
                connection_info = self._connections[connection_id]

                if connection_info.connection_type == \
                        ConnectionType.OUTBOUND_CONNECTION:
                    connection_info.connection.stop()
                    del self._connections[connection_id]

                elif connection_info.connection_type == \
                        ConnectionType.ZMQ_IDENTITY:
                    self._send_receive_thread.remove_connected_identity(
                        connection_info.connection)

    def send_last_message(self,
                          message_type,
                          data,
                          connection_id,
                          callback=None):
        """
        Send a message of message_type and close the connection.
        :param connection_id: the identity for the connection to send to
        :param message_type: validator_pb2.Message.* enum value
        :param data: bytes serialized protobuf
        :return: future.Future
        """
        if connection_id not in self._connections:
            raise ValueError("Unknown connection id: %s", connection_id)
        connection_info = self._connections.get(connection_id)
        if connection_info.connection_type == \
                ConnectionType.ZMQ_IDENTITY:
            message = validator_pb2.Message(correlation_id=_generate_id(),
                                            content=data,
                                            message_type=message_type)

            fut = future.Future(message.correlation_id, message.content,
                                callback)

            self._futures.put(fut)

            self._send_receive_thread.send_last_message(
                msg=message, connection_id=connection_id)
            return fut

        del self._connections[connection_id]
        return connection_info.connection.send_last_message(message_type,
                                                            data,
                                                            callback=callback)

    def has_connection(self, connection_id):
        if connection_id in self._connections:
            return True
        return False

    def is_outbound_connection(self, connection_id):
        connection_info = self._connections[connection_id]
        if connection_info.connection_type == \
                ConnectionType.OUTBOUND_CONNECTION:
            return True
        return False
예제 #10
0
class BlockValidator:
    """
    Responsible for validating a block.
    """
    def __init__(self,
                 block_cache,
                 state_view_factory,
                 transaction_executor,
                 identity_signer,
                 data_dir,
                 config_dir,
                 permission_verifier,
                 thread_pool=None):
        """Initialize the BlockValidator
        Args:
            block_cache: The cache of all recent blocks and the processing
                state associated with them.
            state_view_factory: A factory that can be used to create read-
                only views of state for a particular merkle root, in
                particular the state as it existed when a particular block
                was the chain head.
            transaction_executor: The transaction executor used to
                process transactions.
            identity_signer: A cryptographic signer for signing blocks.
            data_dir: Path to location where persistent data for the
                consensus module can be stored.
            config_dir: Path to location where config data for the
                consensus module can be found.
            permission_verifier: The delegate for handling permission
                validation on blocks.
            thread_pool: (Optional) Executor pool used to submit block
                validation jobs. If not specified, a default will be created.
        Returns:
            None
        """
        self._block_cache = block_cache
        self._state_view_factory = state_view_factory
        self._transaction_executor = transaction_executor
        self._identity_signer = identity_signer
        self._data_dir = data_dir
        self._config_dir = config_dir
        self._permission_verifier = permission_verifier

        self._settings_view_factory = SettingsViewFactory(state_view_factory)

        self._thread_pool = InstrumentedThreadPoolExecutor(1) \
            if thread_pool is None else thread_pool

        self._block_scheduler = BlockScheduler(block_cache)

    def stop(self):
        self._thread_pool.shutdown(wait=True)

    def _get_previous_block_state_root(self, blkw):
        if blkw.previous_block_id == NULL_BLOCK_IDENTIFIER:
            return INIT_ROOT_KEY

        return self._block_cache[blkw.previous_block_id].state_root_hash

    def _validate_batches_in_block(self, blkw, prev_state_root):
        """
        Validate all batches in the block. This includes:
            - Validating all transaction dependencies are met
            - Validating there are no duplicate batches or transactions
            - Validating execution of all batches in the block produces the
              correct state root hash

        Args:
            blkw: the block of batches to validate
            prev_state_root: the state root to execute transactions on top of

        Raises:
            BlockValidationFailure:
                If validation fails, raises this error with the reason.
            MissingDependency:
                Validation failed because of a missing dependency.
            DuplicateTransaction:
                Validation failed because of a duplicate transaction.
            DuplicateBatch:
                Validation failed because of a duplicate batch.
        """
        if not blkw.block.batches:
            return

        scheduler = None
        try:
            while True:
                try:
                    chain_head = self._block_cache.block_store.chain_head

                    chain_commit_state = ChainCommitState(
                        blkw.previous_block_id, self._block_cache,
                        self._block_cache.block_store)

                    chain_commit_state.check_for_duplicate_batches(
                        blkw.block.batches)

                    transactions = []
                    for batch in blkw.block.batches:
                        transactions.extend(batch.transactions)

                    chain_commit_state.check_for_duplicate_transactions(
                        transactions)

                    chain_commit_state.check_for_transaction_dependencies(
                        transactions)

                    if not self._check_chain_head_updated(chain_head, blkw):
                        break

                except (DuplicateBatch, DuplicateTransaction,
                        MissingDependency) as err:
                    if not self._check_chain_head_updated(chain_head, blkw):
                        raise BlockValidationFailure(
                            "Block {} failed validation: {}".format(blkw, err))

            scheduler = self._transaction_executor.create_scheduler(
                prev_state_root)

            for batch, has_more in look_ahead(blkw.block.batches):
                if has_more:
                    scheduler.add_batch(batch)
                else:
                    scheduler.add_batch(batch, blkw.state_root_hash)

        except Exception:
            if scheduler is not None:
                scheduler.cancel()
            raise

        scheduler.finalize()
        scheduler.complete(block=True)
        state_hash = None

        for batch in blkw.batches:
            batch_result = scheduler.get_batch_execution_result(
                batch.header_signature)
            if batch_result is not None and batch_result.is_valid:
                txn_results = \
                    scheduler.get_transaction_execution_results(
                        batch.header_signature)
                blkw.execution_results.extend(txn_results)
                state_hash = batch_result.state_hash
                blkw.num_transactions += len(batch.transactions)
            else:
                raise BlockValidationFailure(
                    "Block {} failed validation: Invalid batch "
                    "{}".format(blkw, batch))

        if blkw.state_root_hash != state_hash:
            raise BlockValidationFailure(
                "Block {} failed state root hash validation. Expected {}"
                " but got {}".format(blkw, blkw.state_root_hash, state_hash))

    def _check_chain_head_updated(self, chain_head, block):
        # The validity of blocks depends partially on whether or not
        # there are any duplicate transactions or batches in the block.
        # This can only be checked accurately if the block store does
        # not update during validation. The current practice is the
        # assume this will not happen and, if it does, to reprocess the
        # validation. This has been experimentally proven to be more
        # performant than locking the chain head and block store around
        # duplicate checking.
        if chain_head is None:
            return False

        current_chain_head = self._block_cache.block_store.chain_head
        if chain_head.identifier != current_chain_head.identifier:
            LOGGER.warning(
                "Chain head updated from %s to %s while checking "
                "duplicates and dependencies in block %s. "
                "Reprocessing validation.", chain_head, current_chain_head,
                block)
            return True

        return False

    def _validate_permissions(self, blkw, prev_state_root):
        """
        Validate that all of the batch signers and transaction signer for the
        batches in the block are permitted by the transactor permissioning
        roles stored in state as of the previous block. If a transactor is
        found to not be permitted, the block is invalid.
        """
        if blkw.block_num != 0:
            for batch in blkw.batches:
                if not self._permission_verifier.is_batch_signer_authorized(
                        batch, prev_state_root, from_state=True):
                    return False
        return True

    def _validate_on_chain_rules(self, blkw, prev_state_root):
        """
        Validate that the block conforms to all validation rules stored in
        state. If the block breaks any of the stored rules, the block is
        invalid.
        """
        if blkw.block_num != 0:
            return enforce_validation_rules(
                self._settings_view_factory.create_settings_view(
                    prev_state_root), blkw.header.signer_public_key,
                blkw.batches)
        return True

    def validate_block(self, blkw):
        if blkw.status == BlockStatus.Valid:
            return
        if blkw.status == BlockStatus.Invalid:
            raise BlockValidationFailure(
                'Block {} is already invalid'.format(blkw))

        # pylint: disable=broad-except
        try:
            try:
                prev_block = self._block_cache[blkw.previous_block_id]
            except KeyError:
                prev_block = None
            else:
                if prev_block.status == BlockStatus.Invalid:
                    raise BlockValidationFailure(
                        "Block {} rejected due to invalid predecessor"
                        " {}".format(blkw, prev_block))
                elif prev_block.status == BlockStatus.Unknown:
                    raise BlockValidationError(
                        "Attempted to validate block {} before its predecessor"
                        " {}".format(blkw, prev_block))

            try:
                prev_state_root = self._get_previous_block_state_root(blkw)
            except KeyError:
                raise BlockValidationError(
                    'Block {} rejected due to missing predecessor'.format(
                        blkw))

            if not self._validate_permissions(blkw, prev_state_root):
                raise BlockValidationFailure(
                    'Block {} failed permission validation'.format(blkw))

            if not self._validate_on_chain_rules(blkw, prev_state_root):
                raise BlockValidationFailure(
                    'Block {} failed on-chain validation rules'.format(blkw))

            self._validate_batches_in_block(blkw, prev_state_root)

            blkw.status = BlockStatus.Valid

        except BlockValidationFailure as err:
            blkw.status = BlockStatus.Invalid
            raise err

        except BlockValidationError as err:
            blkw.status = BlockStatus.Unknown
            raise err

        except Exception as e:
            LOGGER.exception(
                "Unhandled exception BlockValidator.validate_block()")
            raise e

    def submit_blocks_for_verification(self, blocks, callback):
        # This is a work-around for the fact that the blocks passed to this
        # function are both from the ChainController (in Rust) or itself.
        # This ensures that the blocks being operated on come from the cache
        blocks = [self._block_cache[b.identifier] for b in blocks]
        ready = self._block_scheduler.schedule(blocks)
        for block in ready:
            # Schedule the block for processing
            self._thread_pool.submit(self.process_block_verification, block,
                                     callback)

    def _release_pending(self, block):
        """Removes the block from processing and returns any blocks that should
        now be scheduled for processing, cleaning up the pending block trackers
        in the process.
        """
        ready = []
        if block.status == BlockStatus.Valid:
            ready.extend(self._block_scheduler.done(block))

        elif block.status == BlockStatus.Invalid:
            # Mark all pending blocks as invalid
            invalid = self._block_scheduler.done(block, and_descendants=True)
            for blk in invalid:
                blk.status = BlockStatus.Invalid
                LOGGER.debug('Marking descendant block invalid: %s', blk)

        else:
            # An error occured during validation, something is wrong internally
            # and we need to abort validation of this block and all its
            # children without marking them as invalid.
            unknown = self._block_scheduler.done(block, and_descendants=True)
            for blk in unknown:
                LOGGER.debug(
                    'Removing block from cache and pending due to error '
                    'during validation: %s', block)
                try:
                    del self._block_cache[block.identifier]
                except KeyError:
                    LOGGER.exception(
                        "Tried to delete a descendant pending block from the"
                        " block cache because of an error, but the descendant"
                        " was not in the cache.")

        return ready

    def has_block(self, block_id):
        return block_id in self._block_scheduler

    def process_block_verification(self, block, callback):
        """
        Main entry for Block Validation, Take a given candidate block
        and decide if it is valid then if it is valid determine if it should
        be the new head block. Returns the results to the ChainController
        so that the change over can be made if necessary.
        """
        try:
            self.validate_block(block)
            LOGGER.info('Block %s passed validation', block)
        except BlockValidationFailure as err:
            LOGGER.warning('Block %s failed validation: %s', block, err)
        except BlockValidationError as err:
            LOGGER.error('Encountered an error while validating %s: %s', block,
                         err)
        except Exception:  # pylint: disable=broad-except
            LOGGER.exception(
                "Block validation failed with unexpected error: %s", block)
        else:
            callback(block)

        try:
            blocks_now_ready = self._release_pending(block)
            self.submit_blocks_for_verification(blocks_now_ready, callback)
        except Exception:  # pylint: disable=broad-except
            LOGGER.exception(
                "Submitting pending blocks failed with unexpected error: %s",
                block)
예제 #11
0
def verify_state(global_state_db, blockstore, bind_component, scheduler_type):
    """
    Verify the state root hash of all blocks is in state and if not,
    reconstruct the missing state. Assumes that there are no "holes" in
    state, ie starting from genesis, state is present for all blocks up to some
    point and then not at all. If persist is False, this recomputes state in
    memory for all blocks in the blockstore and verifies the state root
    hashes.

    Raises:
        InvalidChainError: The chain in the blockstore is not valid.
        ExecutionError: An unrecoverable error was encountered during batch
            execution.
    """
    state_view_factory = StateViewFactory(global_state_db)

    # Check if we should do state verification
    start_block, prev_state_root = search_for_present_state_root(
        blockstore, state_view_factory)

    if start_block is None:
        LOGGER.info(
            "Skipping state verification: chain head's state root is present")
        return

    LOGGER.info(
        "Recomputing missing state from block %s with %s scheduler",
        start_block, scheduler_type)

    component_thread_pool = InstrumentedThreadPoolExecutor(
        max_workers=10,
        name='Component')

    component_dispatcher = Dispatcher()
    component_service = Interconnect(
        bind_component,
        component_dispatcher,
        secured=False,
        heartbeat=False,
        max_incoming_connections=20,
        monitor=True,
        max_future_callback_workers=10)

    context_manager = ContextManager(global_state_db)

    transaction_executor = TransactionExecutor(
        service=component_service,
        context_manager=context_manager,
        settings_view_factory=SettingsViewFactory(state_view_factory),
        scheduler_type=scheduler_type,
        invalid_observers=[])

    component_service.set_check_connections(
        transaction_executor.check_connections)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_RECEIPT_ADD_DATA_REQUEST,
        tp_state_handlers.TpReceiptAddDataHandler(context_manager),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_EVENT_ADD_REQUEST,
        tp_state_handlers.TpEventAddHandler(context_manager),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_STATE_DELETE_REQUEST,
        tp_state_handlers.TpStateDeleteHandler(context_manager),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_STATE_GET_REQUEST,
        tp_state_handlers.TpStateGetHandler(context_manager),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_STATE_SET_REQUEST,
        tp_state_handlers.TpStateSetHandler(context_manager),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_REGISTER_REQUEST,
        processor_handlers.ProcessorRegisterHandler(
            transaction_executor.processor_manager),
        component_thread_pool)

    component_dispatcher.add_handler(
        validator_pb2.Message.TP_UNREGISTER_REQUEST,
        processor_handlers.ProcessorUnRegisterHandler(
            transaction_executor.processor_manager),
        component_thread_pool)

    component_dispatcher.start()
    component_service.start()

    process_blocks(
        initial_state_root=prev_state_root,
        blocks=blockstore.get_block_iter(
            start_block=start_block, reverse=False),
        transaction_executor=transaction_executor,
        context_manager=context_manager,
        state_view_factory=state_view_factory)

    component_dispatcher.stop()
    component_service.stop()
    component_thread_pool.shutdown(wait=True)
    transaction_executor.stop()
    context_manager.stop()