def validate_account_root_files(self, is_partial_allowed: bool = True): account_root_files_iter = self.yield_blockchain_states( ) # type: ignore with validates('number of account root files (at least one)'): try: first_account_root_file = next(account_root_files_iter) except StopIteration: raise ValidationError( 'Blockchain must contain at least one account root file') is_initial = first_account_root_file.is_initial() if not is_partial_allowed and not is_initial: raise ValidationError( 'Blockchain must start with initial account root file') is_first = True for counter, account_root_file in enumerate( chain((first_account_root_file, ), account_root_files_iter)): with validates(f'account root file number {counter}'): self.validate_account_root_file( account_root_file=account_root_file, is_initial=is_initial, is_first=is_first) is_initial = False # only first iteration can be with initial is_first = False
def validate(self): with validates('recipient'): if not self.recipient: raise ValidationError( f'{self.humanized_class_name} recipient is not set') with validates('amount'): amount = self.amount if not isinstance(amount, int): raise ValidationError( f'{self.humanized_class_name} amount must be an integer') if amount < 1: raise ValidationError( f'{self.humanized_class_name} amount must be greater or equal to 1' ) with validates('fee'): if self.fee not in (True, False, None): raise ValidationError( f'{self.humanized_class_name} fee value is invalid') with validates('memo'): max_len = settings.MEMO_MAX_LENGTH if self.memo and len(self.memo) > max_len: raise ValidationError( f'{self.humanized_class_name} memo must be less than {max_len} characters' )
def deserialize_from_dict(cls, dict_, complain_excessive_keys=True, exclude=()): dict_ = dict_.copy() message_dict = dict_.pop('message', None) if message_dict is None: raise ValidationError('Missing keys: message') elif not isinstance(message_dict, dict): raise ValidationError('message must be a dict') instance_block_type = message_dict.get('block_type') for signed_change_request_class, (block_type, _) in get_request_to_block_type_map().items(): if block_type == instance_block_type: signed_change_request_dict = message_dict.get('signed_change_request') if signed_change_request_dict is None: raise ValidationError('Missing keys: signed_change_request') elif not isinstance(signed_change_request_dict, dict): raise ValidationError('signed_change_request must be a dict') signed_change_request_obj = signed_change_request_class.deserialize_from_dict( signed_change_request_dict ) break else: raise NotImplementedError(f'message.block_type "{instance_block_type}" is not supported') message_obj = BlockMessage.deserialize_from_dict( message_dict, override={'signed_change_request': signed_change_request_obj} ) return super().deserialize_from_dict( dict_, complain_excessive_keys=complain_excessive_keys, override={'message': message_obj} )
def validate_blocks(self, *, offset: int = 0, limit: Optional[int] = None): """ Validate blocks persisted in the blockchain. Some blockchain level validations may overlap with block level validations. We consider it OK since it is better to double check something rather than miss something. We may reconsider this overlap in favor of validation performance. """ assert offset >= 0 blocks_iter = cast(Iterable[Block], self.yield_blocks()) # type: ignore if offset > 0 or limit is not None: # TODO(dmu) HIGH: Consider performance improvements when slicing if limit is None: blocks_iter = islice(blocks_iter, offset) else: blocks_iter = islice(blocks_iter, offset, offset + limit) try: first_block = next(blocks_iter) # type: ignore except StopIteration: return first_account_root_file = self.get_first_blockchain_state() # type: ignore if first_account_root_file is None: raise ValidationError('Account root file prior to first block is not found') first_block_number = first_block.message.block_number if offset == 0: with validates('basing on an account root file'): if first_account_root_file.get_next_block_number() != first_block_number: raise ValidationError('First block number does not match base account root file last block number') if first_account_root_file.get_next_block_identifier() != first_block.message.block_identifier: raise ValidationError( 'First block identifier does not match base account root file last block identifier' ) expected_block_identifier = first_account_root_file.get_next_block_identifier() else: prev_block = self.get_block_by_number(first_block_number - 1) # type: ignore if prev_block is None: raise ValidationError(f'Previous block for block number {first_block_number} is not found') assert prev_block.hash expected_block_identifier = prev_block.hash expected_block_number = first_account_root_file.get_next_block_number() + offset for block in chain((first_block,), blocks_iter): block.validate(self) assert block.message self.validate_block( block=block, expected_block_number=expected_block_number, expected_block_identifier=expected_block_identifier ) expected_block_number += 1 expected_block_identifier = block.hash
def validate_signer(self): signer = self.signer if not signer: raise ValidationError('Signer must be set') if not isinstance(signer, str): raise ValidationError('Signer must be a string') return signer
def validate_block_number(self): block_number = self.block_number if block_number is None: raise ValidationError('Block number must be set') if not isinstance(block_number, int): raise ValidationError('Block number must be integer') if block_number < 0: raise ValidationError('Block number must be greater or equal to 0')
def validate_signature(self): verify_key = self.validate_signer() signature = self.signature if not signature: raise ValidationError('Signature must be set') if not isinstance(signature, str): raise ValidationError('Signature must be a string') self.message.validate_signature(verify_key, self.signature)
def validate_next_block_identifier(self, is_initial): if is_initial: if self.next_block_identifier is not None: raise ValidationError( 'Account root file next block identifier must not be set for initial account root file' ) else: if not isinstance(self.next_block_identifier, str): raise ValidationError( 'Account root file next block identifier must be a string')
def validate_block(self, *, block: Block, expected_block_number: int, expected_block_identifier: str): actual_block_number = block.message.block_number actual_block_identifier = block.message.block_identifier if actual_block_number != expected_block_number: raise ValidationError(f'Expected block number {expected_block_number} but got {actual_block_number}') if actual_block_identifier != expected_block_identifier: raise ValidationError( f'Expected block identifier {expected_block_identifier} but got {actual_block_identifier}' )
def deserialize_from_dict( cls, dict_, complain_excessive_keys=True, override: Optional[dict[str, Any]] = None): # noqa: C901 """Return instance deserialized from `dict_`. Args: dict_ (dict): dict object to be deserialized from complain_excessive_keys (bool): if `True` then `ValidationError` is raise if unknown keys are met override (dict): a dict of values that have already been deserialized """ override = override or {} field_names = set(cls.get_field_names()) missing_keys = [ key for key in field_names - dict_.keys() if key not in override and not cls.is_optional_field(key) ] if missing_keys: raise ValidationError('Missing keys: {}'.format( ', '.join(missing_keys))) deserialized = {} for key, value in dict_.items(): if key in override: continue if key not in field_names: if complain_excessive_keys: raise ValidationError(f'Unknown key: {key}') else: continue field_type = cls.get_field_type(key) if issubclass(field_type, SerializableMixin): value = field_type.deserialize_from_dict( value, complain_excessive_keys=complain_excessive_keys) else: origin = typing.get_origin(field_type) if origin and issubclass(origin, list): value = cls.deserialize_from_inner_list( field_type, value, complain_excessive_keys) elif origin and issubclass(origin, dict): value = cls.deserialize_from_inner_dict( field_type, value, complain_excessive_keys) else: value = coerce_from_json_type(value, field_type) deserialized[key] = value deserialized.update(override) return cls(**deserialized) # type: ignore
def validate_block_identifier(self, blockchain): block_identifier = self.block_identifier if block_identifier is None: raise ValidationError('Block identifier must be set') if not isinstance(block_identifier, str): raise ValidationError('Block identifier must be a string') block_number = self.block_number assert block_number is not None if block_identifier != blockchain.get_expected_block_identifier( block_number): raise ValidationError('Invalid block identifier')
def validate_last_block_number(self, is_initial): if is_initial: if self.last_block_number is not None: raise ValidationError( 'Account root file last block number must not be set for initial account root file' ) else: if not isinstance(self.last_block_number, int): raise ValidationError( 'Account root file last block number must be an integer') if self.last_block_number < 0: raise ValidationError( 'Account root file last block number must be a non-negative integer' )
def validate_account_root_file(self, *, account_root_file, is_initial=False, is_first=False): account_root_file.validate(is_initial=is_initial) if is_initial: return if is_first: logger.debug( 'First account root file is not a subject of further validations' ) return self.validate_account_root_file_balances( account_root_file=account_root_file) first_block = self.get_first_block() # type: ignore if not first_block: return if first_block.message.block_number > account_root_file.last_block_number: logger.debug('First block is after the account root file') if first_block.message.block_number > account_root_file.last_block_number + 1: logger.warning('Unnecessary old account root file detected') return # If account root file is after first known block then we can validate its attributes account_root_file_last_block = self.get_block_by_number( account_root_file.last_block_number) # type: ignore with validates('account root file last_block_number'): if account_root_file_last_block is None: raise ValidationError( 'Account root file last_block_number points to non-existing block' ) with validates('account root file last_block_identifier'): if account_root_file_last_block.message.block_identifier != account_root_file.last_block_identifier: raise ValidationError( 'Account root file last_block_number does not match last_block_identifier' ) with validates('account root file next_block_identifier'): if account_root_file_last_block.message_hash != account_root_file.next_block_identifier: raise ValidationError( 'Account root file next_block_identifier does not match last_block_number message hash' )
def validate_signed_change_request(self, blockchain): signed_change_request = self.signed_change_request if signed_change_request is None: raise ValidationError( 'Block message transfer request must present') signed_change_request.validate(blockchain, self.block_number)
def validate_timestamp(self, blockchain): timestamp = self.timestamp validate_not_none(f'{self.humanized_class_name} timestamp', timestamp) validate_type(f'{self.humanized_class_name} timestamp', timestamp, datetime) validate_is_none(f'{self.humanized_class_name} timestamp timezone', timestamp.tzinfo) block_number = self.block_number assert block_number is not None if block_number > 0: prev_block_number = block_number - 1 prev_block = blockchain.get_block_by_number(prev_block_number) if prev_block is None: logger.debug('Partial blockchain detected') blockchain_state = blockchain.get_closest_blockchain_state_snapshot( block_number) validate_not_none('Closest blockchain state', blockchain_state) if blockchain_state.is_initial(): raise ValidationError( 'Unexpected initial account root file found') validate_exact_value('Blockchain state last block number', blockchain_state.last_block_number, prev_block_number) assert blockchain_state.last_block_timestamp min_timestamp = blockchain_state.last_block_timestamp else: min_timestamp = prev_block.message.timestamp validate_gt_value(f'{self.humanized_class_name} timestamp', timestamp, min_timestamp)
def validate_accounts(self): for account, balance in self.account_states.items(): with validates(f'account root file account {account}'): if not isinstance(account, str): raise ValidationError( 'Account root file account number must be a string') balance.validate()
def deserialize_from_dict(cls, dict_, complain_excessive_keys=True, override=None): # noqa: C901 override = override or {} field_names = set(cls.get_field_names()) missing_keys = [ key for key in field_names - dict_.keys() if key not in override and not cls.is_optional_field(key) ] if missing_keys: raise ValidationError('Missing keys: {}'.format( ', '.join(missing_keys))) deserialized = {} for key, value in dict_.items(): if key in override: continue if key not in field_names: if complain_excessive_keys: raise ValidationError(f'Unknown key: {key}') else: continue field_type = cls.get_field_type(key) if issubclass(field_type, SerializableMixin): value = field_type.deserialize_from_dict( value, complain_excessive_keys=complain_excessive_keys) else: origin = typing.get_origin(field_type) if origin and issubclass(origin, list): value = cls.deserialize_from_inner_list( field_type, value, complain_excessive_keys) elif origin and issubclass(origin, dict): value = cls.deserialize_from_inner_dict( field_type, value, complain_excessive_keys) else: value = coerce_from_json_type(value, field_type) deserialized[key] = value deserialized.update(override) return cls(**deserialized)
def validate_amount(self, blockchain, in_block_number: int): balance = blockchain.get_account_balance(self.signer, in_block_number - 1) validate_greater_than_zero( f'{self.humanized_class_name_lowered} singer balance', balance) if self.message.get_total_amount() > balance: raise ValidationError( f'{self.humanized_class_name} total amount is greater than signer account balance' )
def validate_transactions(self): txs = self.txs if not isinstance(txs, list): raise ValidationError( f'{self.humanized_class_name} txs must be a list') if not txs: raise ValidationError( f'{self.humanized_class_name} txs must contain at least one transaction' ) for tx in self.txs: with validates( f'Validating transaction {tx} on {self.get_humanized_class_name(False)} level' ): if not isinstance(tx, CoinTransferTransaction): raise ValidationError( f'{self.humanized_class_name} txs must contain only Transactions types' ) tx.validate()
def validate_last_block_timestamp(self, is_initial): if is_initial: if self.last_block_timestamp is not None: raise ValidationError( 'Account root file last block timestamp must not be set for initial account root file' ) else: timestamp = self.last_block_timestamp if timestamp is None: raise ValidationError( 'Account root file last block timestamp must be set') if not isinstance(timestamp, datetime): raise ValidationError( 'Account root file last block timestamp must be datetime type' ) if timestamp.tzinfo is not None: raise ValidationError( 'Account root file last block timestamp must be naive datetime (UTC timezone implied)' )
def validate_account_root_file_balances(self, *, account_root_file): generated_account_root_file = self.generate_blockchain_state( account_root_file.last_block_number) # type: ignore with validates('number of account root file balances'): expected_accounts_count = len( generated_account_root_file.account_states) actual_accounts_count = len(account_root_file.account_states) if expected_accounts_count != actual_accounts_count: raise ValidationError( f'Expected {expected_accounts_count} accounts, ' f'but got {actual_accounts_count} in the account root file' ) actual_accounts = account_root_file.account_states for account_number, account_state in generated_account_root_file.account_states.items( ): with validates(f'account {account_number} existence'): actual_account_state = actual_accounts.get(account_number) if actual_account_state is None: raise ValidationError( f'Could not find {account_number} account in the account root file' ) with validates(f'account {account_number} balance value'): expect_balance = account_state.balance actual_state = actual_account_state.balance if actual_state != expect_balance: raise ValidationError( f'Expected {expect_balance} balance value, ' f'but got {actual_state} balance value for account {account_number}' ) with validates(f'account {account_number} balance lock'): expect_lock = account_state.balance_lock actual_lock = actual_account_state.balance_lock if actual_lock != expect_lock: raise ValidationError( f'Expected {expect_lock} balance lock, but got {actual_lock} balance ' f'lock for account {account_number}')
def add_block(self, block: Block, validate=True): block_number = block.message.block_number if validate: if block_number != self.get_next_block_number(): raise ValidationError('Block number must be equal to next block number (== head block number + 1)') block.validate(self) # TODO(dmu) HIGH: Validate block_identifier self.persist_block(block) period = self.snapshot_period_in_blocks # type: ignore if period is not None and (block_number + 1) % period == 0: self.snapshot_blockchain_state() # type: ignore
def validate(self): amount = self.amount validate_not_empty(f'{self.humanized_class_name} recipient', self.recipient) validate_type(f'{self.humanized_class_name} amount', amount, int) validate_gte_value(f'{self.humanized_class_name} amount', amount, 1) validate_in(f'{self.humanized_class_name} is_fee', self.is_fee, (True, False, None)) with validates('memo'): max_len = settings.MEMO_MAX_LENGTH if self.memo and len(self.memo) > max_len: raise ValidationError( f'{self.humanized_class_name} memo must be less than {max_len} characters' )
def validate_timestamp(self, blockchain): timestamp = self.timestamp if timestamp is None: raise ValidationError('Block message timestamp must be set') if not isinstance(timestamp, datetime): raise ValidationError( 'Block message timestamp must be datetime type') if timestamp.tzinfo is not None: raise ValidationError( 'Block message timestamp must be naive datetime (UTC timezone implied)' ) block_number = self.block_number assert block_number is not None if block_number > 0: prev_block_number = block_number - 1 prev_block = blockchain.get_block_by_number(prev_block_number) if prev_block is None: logger.debug('Partial blockchain detected') account_root_file = blockchain.get_closest_blockchain_state_snapshot( block_number) if account_root_file is None: raise ValidationError( 'Unexpected could not find base account root file') if account_root_file.is_initial(): raise ValidationError( 'Unexpected initial account root file found') if account_root_file.last_block_number != prev_block_number: raise ValidationError( 'Base account root file block number mismatch') assert account_root_file.last_block_timestamp min_timestamp = account_root_file.last_block_timestamp else: min_timestamp = prev_block.message.timestamp if timestamp <= min_timestamp: raise ValidationError( 'Block message timestamp must be greater than from previous block' )
def validate_message_hash(self): if self.message.get_hash() != self.message_hash: raise ValidationError('Block message hash is invalid')
def validate_message(self, blockchain): if not self.message: raise ValidationError('Block message must be not empty') self.message.validate(blockchain)
def validate_balance_lock(self, blockchain, in_block_number: int): if self.message.balance_lock != blockchain.get_account_balance_lock( self.signer, in_block_number - 1): raise ValidationError( 'Transfer request balance lock does not match expected balance lock' )
def validate_balance_lock(self): if not self.balance_lock: raise ValidationError( f'{self.humanized_class_name} balance lock must be set')