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 validate(self, blockchain): with validates(f'block number {self.message.block_number} (identifier: {self.message.block_identifier})'): validate_not_empty(f'{self.humanized_class_name} message', self.message) self.message.validate(blockchain) validate_exact_value(f'{self.humanized_class_name} hash', self.hash, self.message.get_hash()) with validates('block signature'): self.validate_signature()
def validate(self, blockchain): with validates( f'block number {self.message.block_number} (identifier: {self.message.block_identifier})' ): self.validate_message(blockchain) self.validate_message_hash() with validates('block signature'): self.validate_signature()
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_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 validate_type(subject, value, type_): with validates(f'{subject} type'): if not isinstance(value, type_): raise ValidationError( upper_first( f'{subject} must be {HUMANIZED_TYPE_NAMES.get(type_, type_.__name__)}' ))
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_transactions(self): txs = self.txs validate_type(f'{self.humanized_class_name} txs', txs, list) validate_not_empty(f'{self.humanized_class_name} txs', txs) for tx in self.txs: with validates(f'Validating transaction {tx} on {self.get_humanized_class_name(False)} level'): validate_type(f'{self.humanized_class_name} txs', tx, CoinTransferTransaction) tx.validate()
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 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_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_updated_account_states(self, blockchain): updated_account_states = self.updated_account_states humanized_class_name = self.humanized_class_name_lowered validate_not_empty(f'{humanized_class_name} updated_account_states', updated_account_states) from .signed_change_request import CoinTransferSignedChangeRequest if isinstance(self.signed_change_request, CoinTransferSignedChangeRequest): validate_min_item_count( f'{humanized_class_name} updated_account_states', updated_account_states, 2) signer = self.signed_change_request.signer sender_account_state = self.updated_account_states.get(signer) validate_not_empty( f'{humanized_class_name} updated_account_states.{signer}', sender_account_state) for account_number, account_state in updated_account_states.items( ): with validates( f'{humanized_class_name} account {account_number} updated state' ): account_subject = f'{humanized_class_name} updated_account_states key (account number)' validate_not_empty(account_subject, account_number) validate_type(account_subject, account_number, str) account_state.validate() is_sender = account_number == signer self.validate_updated_account_balance_lock( account_number=account_number, account_state=account_state, is_sender=is_sender) self.validate_updated_account_balance( account_number=account_number, account_state=account_state, blockchain=blockchain, is_sender=is_sender)
def validate_min_item_count(subject, value, min_): with validates(f'{subject} item count'): if len(value) < min_: raise ValidationError( upper_first(f'{subject} must contain at least {min_} items'))
def validate_hexadecimal(subject, value): with validates(f'{subject} value'): try: hexstr(value).to_bytes() except ValueError: raise ValidationError(upper_first(f'{subject} must be hexadecimal string'))
def validate_network_address(subject, value): with validates(f'{subject} value'): result = urlparse(value) validate_not_empty(f'{subject} scheme', result.scheme) validate_in(f'{subject} scheme', result.scheme, VALID_SCHEMES) validate_not_empty(f'{subject} hostname', result.hostname)
def validate_greater_than_zero(subject, value): with validates(f'{subject} value'): if value <= 0: raise ValidationError( upper_first(f'{subject} must be greater than zero'))
def validate_exact_value(subject, value, expected_value): with validates(f'{subject} value'): if value != expected_value: raise ValidationError( upper_first(f'{subject} must be equal to {expected_value}'))
def validate_lt_value(subject, value, max_): with validates(f'{subject} value'): if value >= max_: raise ValidationError( upper_first(f'{subject} must be less than {max_}'))
def validate_in(subject, value, value_set): with validates(f'{subject} value'): if value not in value_set: value_set_str = ', '.join(map(str, value_set)) raise ValidationError( upper_first(f'{subject} must be one of {value_set_str}'))
def validate_lte_value(subject, value, max_): with validates(f'{subject} value'): if value > max_: raise ValidationError( upper_first(f'{subject} must be less or equal to {max_}'))
def validate(self, blockchain, block_number: int): self.validate_message() with validates('block signature'): self.validate_signature()
def validate(self): self.validate_message() with validates('block signature'): self.validate_signature()
def validate_empty(subject, value): with validates(f'{subject} value'): if value: raise ValidationError(upper_first(f'{subject} must be empty'))
def validate_is_none(subject, value): with validates(f'{subject} value'): if value is not None: raise ValidationError(upper_first(f'{subject} must not be set'))
def validate_accounts(self): for account, balance in self.account_states.items(): with validates(f'blockchain state account {account}'): validate_type(f'{self.humanized_class_name} account', account, str) balance.validate()
def validate_gt_value(subject, value, min_): with validates(f'{subject} value'): if value <= min_: raise ValidationError( upper_first(f'{subject} must be greater than {min_}'))