def __getitem__(self, block_id): """Return the consensus state corresponding to the block ID Args: block_id (str): The ID of the block for which consensus state is being requested Returns: ConsensusState object Raises: KeyError if the block ID is not in the store """ serialized_consensus_state = self._store_db[block_id] if serialized_consensus_state is None: raise KeyError('Block ID {} not found'.format(block_id)) try: consensus_state = ConsensusState() consensus_state.parse_from_bytes( buffer=serialized_consensus_state) return consensus_state except ValueError as error: raise \ KeyError( 'Cannot return block with ID {}: {}'.format( block_id, error))
def setUp(self): # This is a little ham-handed, but we need to ensure that the # PoET enclave is set back to initial state at the start of every # test. self.poet_enclave_module = reload(poet_enclave) self.poet_enclave_module.initialize(self._temp_dir, self._temp_dir) self.mock_poet_config_view = mock.Mock() self.mock_poet_config_view.target_wait_time = 5.0 self.mock_poet_config_view.initial_wait_time = 0.0 self.mock_poet_config_view.minimum_wait_time = 1.0 self.mock_poet_config_view.population_estimate_sample_size = 50 self.consensus_state = ConsensusState()
def __str__(self): out = [] for block_id in self._store_db.keys(): try: serialized_consensus_state = self._store_db[block_id] consensus_state = ConsensusState() consensus_state.parse_from_bytes( buffer=serialized_consensus_state) out.append('{}...{}: {{{}}}'.format(block_id[:8], block_id[-8:], consensus_state)) except ValueError: pass return ', '.join(out)
def _handle_registration_timeout(self, block_header, poet_enclave_module, state_view, signup_nonce, poet_public_key): # See if a registration attempt has timed out. Assumes the caller has # checked for a committed registration and did not find it. # If it has timed out then this method will re-register. consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=block_header.previous_block_id, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) poet_settings_view = PoetSettingsView(state_view) if consensus_state.signup_attempt_timed_out( signup_nonce, poet_settings_view, self._block_cache): LOGGER.error('My poet registration using PPK %s has not ' 'committed by block %s. Create new registration', poet_public_key, block_header.previous_block_id) del self._poet_key_state_store[poet_public_key] self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module)
def __str__(self): out = [] for block_id in self._store_db.keys(): try: serialized_consensus_state = self._store_db[block_id] consensus_state = ConsensusState() consensus_state.parse_from_bytes( buffer=serialized_consensus_state) out.append( '{}...{}: {{{}}}'.format( block_id[:8], block_id[-8:], consensus_state)) except ValueError: pass return ', '.join(out)
def compare_forks(self, cur_fork_head, new_fork_head): """Given the head of two forks, return which should be the fork that the validator chooses. When this is called both forks consist of only valid blocks. Args: cur_fork_head (Block): The current head of the block chain. new_fork_head (Block): The head of the fork that is being evaluated. Returns: Boolean: True if the new chain should replace the current chain. False if the new chain should be discarded. """ chosen_fork_head = None state_view = \ BlockWrapper.state_view_for_block( block_wrapper=cur_fork_head, state_view_factory=self._state_view_factory) poet_enclave_module = \ factory.PoetEnclaveFactory.get_poet_enclave_module( state_view=state_view, config_dir=self._config_dir, data_dir=self._data_dir) current_fork_wait_certificate = \ utils.deserialize_wait_certificate( block=cur_fork_head, poet_enclave_module=poet_enclave_module) new_fork_wait_certificate = \ utils.deserialize_wait_certificate( block=new_fork_head, poet_enclave_module=poet_enclave_module) # If we ever get a new fork head that is not a PoET block, then bail # out. This should never happen, but defensively protect against it. if new_fork_wait_certificate is None: raise \ TypeError( 'New fork head {} is not a PoET block'.format( new_fork_head.identifier[:8])) # Criterion #1: If the current fork head is not PoET, then check to see # if the new fork head is building on top of it. That would be okay. # However if not, then we don't have a good deterministic way of # choosing a winner. Again, the latter should never happen, but # defensively protect against it. if current_fork_wait_certificate is None: if new_fork_head.previous_block_id == cur_fork_head.identifier: LOGGER.info( 'Choose new fork %s over current fork %s: ' 'New fork head switches consensus to PoET', new_fork_head.header_signature[:8], cur_fork_head.header_signature[:8]) chosen_fork_head = new_fork_head else: raise \ TypeError( 'Trying to compare a PoET block {} to a non-PoET ' 'block {} that is not the direct predecessor'.format( new_fork_head.identifier[:8], cur_fork_head.identifier[:8])) # Criterion #2: If they share the same immediate previous block, # then the one with the smaller wait duration is chosen elif cur_fork_head.previous_block_id == \ new_fork_head.previous_block_id: if current_fork_wait_certificate.duration < \ new_fork_wait_certificate.duration: LOGGER.info( 'Choose current fork %s over new fork %s: ' 'Current fork wait duration (%f) less than new fork wait ' 'duration (%f)', cur_fork_head.header_signature[:8], new_fork_head.header_signature[:8], current_fork_wait_certificate.duration, new_fork_wait_certificate.duration) chosen_fork_head = cur_fork_head elif new_fork_wait_certificate.duration < \ current_fork_wait_certificate.duration: LOGGER.info( 'Choose new fork %s over current fork %s: ' 'New fork wait duration (%f) less than current fork wait ' 'duration (%f)', new_fork_head.header_signature[:8], cur_fork_head.header_signature[:8], new_fork_wait_certificate.duration, current_fork_wait_certificate.duration) chosen_fork_head = new_fork_head # Criterion #3: If they don't share the same immediate previous # block, then the one with the higher aggregate local mean wins else: # Get the consensus state for the current fork head and the # block immediately before the new fork head (as we haven't # committed to the block yet). So that the new fork doesn't # have to fight with one hand tied behind its back, add the # new fork head's wait certificate's local mean to the # aggregate local mean for the predecessor block's consensus # state for the comparison. current_fork_consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=cur_fork_head.identifier, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) new_fork_consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=new_fork_head.previous_block_id, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) new_fork_aggregate_local_mean = \ new_fork_consensus_state.aggregate_local_mean + \ new_fork_wait_certificate.local_mean if current_fork_consensus_state.aggregate_local_mean > \ new_fork_aggregate_local_mean: LOGGER.info( 'Choose current fork %s over new fork %s: ' 'Current fork aggregate local mean (%f) greater than new ' 'fork aggregate local mean (%f)', cur_fork_head.header_signature[:8], new_fork_head.header_signature[:8], current_fork_consensus_state.aggregate_local_mean, new_fork_aggregate_local_mean) chosen_fork_head = cur_fork_head elif new_fork_aggregate_local_mean > \ current_fork_consensus_state.aggregate_local_mean: LOGGER.info( 'Choose new fork %s over current fork %s: ' 'New fork aggregate local mean (%f) greater than current ' 'fork aggregate local mean (%f)', new_fork_head.header_signature[:8], cur_fork_head.header_signature[:8], new_fork_aggregate_local_mean, current_fork_consensus_state.aggregate_local_mean) chosen_fork_head = new_fork_head # Criterion #4: If we have gotten to this point and we have not chosen # yet, we are going to fall back on using the block identifiers # (header signatures) . The lexicographically larger one will be the # chosen one. The chance that they are equal are infinitesimally # small. if chosen_fork_head is None: if cur_fork_head.header_signature > \ new_fork_head.header_signature: LOGGER.info( 'Choose current fork %s over new fork %s: ' 'Current fork header signature (%s) greater than new fork ' 'header signature (%s)', cur_fork_head.header_signature[:8], new_fork_head.header_signature[:8], cur_fork_head.header_signature[:8], new_fork_head.header_signature[:8]) chosen_fork_head = cur_fork_head else: LOGGER.info( 'Choose new fork %s over current fork %s: ' 'New fork header signature (%s) greater than current fork ' 'header signature (%s)', new_fork_head.header_signature[:8], cur_fork_head.header_signature[:8], new_fork_head.header_signature[:8], cur_fork_head.header_signature[:8]) chosen_fork_head = new_fork_head # Now that we have chosen a fork for the chain head, if we chose the # new fork and it is a PoET block (i.e., it has a wait certificate), # we need to create consensus state store information for the new # fork's chain head. if chosen_fork_head == new_fork_head: # Get the state view for the previous block in the chain so we can # create a PoET enclave previous_block = None try: previous_block = \ self._block_cache[new_fork_head.previous_block_id] except KeyError: pass state_view = \ BlockWrapper.state_view_for_block( block_wrapper=previous_block, state_view_factory=self._state_view_factory) validator_registry_view = ValidatorRegistryView(state_view) try: # Get the validator info for the validator that claimed the # fork head validator_info = \ validator_registry_view.get_validator_info( new_fork_head.header.signer_public_key) # Get the consensus state for the new fork head's previous # block, let the consensus state update itself appropriately # based upon the validator claiming a block, and then # associate the consensus state with the new block in the # store. consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=new_fork_head.previous_block_id, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) consensus_state.validator_did_claim_block( validator_info=validator_info, wait_certificate=new_fork_wait_certificate, poet_settings_view=PoetSettingsView(state_view)) self._consensus_state_store[new_fork_head.identifier] = \ consensus_state LOGGER.debug('Create consensus state: BID=%s, ALM=%f, TBCC=%d', new_fork_head.identifier[:8], consensus_state.aggregate_local_mean, consensus_state.total_block_claim_count) except KeyError: # This _should_ never happen. The new potential fork head # has to have been a PoET block and for it to be verified # by the PoET block verifier, it must have been signed by # validator in the validator registry. If not found, we # are going to just stick with the current fork head. LOGGER.error( 'New fork head claimed by validator not in validator ' 'registry: %s...%s', new_fork_head.header.signer_public_key[:8], new_fork_head.header.signer_public_key[-8:]) chosen_fork_head = cur_fork_head return chosen_fork_head == new_fork_head
def initialize_block(self, block_header): """Do initialization necessary for the consensus to claim a block, this may include initiating voting activities, starting proof of work hash generation, or create a PoET wait timer. Args: block_header (BlockHeader): The BlockHeader to initialize. Returns: Boolean: True if the candidate block should be built. False if no candidate should be built. """ # If the previous block ID matches our cached one, that means that we # have already determined that even if we initialize the requested # block we would not be able to claim it. So, instead of wasting time # doing all of the checking again, simply short-circuit the failure so # that the validator can go do something more useful. if block_header.previous_block_id == \ PoetBlockPublisher._previous_block_id: return False PoetBlockPublisher._previous_block_id = block_header.previous_block_id # Using the current chain head, we need to create a state view so we # can create a PoET enclave. state_view = \ BlockWrapper.state_view_for_block( block_wrapper=self._block_cache.block_store.chain_head, state_view_factory=self._state_view_factory) poet_enclave_module = \ factory.PoetEnclaveFactory.get_poet_enclave_module( state_view=state_view, config_dir=self._config_dir, data_dir=self._data_dir) # Get our validator registry entry to see what PoET public key # other validators think we are using. validator_registry_view = ValidatorRegistryView(state_view) validator_info = None try: validator_id = block_header.signer_public_key validator_info = \ validator_registry_view.get_validator_info( validator_id=validator_id) except KeyError: pass # If we don't have a validator registry entry, then check the active # key. If we don't have one, then we need to sign up. If we do have # one, then our validator registry entry has not percolated through the # system, so nothing to to but wait. active_poet_public_key = self._poet_key_state_store.active_key if validator_info is None: if active_poet_public_key is None: LOGGER.debug( 'No public key found, so going to register new signup ' 'information') self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) else: # Check if we need to give up on this registration attempt try: nonce = self._poet_key_state_store[ active_poet_public_key].signup_nonce except (ValueError, AttributeError): self._poet_key_state_store.active_key = None LOGGER.warning('Poet Key State Store had inaccessible or ' 'corrupt active key [%s] clearing ' 'key.', active_poet_public_key) return False self._handle_registration_timeout( block_header=block_header, poet_enclave_module=poet_enclave_module, state_view=state_view, signup_nonce=nonce, poet_public_key=active_poet_public_key ) return False # Retrieve the key state corresponding to the PoET public key in our # validator registry entry. poet_key_state = None try: poet_key_state = \ self._poet_key_state_store[ validator_info.signup_info.poet_public_key] except (ValueError, KeyError): pass # If there is no key state associated with the PoET public key that # other validators think we should be using, then we need to create # new signup information as we have no way whatsoever to publish # blocks that other validators will accept. if poet_key_state is None: LOGGER.debug( 'PoET public key %s...%s in validator registry not found in ' 'key state store. Sign up again', validator_info.signup_info.poet_public_key[:8], validator_info.signup_info.poet_public_key[-8:]) self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) # We need to put fake information in the key state store for the # PoET public key the other validators think we are using so that # we don't try to keep signing up. However, we are going to mark # that key state store entry as being refreshed so that we will # never actually try to use it. dummy_data = b64encode(b'No sealed signup data').decode('utf-8') self._poet_key_state_store[ validator_info.signup_info.poet_public_key] = \ PoetKeyState( sealed_signup_data=dummy_data, has_been_refreshed=True, signup_nonce='unknown') return False # Check the key state. If it is marked as being refreshed, then we are # waiting until our PoET public key is updated in the validator # registry and therefore we cannot publish any blocks. if poet_key_state.has_been_refreshed: LOGGER.debug( 'PoET public key %s...%s has been refreshed. Wait for new ' 'key to show up in validator registry.', validator_info.signup_info.poet_public_key[:8], validator_info.signup_info.poet_public_key[-8:]) # Check if we need to give up on this registration attempt self._handle_registration_timeout( block_header=block_header, poet_enclave_module=poet_enclave_module, state_view=state_view, signup_nonce=poet_key_state.signup_nonce, poet_public_key=active_poet_public_key ) return False # If the PoET public key in the validator registry is not the active # one, then we need to switch the active key in the key state store. if validator_info.signup_info.poet_public_key != \ active_poet_public_key: active_poet_public_key = validator_info.signup_info.poet_public_key self._poet_key_state_store.active_key = active_poet_public_key # Ensure that the enclave is using the appropriate keys try: unsealed_poet_public_key = \ SignupInfo.unseal_signup_data( poet_enclave_module=poet_enclave_module, sealed_signup_data=poet_key_state.sealed_signup_data) except SystemError: # Signup data is unuseable LOGGER.error( 'Could not unseal signup data associated with PPK: %s..%s', active_poet_public_key[:8], active_poet_public_key[-8:]) self._poet_key_state_store.active_key = None return False assert active_poet_public_key == unsealed_poet_public_key LOGGER.debug( 'Using PoET public key: %s...%s', active_poet_public_key[:8], active_poet_public_key[-8:]) LOGGER.debug( 'Unseal signup data: %s...%s', poet_key_state.sealed_signup_data[:8], poet_key_state.sealed_signup_data[-8:]) consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=block_header.previous_block_id, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) poet_settings_view = PoetSettingsView(state_view) # If our signup information does not pass the freshness test, then we # know that other validators will reject any blocks we try to claim so # we need to try to sign up again. if consensus_state.validator_signup_was_committed_too_late( validator_info=validator_info, poet_settings_view=poet_settings_view, block_cache=self._block_cache): LOGGER.info( 'Reject building on block %s: Validator signup information ' 'not committed in a timely manner.', block_header.previous_block_id[:8]) self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) return False # Using the consensus state for the block upon which we want to # build, check to see how many blocks we have claimed on this chain # with this PoET key. If we have hit the key block claim limit, then # we need to check if the key has been refreshed. if consensus_state.validator_has_claimed_block_limit( validator_info=validator_info, poet_settings_view=poet_settings_view): # Because we have hit the limit, check to see if we have already # submitted a validator registry transaction with new signup # information, and therefore a new PoET public key. If not, then # mark this PoET public key in the store as having been refreshed # and register new signup information. Regardless, since we have # hit the key block claim limit, we won't even bother initializing # a block on this chain as it will be rejected by other # validators. poet_key_state = self._poet_key_state_store[active_poet_public_key] if not poet_key_state.has_been_refreshed: LOGGER.info( 'Reached block claim limit for key: %s...%s', active_poet_public_key[:8], active_poet_public_key[-8:]) sealed_signup_data = poet_key_state.sealed_signup_data signup_nonce = poet_key_state.signup_nonce self._poet_key_state_store[active_poet_public_key] = \ PoetKeyState( sealed_signup_data=sealed_signup_data, has_been_refreshed=True, signup_nonce=signup_nonce) # Release enclave resources for this identity # This signup will be invalid on all forks that use it, # even if there is a rollback to a point it should be valid. # A more sophisticated policy would be to release signups # only at a block depth where finality probability # is high. SignupInfo.release_signup_data( poet_enclave_module=poet_enclave_module, sealed_signup_data=sealed_signup_data) self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) LOGGER.info( 'Reject building on block %s: Validator has reached maximum ' 'number of blocks with key pair.', block_header.previous_block_id[:8]) return False # Verify that we are abiding by the block claim delay (i.e., waiting a # certain number of blocks since our validator registry was added/ # updated). if consensus_state.validator_is_claiming_too_early( validator_info=validator_info, block_number=block_header.block_num, validator_registry_view=validator_registry_view, poet_settings_view=poet_settings_view, block_store=self._block_cache.block_store): LOGGER.info( 'Reject building on block %s: Validator has not waited long ' 'enough since registering validator information.', block_header.previous_block_id[:8]) return False # We need to create a wait timer for the block...this is what we # will check when we are asked if it is time to publish the block poet_key_state = self._poet_key_state_store[active_poet_public_key] sealed_signup_data = poet_key_state.sealed_signup_data previous_certificate_id = \ utils.get_previous_certificate_id( block_header=block_header, block_cache=self._block_cache, poet_enclave_module=poet_enclave_module) wait_timer = \ WaitTimer.create_wait_timer( poet_enclave_module=poet_enclave_module, sealed_signup_data=sealed_signup_data, validator_address=block_header.signer_public_key, previous_certificate_id=previous_certificate_id, consensus_state=consensus_state, poet_settings_view=poet_settings_view) # NOTE - we do the zTest after we create the wait timer because we # need its population estimate to see if this block would be accepted # by other validators based upon the zTest. # Check to see if by chance we were to be able to claim this block # if it would result in us winning more frequently than statistically # expected. If so, then refuse to initialize the block because other # validators will not accept anyway. if consensus_state.validator_is_claiming_too_frequently( validator_info=validator_info, previous_block_id=block_header.previous_block_id, poet_settings_view=poet_settings_view, population_estimate=wait_timer.population_estimate( poet_settings_view=poet_settings_view), block_cache=self._block_cache, poet_enclave_module=poet_enclave_module): LOGGER.info( 'Reject building on block %s: ' 'Validator (signing public key: %s) is claiming blocks ' 'too frequently.', block_header.previous_block_id[:8], block_header.signer_public_key) return False # At this point, we know that if we are able to claim the block we are # initializing, we will not be prevented from doing so because of PoET # policies. self._wait_timer = wait_timer PoetBlockPublisher._previous_block_id = None LOGGER.debug('Created wait timer: %s', self._wait_timer) return True
def verify_block(self, block_wrapper): """Check that the block received conforms to the consensus rules. Args: block_wrapper (BlockWrapper): The block to validate. Returns: Boolean: True if the Block is valid, False if the block is invalid. """ # Get the state view for the previous block in the chain so we can # create a PoET enclave and validator registry view previous_block = None try: previous_block = \ self._block_cache[block_wrapper.previous_block_id] except KeyError: pass state_view = \ BlockWrapper.state_view_for_block( block_wrapper=previous_block, state_view_factory=self._state_view_factory) poet_enclave_module = \ factory.PoetEnclaveFactory.get_poet_enclave_module( state_view=state_view, config_dir=self._config_dir) validator_registry_view = ValidatorRegistryView(state_view) # Grab the validator info based upon the block signer's public # key try: validator_info = \ validator_registry_view.get_validator_info( block_wrapper.header.signer_pubkey) except KeyError: LOGGER.error( 'Block %s rejected: Received block from an unregistered ' 'validator %s...%s', block_wrapper.identifier[:8], block_wrapper.header.signer_pubkey[:8], block_wrapper.header.signer_pubkey[-8:]) return False LOGGER.debug( 'Block Signer Name=%s, ID=%s...%s, PoET public key=' '%s...%s', validator_info.name, validator_info.id[:8], validator_info.id[-8:], validator_info.signup_info.poet_public_key[:8], validator_info.signup_info.poet_public_key[-8:]) # For the candidate block, reconstitute the wait certificate # and verify that it is valid wait_certificate = \ utils.deserialize_wait_certificate( block=block_wrapper, poet_enclave_module=poet_enclave_module) if wait_certificate is None: LOGGER.error( 'Block %s rejected: Block from validator %s (ID=%s...%s) was ' 'not created by PoET consensus module', block_wrapper.identifier[:8], validator_info.name, validator_info.id[:8], validator_info.id[-8:]) return False # Get the consensus state and PoET configuration view for the block # that is being built upon consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=block_wrapper.previous_block_id, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) poet_config_view = PoetConfigView(state_view=state_view) previous_certificate_id = \ utils.get_previous_certificate_id( block_header=block_wrapper.header, block_cache=self._block_cache, poet_enclave_module=poet_enclave_module) try: wait_certificate.check_valid( poet_enclave_module=poet_enclave_module, previous_certificate_id=previous_certificate_id, poet_public_key=validator_info.signup_info.poet_public_key, consensus_state=consensus_state, poet_config_view=poet_config_view) except ValueError as error: LOGGER.error( 'Block %s rejected: Wait certificate check failed - %s', block_wrapper.identifier[:8], error) return False # Reject the block if the validator signup information fails the # freshness check. if consensus_state.validator_signup_was_committed_too_late( validator_info=validator_info, poet_config_view=poet_config_view, block_cache=self._block_cache): LOGGER.error( 'Block %s rejected: Validator signup information not ' 'committed in a timely manner.', block_wrapper.identifier[:8]) return False # Reject the block if the validator has already claimed the key bock # limit for its current PoET key pair. if consensus_state.validator_has_claimed_block_limit( validator_info=validator_info, poet_config_view=poet_config_view): LOGGER.error( 'Block %s rejected: Validator has reached maximum number of ' 'blocks with key pair.', block_wrapper.identifier[:8]) return False # Reject the block if the validator has not waited the required number # of blocks between when the block containing its validator registry # transaction was committed to the chain and trying to claim this # block if consensus_state.validator_is_claiming_too_early( validator_info=validator_info, block_number=block_wrapper.block_num, validator_registry_view=validator_registry_view, poet_config_view=poet_config_view, block_store=self._block_cache.block_store): LOGGER.error( 'Block %s rejected: Validator has not waited long enough ' 'since registering validator information.', block_wrapper.identifier[:8]) return False # Reject the block if the validator is claiming blocks at a rate that # is more frequent than is statistically allowed (i.e., zTest) if consensus_state.validator_is_claiming_too_frequently( validator_info=validator_info, previous_block_id=block_wrapper.previous_block_id, poet_config_view=poet_config_view, population_estimate=wait_certificate.population_estimate( poet_config_view=poet_config_view), block_cache=self._block_cache, poet_enclave_module=poet_enclave_module): LOGGER.error( 'Block %s rejected: Validator is claiming blocks too ' 'frequently.', block_wrapper.identifier[:8]) return False return True
class TestWaitCertificate(TestCase): @classmethod def setUpClass(cls): cls._originator_public_key_hash = create_random_public_key_hash() def setUp(self): # This is a little ham-handed, but we need to ensure that the # PoET enclave is set back to initial state at the start of every # test. self.poet_enclave_module = reload(poet_enclave) args = {"NodeName": "DasValidator"} self.poet_enclave_module.initialize(**args) self.mock_poet_config_view = mock.Mock() self.mock_poet_config_view.target_wait_time = 5.0 self.mock_poet_config_view.initial_wait_time = 0.0 self.mock_poet_config_view.minimum_wait_time = 1.0 self.mock_poet_config_view.population_estimate_sample_size = 50 self.consensus_state = ConsensusState() def test_create_before_create_signup_info(self): # Make sure that trying to create a wait certificate before signup # information is provided causes an error with self.assertRaises(ValueError): WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=None, block_hash="Reader's Digest") def test_create_before_create_wait_timer(self): # Need to create signup information SignupInfo.create_signup_info( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', originator_public_key_hash=self._originator_public_key_hash, nonce=NULL_BLOCK_IDENTIFIER) # Make sure that trying to create a wait certificate before creating # a wait timer causes an error with self.assertRaises(ValueError): WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=None, block_hash="Reader's Digest") def test_create_before_wait_timer_expires(self): # Need to create signup information SignupInfo.create_signup_info( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', originator_public_key_hash=self._originator_public_key_hash, nonce=NULL_BLOCK_IDENTIFIER) # Create a wait certificate for the genesis block so that we can # create another wait certificate that has to play by the rules. wt = \ WaitTimer.create_wait_timer( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', previous_certificate_id=NULL_BLOCK_IDENTIFIER, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) wc = \ WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=wt, block_hash="Reader's Digest") wt = \ WaitTimer.create_wait_timer( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', previous_certificate_id=wc.identifier, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) with self.assertRaises(ValueError): WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=wt, block_hash="Reader's Digest") @skip("Disabled until poet integration -- too slow!!!!!") def test_create_after_wait_timer_timed_out(self): # Need to create signup information SignupInfo.create_signup_info( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', originator_public_key_hash=self._originator_public_key_hash, nonce=NULL_BLOCK_IDENTIFIER) # Create a wait certificate for the genesis block so that we can # create another wait certificate that has to play by the rules. wt = \ WaitTimer.create_wait_timer( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', previous_certificate_id=NULL_BLOCK_IDENTIFIER, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) wc = \ WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=wt, block_hash="Reader's Digest") wt = \ WaitTimer.create_wait_timer( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', previous_certificate_id=wc.identifier, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) while not wt.has_expired(time.time()): time.sleep(1) time.sleep(self.poet_enclave_module.TIMER_TIMEOUT_PERIOD + 1) with self.assertRaises(ValueError): WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=wt, block_hash="Reader's Digest") def test_create_with_wrong_wait_timer(self): # Need to create signup information SignupInfo.create_signup_info( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', originator_public_key_hash=self._originator_public_key_hash, nonce=NULL_BLOCK_IDENTIFIER) # Create two timers and try to create the wait certificate with the # first one, which should fail as it is not the current wait timer invalid_wt = \ WaitTimer.create_wait_timer( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', previous_certificate_id=NULL_BLOCK_IDENTIFIER, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) valid_wt = \ WaitTimer.create_wait_timer( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', previous_certificate_id=NULL_BLOCK_IDENTIFIER, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) # Verify that we cannot create a wait certificate with the old wait # timer, but we can with the new one with self.assertRaises(ValueError): WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=invalid_wt, block_hash="Reader's Digest") WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=valid_wt, block_hash="Reader's Digest") def test_create_with_reused_wait_timer(self): # Need to create signup information SignupInfo.create_signup_info( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', originator_public_key_hash=self._originator_public_key_hash, nonce=NULL_BLOCK_IDENTIFIER) # Create a wait certificate for the genesis block so that we can # create another wait certificate that has to play by the rules. wt = \ WaitTimer.create_wait_timer( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', previous_certificate_id=NULL_BLOCK_IDENTIFIER, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) wc = \ WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=wt, block_hash="Reader's Digest") consumed_wt = wt # Verify that we cannot use the consumed wait timer to create a wait # certificate either before or after creating a new wait timer with self.assertRaises(ValueError): WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=consumed_wt, block_hash="Reader's Digest") wt = \ WaitTimer.create_wait_timer( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', previous_certificate_id=wc.identifier, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) with self.assertRaises(ValueError): WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=consumed_wt, block_hash="Reader's Digest") # Verify that once the new timer expires, we can create a wait # certificate with it while not wt.has_expired(time.time()): time.sleep(1) WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=wt, block_hash="Reader's Digest") @skip("Disabled until poet integration -- too slow!!!!!") def test_create(self): # Need to create signup information and wait timer first signup_info = \ SignupInfo.create_signup_info( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', originator_public_key_hash=self._originator_public_key_hash, nonce=NULL_BLOCK_IDENTIFIER) wt = \ WaitTimer.create_wait_timer( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', previous_certificate_id=NULL_BLOCK_IDENTIFIER, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) while not wt.has_expired(time.time()): time.sleep(1) # Now we can create a wait certificate and verify that it correlates # to the wait timer we just created wc = \ WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=wt, block_hash="Reader's Digest") self.assertIsNotNone(wc) self.assertEqual(wc.previous_certificate_id, wt.previous_certificate_id) self.assertAlmostEqual(wc.local_mean, wt.local_mean) self.assertAlmostEqual(wc.request_time, wt.request_time) self.assertAlmostEqual(wc.duration, wt.duration) self.assertEqual(wc.validator_address, wt.validator_address) self.assertEqual(wc.block_hash, "Reader's Digest") self.assertIsNotNone(wc.signature) self.assertIsNotNone(wc.identifier) # A newly-created wait certificate should be valid wc.check_valid(poet_enclave_module=self.poet_enclave_module, previous_certificate_id=NULL_BLOCK_IDENTIFIER, poet_public_key=signup_info.poet_public_key, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) validator_info = \ ValidatorInfo( id='validator_001', signup_info=SignUpInfo( poet_public_key='key_001')) self.consensus_state.validator_did_claim_block( validator_info=validator_info, wait_certificate=wc, poet_config_view=self.mock_poet_config_view) # Create another wait certificate and verify it is valid also wt = \ WaitTimer.create_wait_timer( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', previous_certificate_id=wc.identifier, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) while not wt.has_expired(time.time()): time.sleep(1) # Now we can create a wait certificate and verify that it correlates # to the wait timer we just created another_wc = \ WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=wt, block_hash="Pepto Bismol") another_wc.check_valid(poet_enclave_module=self.poet_enclave_module, previous_certificate_id=wc.identifier, poet_public_key=signup_info.poet_public_key, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) @skip("Disabled until poet integration -- too slow!!!!!") def test_serialization(self): # Need to create signup information and wait timer first signup_info = \ SignupInfo.create_signup_info( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', originator_public_key_hash=self._originator_public_key_hash, nonce=NULL_BLOCK_IDENTIFIER) wt = \ WaitTimer.create_wait_timer( poet_enclave_module=self.poet_enclave_module, validator_address='1660 Pennsylvania Avenue NW', previous_certificate_id=NULL_BLOCK_IDENTIFIER, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view) while not wt.has_expired(time.time()): time.sleep(1) # Now we can create a wait certificate and serialize wc = \ WaitCertificate.create_wait_certificate( poet_enclave_module=self.poet_enclave_module, wait_timer=wt, block_hash="Reader's Digest") dumped = wc.dump() self.assertIsNotNone(dumped.get('SerializedCertificate')) self.assertIsNotNone(dumped.get('Signature')) # Deserialize and verify that wait certificates are the same # and that deserialized one is valid wc_copy = \ WaitCertificate.wait_certificate_from_serialized( poet_enclave_module=self.poet_enclave_module, serialized=dumped.get('SerializedCertificate'), signature=dumped.get('Signature')) self.assertEqual(wc.previous_certificate_id, wc_copy.previous_certificate_id) self.assertAlmostEqual(wc.local_mean, wc_copy.local_mean) self.assertAlmostEqual(wc.request_time, wc_copy.request_time) self.assertAlmostEqual(wc.duration, wc_copy.duration) self.assertEqual(wc.validator_address, wc_copy.validator_address) self.assertEqual(wc.block_hash, wc_copy.block_hash) self.assertEqual(wc.signature, wc_copy.signature) self.assertEqual(wc.identifier, wc_copy.identifier) # Serialize the copy and verify that its serialization and # signature are the same dumped_copy = wc_copy.dump() self.assertTrue(dumped.get('SerializedCertificate'), dumped_copy.get('SerializedCertificate')) self.assertTrue(dumped.get('Signature'), dumped_copy.get('Signature')) wc_copy.check_valid(poet_enclave_module=self.poet_enclave_module, previous_certificate_id=NULL_BLOCK_IDENTIFIER, poet_public_key=signup_info.poet_public_key, consensus_state=self.consensus_state, poet_config_view=self.mock_poet_config_view)
def get_consensus_state_for_block_id(block_id, block_cache, state_view_factory, consensus_state_store, poet_enclave_module): """Returns the consensus state for the block referenced by block ID, creating it from the consensus state history if necessary. Args: block_id (str): The ID of the block for which consensus state will be returned. block_cache (BlockCache): The block store cache state_view_factory (StateViewFactory): A factory that can be used to create state view object corresponding to blocks consensus_state_store (ConsensusStateStore): The consensus state store poet_enclave_module (module): The PoET enclave module Returns: ConsensusState object representing the consensus state for the block referenced by block_id """ # Starting at the chain head, walk the block store backwards until we # either get to the root or we get a block for which we have already # created consensus state block = \ block_cache[block_id] if block_id != NULL_BLOCK_IDENTIFIER else None consensus_state = None block_ids = collections.deque() while block is not None: # Try to fetch the consensus state. If that succeeds, we can # stop walking back as we can now build on that consensus # state. consensus_state = consensus_state_store.get(block.identifier) if consensus_state is not None: break # If this is a PoET block, then we want to create consensus state # for it when we are done if deserialize_wait_certificate(block, poet_enclave_module): LOGGER.debug( 'We need to build consensus state for block ID %s...%s', block.identifier[:8], block.identifier[-8:]) block_ids.appendleft(block.identifier) # Move to the previous block block = \ block_cache[block.previous_block_id] \ if block.previous_block_id != NULL_BLOCK_IDENTIFIER else None # If didn't find any consensus state, see if there is any "before" any # blocks were created (this might be because we are the first validator # and PoET signup information was created, including sealed signup data # that was saved in the consensus state store). if consensus_state is None: consensus_state = consensus_state_store.get(NULL_BLOCK_IDENTIFIER) # At this point, if we have not found any consensus state, we need to # create default state from which we can build upon if consensus_state is None: consensus_state = ConsensusState() # Now, walking forward through the blocks for which we were supposed to # create consensus state, we are going to create and store state for each # one so that the next time we don't have to walk so far back through the # block chain. for identifier in block_ids: block = block_cache[identifier] # Get the validator registry view for this block's state view and # then fetch the validator info for the validator that signed this # block. state_view = state_view_factory.create_view(block.state_root_hash) validator_registry_view = ValidatorRegistryView(state_view) validator_id = block.header.signer_pubkey validator_info = \ validator_registry_view.get_validator_info( validator_id=validator_id) # Fetch the current validator state, set/update the validator state # in the consensus state object, and then create the consensus state # in the consensus state store and associate it with this block validator_state = \ consensus_state.get_validator_state(validator_id=validator_id) consensus_state.set_validator_state( validator_id=validator_id, validator_state=create_validator_state( validator_info=validator_info, current_validator_state=validator_state)) LOGGER.debug('Store consensus state for block ID %s...%s', identifier[:8], identifier[-8:]) consensus_state_store[identifier] = consensus_state return consensus_state
def initialize_block(self, block_header): """Do initialization necessary for the consensus to claim a block, this may include initiating voting activities, starting proof of work hash generation, or create a PoET wait timer. Args: block_header (BlockHeader): The BlockHeader to initialize. Returns: Boolean: True if the candidate block should be built. False if no candidate should be built. """ # Using the current chain head, we need to create a state view so we # can create a PoET enclave. state_view = \ BlockWrapper.state_view_for_block( block_wrapper=self._block_cache.block_store.chain_head, state_view_factory=self._state_view_factory) poet_enclave_module = \ factory.PoetEnclaveFactory.get_poet_enclave_module(state_view) # Check the consensus state to see if we have current sealed signup # information. consensus_state = \ utils.get_consensus_state_for_block_id( block_id=block_header.previous_block_id, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) if consensus_state is not None and \ consensus_state.sealed_signup_data is not None: # Check to see if the sealed signup data we have cached is the # same as what is in the consensus store. If not, then it is # stale and so we need to unseal it and update our cached copy. if consensus_state.sealed_signup_data != \ PoetBlockPublisher._sealed_signup_data: LOGGER.debug('Unseal signup data %s...%s', consensus_state.sealed_signup_data[:8], consensus_state.sealed_signup_data[-8:]) PoetBlockPublisher._sealed_signup_data = \ consensus_state.sealed_signup_data PoetBlockPublisher._poet_public_key = \ SignupInfo.unseal_signup_data( poet_enclave_module=poet_enclave_module, validator_address=block_header.signer_pubkey, sealed_signup_data=consensus_state.sealed_signup_data) # Otherwise, if we don't already have a public key, we need to create # signup information and create a transaction to add it to the # validator registry. elif PoetBlockPublisher._poet_public_key is None: LOGGER.debug( 'No public key found, so going to register new signup ' 'information') self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) return False # Otherwise, at this point we need to check the validator registry to # see if our _current_ validator registry information was added to the # validator registry. else: validator_registry_view = ValidatorRegistryView(state_view) try: validator_id = block_header.signer_pubkey validator_info = \ validator_registry_view.get_validator_info( validator_id=validator_id) LOGGER.debug( 'Our Validator Registry Entry: Name=%s, ID=%s...%s, PoET ' 'public key=%s...%s', validator_info.name, validator_info.id[:8], validator_info.id[-8:], validator_info.signup_info.poet_public_key[:8], validator_info.signup_info.poet_public_key[-8:]) # We need to verify that our validator registry entry is # current - basically this means verifying that the PoET # public key that will be used to verify the validity of # our wait certificates is the PoET public key matching our # current private key. if validator_info.signup_info.poet_public_key != \ PoetBlockPublisher._poet_public_key: LOGGER.debug( 'Our Validator Registry Entry PoET public key ' '(%s...%s) doesn' 't match the PoET public key ' 'expected (%s...%s)', validator_info.signup_info.poet_public_key[:8], validator_info.signup_info.poet_public_key[-8:], PoetBlockPublisher._poet_public_key[:8], PoetBlockPublisher._poet_public_key[-8:]) return False # At this point, we know that we are in the validator registry # and the entry is current. We can save the sealed signup # data to the consensus state for the previous block and can # clear out our cached copies of the sealed signup data and # the PoET public key. if consensus_state is None: consensus_state = ConsensusState() consensus_state.sealed_signup_data = \ PoetBlockPublisher._sealed_signup_data self._consensus_state_store[block_header.previous_block_id] = \ consensus_state PoetBlockPublisher._sealed_signup_data = None PoetBlockPublisher._poet_public_key = None except KeyError: LOGGER.debug( 'We cannot initialize the block because our PoET signup ' 'information is not in the validator registry') return False # Since we are registering, don't bother trying to initialize # the block return False # Create a list of certificates for the wait timer. This seems to have # a little too much knowledge of the WaitTimer implementation, but # there is no use getting more than # WaitTimer.certificate_sample_length wait certificates. certificates = \ utils.build_certificate_list( block_header=block_header, block_cache=self._block_cache, poet_enclave_module=poet_enclave_module, maximum_number=WaitTimer.certificate_sample_length) # We need to create a wait timer for the block...this is what we # will check when we are asked if it is time to publish the block self._wait_timer = \ WaitTimer.create_wait_timer( poet_enclave_module=poet_enclave_module, validator_address=block_header.signer_pubkey, certificates=list(certificates)) LOGGER.debug('Created wait timer: %s', self._wait_timer) return True
def do_genesis(args): """Executes the `poet genesis` subcommand. This command generates a validator registry transaction and saves it to a file, whose location is determined by the args. The signup data, generated by the selected enclave, is also stored in a well-known location. """ if args.enclave_module == 'simulator': module_name = SIMUATOR_MODULE elif args.enclave_module == 'sgx': module_name = SGX_MODULE else: raise AssertionError('Unknown enclave module: {}'.format( args.enclave_module)) try: poet_enclave_module = importlib.import_module(module_name) except ImportError as e: raise AssertionError(str(e)) poet_enclave_module.initialize(**{}) pubkey, signing_key = _read_signing_keys(args.key) public_key_hash = sha256(pubkey.encode()).hexdigest() signup_info = SignupInfo.create_signup_info( poet_enclave_module=poet_enclave_module, validator_address=pubkey, originator_public_key_hash=public_key_hash, most_recent_wait_certificate_id=NULL_BLOCK_IDENTIFIER) print('Writing sealed signup data to consensus state store') consensus_state = ConsensusState() consensus_state.sealed_signup_data = signup_info.sealed_signup_data consensus_state_store = \ ConsensusStateStore( data_dir=config.get_data_dir(), validator_id=pubkey) consensus_state_store[NULL_BLOCK_IDENTIFIER] = consensus_state # Create the validator registry payload payload = \ vr_pb.ValidatorRegistryPayload( verb='register', name='validator-{}'.format(pubkey[-8:]), id=pubkey, signup_info=vr_pb.SignUpInfo( poet_public_key=signup_info.poet_public_key, proof_data=signup_info.proof_data, anti_sybil_id=signup_info.anti_sybil_id), ) serialized = payload.SerializeToString() # Create the address that will be used to look up this validator # registry transaction. Seems like a potential for refactoring.. validator_entry_address = \ VR_NAMESPACE + sha256(pubkey.encode()).hexdigest() # Create a transaction header and transaction for the validator # registry update amd then hand it off to the batch publisher to # send out. addresses = [validator_entry_address, VALIDATOR_MAP_ADDRESS] header = \ txn_pb.TransactionHeader( signer_pubkey=pubkey, family_name='sawtooth_validator_registry', family_version='1.0', inputs=addresses, outputs=addresses, dependencies=[], payload_encoding="application/protobuf", payload_sha512=sha512(serialized).hexdigest(), batcher_pubkey=pubkey, nonce=time.time().hex().encode()).SerializeToString() signature = signing.sign(header, signing_key) transaction = \ txn_pb.Transaction( header=header, payload=serialized, header_signature=signature) batch = _create_batch(pubkey, signing_key, [transaction]) batch_list = batch_pb.BatchList(batches=[batch]) try: print('Generating {}'.format(args.output)) with open(args.output, 'wb') as batch_file: batch_file.write(batch_list.SerializeToString()) except IOError as e: raise CliException( 'Unable to write to batch file: {}'.format(str(e)))
def initialize_block(self, block_header): """Do initialization necessary for the consensus to claim a block, this may include initiating voting activities, starting proof of work hash generation, or create a PoET wait timer. Args: block_header (BlockHeader): The BlockHeader to initialize. Returns: Boolean: True if the candidate block should be built. False if no candidate should be built. """ # If the previous block ID matches our cached one, that means that we # have already determined that even if we initialize the requested # block we would not be able to claim it. So, instead of wasting time # doing all of the checking again, simply short-circuit the failure so # that the validator can go do something more useful. if block_header.previous_block_id == \ PoetBlockPublisher._previous_block_id: return False PoetBlockPublisher._previous_block_id = block_header.previous_block_id # Using the current chain head, we need to create a state view so we # can create a PoET enclave. state_view = \ BlockWrapper.state_view_for_block( block_wrapper=self._block_cache.block_store.chain_head, state_view_factory=self._state_view_factory) poet_enclave_module = \ factory.PoetEnclaveFactory.get_poet_enclave_module(state_view) # Get our validator registry entry to see what PoET public key # other validators think we are using. validator_registry_view = ValidatorRegistryView(state_view) validator_info = None try: validator_id = block_header.signer_pubkey validator_info = \ validator_registry_view.get_validator_info( validator_id=validator_id) except KeyError: pass # If we don't have a validator registry entry, then check our cached # PoET public key. If we don't have one, then we need to sign up. # If we do have one, then our validator registry entry has not # percolated through the system, so nothing to to but wait. if validator_info is None: if PoetBlockPublisher._poet_public_key is None: LOGGER.debug( 'No public key found, so going to register new signup ' 'information') self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) return False # Otherwise, we have a current validator registry entry. In that # case, we need to make sure that we are using the same PPK that the # other validators think we are using. If not, then we need to switch # the PoET enclave to using the correct keys. elif validator_info.signup_info.poet_public_key != \ PoetBlockPublisher._poet_public_key: # Retrieve the key state corresponding to the PoET public key and # use it to re-establish the key used by the enclave. poet_key_state = \ self._poet_key_state_store[ validator_info.signup_info.poet_public_key] PoetBlockPublisher._poet_public_key = \ SignupInfo.unseal_signup_data( poet_enclave_module=poet_enclave_module, validator_address=block_header.signer_pubkey, sealed_signup_data=poet_key_state.sealed_signup_data) assert PoetBlockPublisher._poet_public_key == \ validator_info.signup_info.poet_public_key LOGGER.debug( 'Switched to public key: %s...%s', PoetBlockPublisher._poet_public_key[:8], PoetBlockPublisher._poet_public_key[-8:]) LOGGER.debug( 'Unseal signup data: %s...%s', poet_key_state.sealed_signup_data[:8], poet_key_state.sealed_signup_data[-8:]) consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=block_header.previous_block_id, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) poet_config_view = PoetConfigView(state_view) # If our signup information does not pass the freshness test, then we # know that other validators will reject any blocks we try to claim so # we need to try to sign up again. if consensus_state.validator_signup_was_committed_too_late( validator_info=validator_info, poet_config_view=poet_config_view, block_cache=self._block_cache): LOGGER.error( 'Reject building on block %s: Validator signup information ' 'not committed in a timely manner.', block_header.previous_block_id[:8]) self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) return False # Using the consensus state for the block upon which we want to # build, check to see how many blocks we have claimed on this chain # with this PoET key. If we have hit the key block claim limit, then # we need to check if the key has been refreshed. if consensus_state.validator_has_claimed_block_limit( validator_info=validator_info, poet_config_view=poet_config_view): # Because we have hit the limit, check to see if we have already # submitted a validator registry transaction with new signup # information, and therefore a new PoET public key. If not, then # mark this PoET public key in the store as having been refreshed # and register new signup information. Regardless, since we have # hit the key block claim limit, we won't even bother initializing # a block on this chain as it will be rejected by other # validators. poet_key_state = \ self._poet_key_state_store[ PoetBlockPublisher._poet_public_key] if not poet_key_state.has_been_refreshed: LOGGER.info( 'Reached block claim limit for key: %s...%s', PoetBlockPublisher._poet_public_key[:8], PoetBlockPublisher._poet_public_key[-8:]) sealed_signup_data = poet_key_state.sealed_signup_data self._poet_key_state_store[ PoetBlockPublisher._poet_public_key] = \ PoetKeyState( sealed_signup_data=sealed_signup_data, has_been_refreshed=True) self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) LOGGER.error( 'Reject building on block %s: Validator has reached maximum ' 'number of blocks with key pair.', block_header.previous_block_id[:8]) return False # Verify that we are abiding by the block claim delay (i.e., waiting a # certain number of blocks since our validator registry was added/ # updated). if consensus_state.validator_is_claiming_too_early( validator_info=validator_info, block_number=block_header.block_num, validator_registry_view=validator_registry_view, poet_config_view=poet_config_view, block_store=self._block_cache.block_store): LOGGER.error( 'Reject building on block %s: Validator has not waited long ' 'enough since registering validator information.', block_header.previous_block_id[:8]) return False # We need to create a wait timer for the block...this is what we # will check when we are asked if it is time to publish the block previous_certificate_id = \ utils.get_previous_certificate_id( block_header=block_header, block_cache=self._block_cache, poet_enclave_module=poet_enclave_module) wait_timer = \ WaitTimer.create_wait_timer( poet_enclave_module=poet_enclave_module, validator_address=block_header.signer_pubkey, previous_certificate_id=previous_certificate_id, consensus_state=consensus_state, poet_config_view=poet_config_view) # NOTE - we do the zTest after we create the wait timer because we # need its population estimate to see if this block would be accepted # by other validators based upon the zTest. # Check to see if by chance we were to be able to claim this block # if it would result in us winning more frequently than statistically # expected. If so, then refuse to initialize the block because other # validators will not accept anyway. if consensus_state.validator_is_claiming_too_frequently( validator_info=validator_info, previous_block_id=block_header.previous_block_id, poet_config_view=poet_config_view, population_estimate=wait_timer.population_estimate( poet_config_view=poet_config_view), block_cache=self._block_cache, poet_enclave_module=poet_enclave_module): LOGGER.error( 'Reject building on block %s: Validator is claiming blocks ' 'too frequently.', block_header.previous_block_id[:8]) return False # At this point, we know that if we are able to claim the block we are # initializing, we will not be prevented from doing so because of PoET # policies. self._wait_timer = wait_timer PoetBlockPublisher._previous_block_id = None LOGGER.debug('Created wait timer: %s', self._wait_timer) return True
def compare_forks(self, cur_fork_head, new_fork_head): """Given the head of two forks, return which should be the fork that the validator chooses. When this is called both forks consist of only valid blocks. Args: cur_fork_head (Block): The current head of the block chain. new_fork_head (Block): The head of the fork that is being evaluated. Returns: Boolean: True if the new chain should replace the current chain. False if the new chain should be discarded. """ chosen_fork_head = None state_view = \ BlockWrapper.state_view_for_block( block_wrapper=cur_fork_head, state_view_factory=self._state_view_factory) poet_enclave_module = \ factory.PoetEnclaveFactory.get_poet_enclave_module( state_view=state_view, config_dir=self._config_dir, data_dir=self._data_dir) current_fork_wait_certificate = \ utils.deserialize_wait_certificate( block=cur_fork_head, poet_enclave_module=poet_enclave_module) new_fork_wait_certificate = \ utils.deserialize_wait_certificate( block=new_fork_head, poet_enclave_module=poet_enclave_module) # If we ever get a new fork head that is not a PoET block, then bail # out. This should never happen, but defensively protect against it. if new_fork_wait_certificate is None: raise \ TypeError( 'New fork head {} is not a PoET block'.format( new_fork_head.identifier[:8])) # Criterion #1: If the current fork head is not PoET, then check to see # if the new fork head is building on top of it. That would be okay. # However if not, then we don't have a good deterministic way of # choosing a winner. Again, the latter should never happen, but # defensively protect against it. if current_fork_wait_certificate is None: if new_fork_head.previous_block_id == cur_fork_head.identifier: LOGGER.info( 'Choose new fork %s over current fork %s: ' 'New fork head switches consensus to PoET', new_fork_head.header_signature[:8], cur_fork_head.header_signature[:8]) chosen_fork_head = new_fork_head else: raise \ TypeError( 'Trying to compare a PoET block {} to a non-PoET ' 'block {} that is not the direct predecessor'.format( new_fork_head.identifier[:8], cur_fork_head.identifier[:8])) # Criterion #2: If they share the same immediate previous block, # then the one with the smaller wait duration is chosen elif cur_fork_head.previous_block_id == \ new_fork_head.previous_block_id: if current_fork_wait_certificate.duration < \ new_fork_wait_certificate.duration: LOGGER.info( 'Choose current fork %s over new fork %s: ' 'Current fork wait duration (%f) less than new fork wait ' 'duration (%f)', cur_fork_head.header_signature[:8], new_fork_head.header_signature[:8], current_fork_wait_certificate.duration, new_fork_wait_certificate.duration) chosen_fork_head = cur_fork_head elif new_fork_wait_certificate.duration < \ current_fork_wait_certificate.duration: LOGGER.info( 'Choose new fork %s over current fork %s: ' 'New fork wait duration (%f) less than current fork wait ' 'duration (%f)', new_fork_head.header_signature[:8], cur_fork_head.header_signature[:8], new_fork_wait_certificate.duration, current_fork_wait_certificate.duration) chosen_fork_head = new_fork_head # Criterion #3: If they don't share the same immediate previous # block, then the one with the higher aggregate local mean wins else: # Get the consensus state for the current fork head and the # block immediately before the new fork head (as we haven't # committed to the block yet). So that the new fork doesn't # have to fight with one hand tied behind its back, add the # new fork head's wait certificate's local mean to the # aggregate local mean for the predecessor block's consensus # state for the comparison. current_fork_consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=cur_fork_head.identifier, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) new_fork_consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=new_fork_head.previous_block_id, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) new_fork_aggregate_local_mean = \ new_fork_consensus_state.aggregate_local_mean + \ new_fork_wait_certificate.local_mean if current_fork_consensus_state.aggregate_local_mean > \ new_fork_aggregate_local_mean: LOGGER.info( 'Choose current fork %s over new fork %s: ' 'Current fork aggregate local mean (%f) greater than new ' 'fork aggregate local mean (%f)', cur_fork_head.header_signature[:8], new_fork_head.header_signature[:8], current_fork_consensus_state.aggregate_local_mean, new_fork_aggregate_local_mean) chosen_fork_head = cur_fork_head elif new_fork_aggregate_local_mean > \ current_fork_consensus_state.aggregate_local_mean: LOGGER.info( 'Choose new fork %s over current fork %s: ' 'New fork aggregate local mean (%f) greater than current ' 'fork aggregate local mean (%f)', new_fork_head.header_signature[:8], cur_fork_head.header_signature[:8], new_fork_aggregate_local_mean, current_fork_consensus_state.aggregate_local_mean) chosen_fork_head = new_fork_head # Criterion #4: If we have gotten to this point and we have not chosen # yet, we are going to fall back on using the block identifiers # (header signatures) . The lexicographically larger one will be the # chosen one. The chance that they are equal are infinitesimally # small. if chosen_fork_head is None: if cur_fork_head.header_signature > \ new_fork_head.header_signature: LOGGER.info( 'Choose current fork %s over new fork %s: ' 'Current fork header signature (%s) greater than new fork ' 'header signature (%s)', cur_fork_head.header_signature[:8], new_fork_head.header_signature[:8], cur_fork_head.header_signature[:8], new_fork_head.header_signature[:8]) chosen_fork_head = cur_fork_head else: LOGGER.info( 'Choose new fork %s over current fork %s: ' 'New fork header signature (%s) greater than current fork ' 'header signature (%s)', new_fork_head.header_signature[:8], cur_fork_head.header_signature[:8], new_fork_head.header_signature[:8], cur_fork_head.header_signature[:8]) chosen_fork_head = new_fork_head # Now that we have chosen a fork for the chain head, if we chose the # new fork and it is a PoET block (i.e., it has a wait certificate), # we need to create consensus state store information for the new # fork's chain head. if chosen_fork_head == new_fork_head: # Get the state view for the previous block in the chain so we can # create a PoET enclave previous_block = None try: previous_block = \ self._block_cache[new_fork_head.previous_block_id] except KeyError: pass state_view = \ BlockWrapper.state_view_for_block( block_wrapper=previous_block, state_view_factory=self._state_view_factory) validator_registry_view = ValidatorRegistryView(state_view) try: # Get the validator info for the validator that claimed the # fork head validator_info = \ validator_registry_view.get_validator_info( new_fork_head.header.signer_public_key) # Get the consensus state for the new fork head's previous # block, let the consensus state update itself appropriately # based upon the validator claiming a block, and then # associate the consensus state with the new block in the # store. consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=new_fork_head.previous_block_id, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) consensus_state.validator_did_claim_block( validator_info=validator_info, wait_certificate=new_fork_wait_certificate, poet_settings_view=PoetSettingsView(state_view)) self._consensus_state_store[new_fork_head.identifier] = \ consensus_state LOGGER.debug( 'Create consensus state: BID=%s, ALM=%f, TBCC=%d', new_fork_head.identifier[:8], consensus_state.aggregate_local_mean, consensus_state.total_block_claim_count) except KeyError: # This _should_ never happen. The new potential fork head # has to have been a PoET block and for it to be verified # by the PoET block verifier, it must have been signed by # validator in the validator registry. If not found, we # are going to just stick with the current fork head. LOGGER.error( 'New fork head claimed by validator not in validator ' 'registry: %s...%s', new_fork_head.header.signer_public_key[:8], new_fork_head.header.signer_public_key[-8:]) chosen_fork_head = cur_fork_head return chosen_fork_head == new_fork_head
def initialize_block(self, block_header): """Do initialization necessary for the consensus to claim a block, this may include initiating voting activities, starting proof of work hash generation, or create a PoET wait timer. Args: block_header (BlockHeader): The BlockHeader to initialize. Returns: Boolean: True if the candidate block should be built. False if no candidate should be built. """ # If the previous block ID matches our cached one, that means that we # have already determined that even if we initialize the requested # block we would not be able to claim it. So, instead of wasting time # doing all of the checking again, simply short-circuit the failure so # that the validator can go do something more useful. if block_header.previous_block_id == \ PoetBlockPublisher._previous_block_id: return False PoetBlockPublisher._previous_block_id = block_header.previous_block_id # Using the current chain head, we need to create a state view so we # can create a PoET enclave. state_view = \ BlockWrapper.state_view_for_block( block_wrapper=self._block_cache.block_store.chain_head, state_view_factory=self._state_view_factory) poet_enclave_module = \ factory.PoetEnclaveFactory.get_poet_enclave_module( state_view=state_view, config_dir=self._config_dir, data_dir=self._data_dir) # Get our validator registry entry to see what PoET public key # other validators think we are using. validator_registry_view = ValidatorRegistryView(state_view) validator_info = None try: validator_id = block_header.signer_pubkey validator_info = \ validator_registry_view.get_validator_info( validator_id=validator_id) except KeyError: pass # If we don't have a validator registry entry, then check the active # key. If we don't have one, then we need to sign up. # If we do have one, then our validator registry entry has not # percolated through the system, so nothing to to but wait. active_poet_public_key = self._poet_key_state_store.active_key if validator_info is None: if active_poet_public_key is None: LOGGER.debug( 'No public key found, so going to register new signup ' 'information') self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) return False # Otherwise, we have a current validator registry entry. In that # case, we need to make sure that we are using the same PPK that the # other validators think we are using. If not, then we need to switch # the PoET enclave to using the correct keys. elif validator_info.signup_info.poet_public_key != \ active_poet_public_key: # Retrieve the key state corresponding to the PoET public key and # use it to re-establish the key used by the enclave. Also update # the active PoET public key. poet_key_state = \ self._poet_key_state_store[ validator_info.signup_info.poet_public_key] active_poet_public_key = \ SignupInfo.unseal_signup_data( poet_enclave_module=poet_enclave_module, sealed_signup_data=poet_key_state.sealed_signup_data) self._poet_key_state_store.active_key = active_poet_public_key assert active_poet_public_key == \ validator_info.signup_info.poet_public_key LOGGER.debug( 'Switched to public key: %s...%s', active_poet_public_key[:8], active_poet_public_key[-8:]) LOGGER.debug( 'Unseal signup data: %s...%s', poet_key_state.sealed_signup_data[:8], poet_key_state.sealed_signup_data[-8:]) consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=block_header.previous_block_id, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) poet_settings_view = PoetSettingsView(state_view) # If our signup information does not pass the freshness test, then we # know that other validators will reject any blocks we try to claim so # we need to try to sign up again. if consensus_state.validator_signup_was_committed_too_late( validator_info=validator_info, poet_settings_view=poet_settings_view, block_cache=self._block_cache): LOGGER.info( 'Reject building on block %s: Validator signup information ' 'not committed in a timely manner.', block_header.previous_block_id[:8]) self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) return False # Using the consensus state for the block upon which we want to # build, check to see how many blocks we have claimed on this chain # with this PoET key. If we have hit the key block claim limit, then # we need to check if the key has been refreshed. if consensus_state.validator_has_claimed_block_limit( validator_info=validator_info, poet_settings_view=poet_settings_view): # Because we have hit the limit, check to see if we have already # submitted a validator registry transaction with new signup # information, and therefore a new PoET public key. If not, then # mark this PoET public key in the store as having been refreshed # and register new signup information. Regardless, since we have # hit the key block claim limit, we won't even bother initializing # a block on this chain as it will be rejected by other # validators. poet_key_state = self._poet_key_state_store[active_poet_public_key] if not poet_key_state.has_been_refreshed: LOGGER.info( 'Reached block claim limit for key: %s...%s', active_poet_public_key[:8], active_poet_public_key[-8:]) sealed_signup_data = poet_key_state.sealed_signup_data self._poet_key_state_store[active_poet_public_key] = \ PoetKeyState( sealed_signup_data=sealed_signup_data, has_been_refreshed=True) self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) LOGGER.info( 'Reject building on block %s: Validator has reached maximum ' 'number of blocks with key pair.', block_header.previous_block_id[:8]) return False # Verify that we are abiding by the block claim delay (i.e., waiting a # certain number of blocks since our validator registry was added/ # updated). if consensus_state.validator_is_claiming_too_early( validator_info=validator_info, block_number=block_header.block_num, validator_registry_view=validator_registry_view, poet_settings_view=poet_settings_view, block_store=self._block_cache.block_store): LOGGER.info( 'Reject building on block %s: Validator has not waited long ' 'enough since registering validator information.', block_header.previous_block_id[:8]) return False # We need to create a wait timer for the block...this is what we # will check when we are asked if it is time to publish the block previous_certificate_id = \ utils.get_previous_certificate_id( block_header=block_header, block_cache=self._block_cache, poet_enclave_module=poet_enclave_module) wait_timer = \ WaitTimer.create_wait_timer( poet_enclave_module=poet_enclave_module, validator_address=block_header.signer_pubkey, previous_certificate_id=previous_certificate_id, consensus_state=consensus_state, poet_settings_view=poet_settings_view) # NOTE - we do the zTest after we create the wait timer because we # need its population estimate to see if this block would be accepted # by other validators based upon the zTest. # Check to see if by chance we were to be able to claim this block # if it would result in us winning more frequently than statistically # expected. If so, then refuse to initialize the block because other # validators will not accept anyway. if consensus_state.validator_is_claiming_too_frequently( validator_info=validator_info, previous_block_id=block_header.previous_block_id, poet_settings_view=poet_settings_view, population_estimate=wait_timer.population_estimate( poet_settings_view=poet_settings_view), block_cache=self._block_cache, poet_enclave_module=poet_enclave_module): LOGGER.info( 'Reject building on block %s: Validator is claiming blocks ' 'too frequently.', block_header.previous_block_id[:8]) return False # At this point, we know that if we are able to claim the block we are # initializing, we will not be prevented from doing so because of PoET # policies. self._wait_timer = wait_timer PoetBlockPublisher._previous_block_id = None LOGGER.debug('Created wait timer: %s', self._wait_timer) return True
def initialize_block(self, block_header): """Do initialization necessary for the consensus to claim a block, this may include initiating voting activities, starting proof of work hash generation, or create a PoET wait timer. Args: block_header (BlockHeader): The BlockHeader to initialize. Returns: Boolean: True if the candidate block should be built. False if no candidate should be built. """ # If the previous block ID matches our cached one, that means that we # have already determined that even if we initialize the requested # block we would not be able to claim it. So, instead of wasting time # doing all of the checking again, simply short-circuit the failure so # that the validator can go do something more useful. if block_header.previous_block_id == \ PoetBlockPublisher._previous_block_id: return False PoetBlockPublisher._previous_block_id = block_header.previous_block_id # Using the current chain head, we need to create a state view so we # can create a PoET enclave. state_view = \ BlockWrapper.state_view_for_block( block_wrapper=self._block_cache.block_store.chain_head, state_view_factory=self._state_view_factory) poet_enclave_module = \ factory.PoetEnclaveFactory.get_poet_enclave_module( state_view=state_view, config_dir=self._config_dir, data_dir=self._data_dir) # Get our validator registry entry to see what PoET public key # other validators think we are using. validator_registry_view = ValidatorRegistryView(state_view) validator_info = None try: validator_id = block_header.signer_pubkey validator_info = \ validator_registry_view.get_validator_info( validator_id=validator_id) except KeyError: pass # If we don't have a validator registry entry, then check the active # key. If we don't have one, then we need to sign up. If we do have # one, then our validator registry entry has not percolated through the # system, so nothing to to but wait. active_poet_public_key = self._poet_key_state_store.active_key if validator_info is None: if active_poet_public_key is None: LOGGER.debug( 'No public key found, so going to register new signup ' 'information') self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) return False # Retrieve the key state corresponding to the PoET public key in our # validator registry entry. poet_key_state = None try: poet_key_state = \ self._poet_key_state_store[ validator_info.signup_info.poet_public_key] except (ValueError, KeyError): pass # If there is no key state associated with the PoET public key that # other validators think we should be using, then we need to create # new signup information as we have no way whatsoever to publish # blocks that other validators will accept. if poet_key_state is None: LOGGER.debug( 'PoET public key %s...%s in validator registry not found in ' 'key state store. Sign up again', validator_info.signup_info.poet_public_key[:8], validator_info.signup_info.poet_public_key[-8:]) self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) # We need to put fake information in the key state store for the # PoET public key the other validators think we are using so that # we don't try to keep signing up. However, we are going to mark # that key state store entry as being refreshed so that we will # never actually try to use it. dummy_data = b64encode(b'No sealed signup data').decode('utf-8') self._poet_key_state_store[ validator_info.signup_info.poet_public_key] = \ PoetKeyState( sealed_signup_data=dummy_data, has_been_refreshed=True) return False # Check the key state. If it is marked as being refreshed, then we are # waiting until our PoET public key is updated in the validator # registry and therefore we cannot publish any blocks. if poet_key_state.has_been_refreshed: LOGGER.debug( 'PoET public key %s...%s has been refreshed. Wait for new ' 'key to show up in validator registry.', validator_info.signup_info.poet_public_key[:8], validator_info.signup_info.poet_public_key[-8:]) return False # If the PoET public key in the validator registry is not the active # one, then we need to switch the active key in the key state store. if validator_info.signup_info.poet_public_key != \ active_poet_public_key: active_poet_public_key = validator_info.signup_info.poet_public_key self._poet_key_state_store.active_key = active_poet_public_key # Ensure that the enclave is using the appropriate keys try: unsealed_poet_public_key = \ SignupInfo.unseal_signup_data( poet_enclave_module=poet_enclave_module, sealed_signup_data=poet_key_state.sealed_signup_data) except SystemError: # Signup data is unuseable LOGGER.error( 'Could not unseal signup data associated with PPK: %s..%s', active_poet_public_key[:8], active_poet_public_key[-8:]) self._poet_key_state_store.active_key = None return False assert active_poet_public_key == unsealed_poet_public_key LOGGER.debug( 'Using PoET public key: %s...%s', active_poet_public_key[:8], active_poet_public_key[-8:]) LOGGER.debug( 'Unseal signup data: %s...%s', poet_key_state.sealed_signup_data[:8], poet_key_state.sealed_signup_data[-8:]) consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=block_header.previous_block_id, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) poet_settings_view = PoetSettingsView(state_view) # If our signup information does not pass the freshness test, then we # know that other validators will reject any blocks we try to claim so # we need to try to sign up again. if consensus_state.validator_signup_was_committed_too_late( validator_info=validator_info, poet_settings_view=poet_settings_view, block_cache=self._block_cache): LOGGER.info( 'Reject building on block %s: Validator signup information ' 'not committed in a timely manner.', block_header.previous_block_id[:8]) self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) return False # Using the consensus state for the block upon which we want to # build, check to see how many blocks we have claimed on this chain # with this PoET key. If we have hit the key block claim limit, then # we need to check if the key has been refreshed. if consensus_state.validator_has_claimed_block_limit( validator_info=validator_info, poet_settings_view=poet_settings_view): # Because we have hit the limit, check to see if we have already # submitted a validator registry transaction with new signup # information, and therefore a new PoET public key. If not, then # mark this PoET public key in the store as having been refreshed # and register new signup information. Regardless, since we have # hit the key block claim limit, we won't even bother initializing # a block on this chain as it will be rejected by other # validators. poet_key_state = self._poet_key_state_store[active_poet_public_key] if not poet_key_state.has_been_refreshed: LOGGER.info( 'Reached block claim limit for key: %s...%s', active_poet_public_key[:8], active_poet_public_key[-8:]) sealed_signup_data = poet_key_state.sealed_signup_data self._poet_key_state_store[active_poet_public_key] = \ PoetKeyState( sealed_signup_data=sealed_signup_data, has_been_refreshed=True) # Release enclave resources for this identity # This signup will be invalid on all forks that use it, # even if there is a rollback to a point it should be valid. # A more sophisticated policy would be to release signups # only at a block depth where finality probability # is high. SignupInfo.release_signup_data( poet_enclave_module=poet_enclave_module, sealed_signup_data=sealed_signup_data) self._register_signup_information( block_header=block_header, poet_enclave_module=poet_enclave_module) LOGGER.info( 'Reject building on block %s: Validator has reached maximum ' 'number of blocks with key pair.', block_header.previous_block_id[:8]) return False # Verify that we are abiding by the block claim delay (i.e., waiting a # certain number of blocks since our validator registry was added/ # updated). if consensus_state.validator_is_claiming_too_early( validator_info=validator_info, block_number=block_header.block_num, validator_registry_view=validator_registry_view, poet_settings_view=poet_settings_view, block_store=self._block_cache.block_store): LOGGER.info( 'Reject building on block %s: Validator has not waited long ' 'enough since registering validator information.', block_header.previous_block_id[:8]) return False # We need to create a wait timer for the block...this is what we # will check when we are asked if it is time to publish the block poet_key_state = self._poet_key_state_store[active_poet_public_key] sealed_signup_data = poet_key_state.sealed_signup_data previous_certificate_id = \ utils.get_previous_certificate_id( block_header=block_header, block_cache=self._block_cache, poet_enclave_module=poet_enclave_module) wait_timer = \ WaitTimer.create_wait_timer( poet_enclave_module=poet_enclave_module, sealed_signup_data=sealed_signup_data, validator_address=block_header.signer_pubkey, previous_certificate_id=previous_certificate_id, consensus_state=consensus_state, poet_settings_view=poet_settings_view) # NOTE - we do the zTest after we create the wait timer because we # need its population estimate to see if this block would be accepted # by other validators based upon the zTest. # Check to see if by chance we were to be able to claim this block # if it would result in us winning more frequently than statistically # expected. If so, then refuse to initialize the block because other # validators will not accept anyway. if consensus_state.validator_is_claiming_too_frequently( validator_info=validator_info, previous_block_id=block_header.previous_block_id, poet_settings_view=poet_settings_view, population_estimate=wait_timer.population_estimate( poet_settings_view=poet_settings_view), block_cache=self._block_cache, poet_enclave_module=poet_enclave_module): LOGGER.info( 'Reject building on block %s: Validator is claiming blocks ' 'too frequently.', block_header.previous_block_id[:8]) return False # At this point, we know that if we are able to claim the block we are # initializing, we will not be prevented from doing so because of PoET # policies. self._wait_timer = wait_timer PoetBlockPublisher._previous_block_id = None LOGGER.debug('Created wait timer: %s', self._wait_timer) return True
def get_consensus_state_for_block_id(block_id, block_cache, state_view_factory, consensus_state_store, poet_enclave_module): """Returns the consensus state for the block referenced by block ID, creating it from the consensus state history if necessary. Args: block_id (str): The ID of the block for which consensus state will be returned. block_cache (BlockCache): The block store cache state_view_factory (StateViewFactory): A factory that can be used to create state view object corresponding to blocks consensus_state_store (ConsensusStateStore): The consensus state store poet_enclave_module (module): The PoET enclave module Returns: ConsensusState object representing the consensus state for the block referenced by block_id """ consensus_state = None previous_wait_certificate = None blocks = collections.OrderedDict() # Starting at the chain head, walk the block store backwards until we # either get to the root or we get a block for which we have already # created consensus state while True: block = _block_for_id(block_id=block_id, block_cache=block_cache) if block is None: break # Try to fetch the consensus state. If that succeeds, we can # stop walking back as we can now build on that consensus # state. consensus_state = consensus_state_store.get(block_id=block_id) if consensus_state is not None: break wait_certificate = \ deserialize_wait_certificate( block=block, poet_enclave_module=poet_enclave_module) # If this is a PoET block (i.e., it has a wait certificate), get the # validator info for the validator that signed this block and add the # block information we will need to set validator state in the block's # consensus state. if wait_certificate is not None: state_view = \ state_view_factory.create_view( state_root_hash=block.state_root_hash) validator_registry_view = \ ValidatorRegistryView(state_view=state_view) validator_info = \ validator_registry_view.get_validator_info( validator_id=block.header.signer_pubkey) LOGGER.debug('We need to build consensus state for block: %s...%s', block_id[:8], block_id[-8:]) blocks[block_id] = \ BlockInfo( wait_certificate=wait_certificate, validator_info=validator_info) # Otherwise, this is a non-PoET block. If we don't have any blocks # yet or the last block we processed was a PoET block, put a # placeholder in the list so that when we get to it we know that we # need to reset the statistics. elif len(blocks) == 0 or previous_wait_certificate is not None: blocks[block_id] = \ BlockInfo( wait_certificate=None, validator_info=None) previous_wait_certificate = wait_certificate # Move to the previous block block_id = block.previous_block_id # At this point, if we have not found any consensus state, we need to # create default state from which we can build upon if consensus_state is None: consensus_state = ConsensusState() # Now, walk through the blocks for which we were supposed to create # consensus state, from oldest to newest (i.e., in the reverse order in # which they were added), and store state for PoET blocks so that the next # time we don't have to walk so far back through the block chain. for block_id, block_info in reversed(blocks.items()): # If the block was not a PoET block (i.e., didn't have a wait # certificate), reset the consensus state statistics. We are not # going to store this in the consensus state store, but we will use it # as the starting for the next PoET block. if block_info.wait_certificate is None: consensus_state = ConsensusState() # Otherwise, update the consensus state statistics and fetch the # validator state for the validator which claimed the block, create # updated validator state for the validator, set/update the validator # state in the consensus state object, and then associate the # consensus state with the corresponding block in the consensus state # store. else: validator_state = \ get_current_validator_state( validator_info=block_info.validator_info, consensus_state=consensus_state, block_cache=block_cache) consensus_state.set_validator_state( validator_id=block_info.validator_info.id, validator_state=create_next_validator_state( validator_info=block_info.validator_info, current_validator_state=validator_state, block_cache=block_cache)) LOGGER.debug('Store consensus state for block: %s...%s', block_id[:8], block_id[-8:]) consensus_state.total_block_claim_count += 1 consensus_state_store[block_id] = consensus_state return consensus_state
def verify_block(self, block_wrapper): """Check that the block received conforms to the consensus rules. Args: block_wrapper (BlockWrapper): The block to validate. Returns: Boolean: True if the Block is valid, False if the block is invalid. """ # Get the state view for the previous block in the chain so we can # create a PoET enclave and validator registry view previous_block = None try: previous_block = \ self._block_cache[block_wrapper.previous_block_id] except KeyError: pass state_view = \ BlockWrapper.state_view_for_block( block_wrapper=previous_block, state_view_factory=self._state_view_factory) poet_enclave_module = \ factory.PoetEnclaveFactory.get_poet_enclave_module( state_view=state_view, config_dir=self._config_dir, data_dir=self._data_dir) validator_registry_view = ValidatorRegistryView(state_view) # Grab the validator info based upon the block signer's public # key try: validator_info = \ validator_registry_view.get_validator_info( block_wrapper.header.signer_public_key) except KeyError: LOGGER.error( 'Block %s rejected: Received block from an unregistered ' 'validator %s...%s', block_wrapper.identifier[:8], block_wrapper.header.signer_public_key[:8], block_wrapper.header.signer_public_key[-8:]) return False LOGGER.debug( 'Block Signer Name=%s, ID=%s...%s, PoET public key=' '%s...%s', validator_info.name, validator_info.id[:8], validator_info.id[-8:], validator_info.signup_info.poet_public_key[:8], validator_info.signup_info.poet_public_key[-8:]) # For the candidate block, reconstitute the wait certificate # and verify that it is valid wait_certificate = \ utils.deserialize_wait_certificate( block=block_wrapper, poet_enclave_module=poet_enclave_module) if wait_certificate is None: LOGGER.error( 'Block %s rejected: Block from validator %s (ID=%s...%s) was ' 'not created by PoET consensus module', block_wrapper.identifier[:8], validator_info.name, validator_info.id[:8], validator_info.id[-8:]) return False # Get the consensus state and PoET configuration view for the block # that is being built upon consensus_state = \ ConsensusState.consensus_state_for_block_id( block_id=block_wrapper.previous_block_id, block_cache=self._block_cache, state_view_factory=self._state_view_factory, consensus_state_store=self._consensus_state_store, poet_enclave_module=poet_enclave_module) poet_settings_view = PoetSettingsView(state_view=state_view) previous_certificate_id = \ utils.get_previous_certificate_id( block_header=block_wrapper.header, block_cache=self._block_cache, poet_enclave_module=poet_enclave_module) try: wait_certificate.check_valid( poet_enclave_module=poet_enclave_module, previous_certificate_id=previous_certificate_id, poet_public_key=validator_info.signup_info.poet_public_key, consensus_state=consensus_state, poet_settings_view=poet_settings_view) except ValueError as error: LOGGER.error( 'Block %s rejected: Wait certificate check failed - %s', block_wrapper.identifier[:8], error) return False # Reject the block if the validator signup information fails the # freshness check. if consensus_state.validator_signup_was_committed_too_late( validator_info=validator_info, poet_settings_view=poet_settings_view, block_cache=self._block_cache): LOGGER.error( 'Block %s rejected: Validator signup information not ' 'committed in a timely manner.', block_wrapper.identifier[:8]) return False # Reject the block if the validator has already claimed the key block # limit for its current PoET key pair. if consensus_state.validator_has_claimed_block_limit( validator_info=validator_info, poet_settings_view=poet_settings_view): LOGGER.error( 'Block %s rejected: Validator has reached maximum number of ' 'blocks with key pair.', block_wrapper.identifier[:8]) return False # Reject the block if the validator has not waited the required number # of blocks between when the block containing its validator registry # transaction was committed to the chain and trying to claim this # block if consensus_state.validator_is_claiming_too_early( validator_info=validator_info, block_number=block_wrapper.block_num, validator_registry_view=validator_registry_view, poet_settings_view=poet_settings_view, block_store=self._block_cache.block_store): LOGGER.error( 'Block %s rejected: Validator has not waited long enough ' 'since registering validator information.', block_wrapper.identifier[:8]) return False # Reject the block if the validator is claiming blocks at a rate that # is more frequent than is statistically allowed (i.e., zTest) if consensus_state.validator_is_claiming_too_frequently( validator_info=validator_info, previous_block_id=block_wrapper.previous_block_id, poet_settings_view=poet_settings_view, population_estimate=wait_certificate.population_estimate( poet_settings_view=poet_settings_view), block_cache=self._block_cache, poet_enclave_module=poet_enclave_module): LOGGER.error( 'Block %s rejected: Validator is claiming blocks too ' 'frequently.', block_wrapper.identifier[:8]) return False return True