예제 #1
0
    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
예제 #2
0
    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'
                )
예제 #3
0
    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}
        )
예제 #4
0
    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
예제 #5
0
    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
예제 #6
0
    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')
예제 #7
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)
예제 #8
0
 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')
예제 #9
0
    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}'
            )
예제 #10
0
    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
예제 #11
0
    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')
예제 #12
0
 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'
             )
예제 #13
0
    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'
                )
예제 #14
0
    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)
예제 #15
0
    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)
예제 #16
0
 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)
예제 #18
0
    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'
            )
예제 #19
0
    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()
예제 #20
0
    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)'
                )
예제 #21
0
    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}')
예제 #22
0
    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
예제 #23
0
    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'
                )
예제 #24
0
    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'
                )
예제 #25
0
 def validate_message_hash(self):
     if self.message.get_hash() != self.message_hash:
         raise ValidationError('Block message hash is invalid')
예제 #26
0
    def validate_message(self, blockchain):
        if not self.message:
            raise ValidationError('Block message must be not empty')

        self.message.validate(blockchain)
예제 #27
0
 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'
         )
예제 #28
0
 def validate_balance_lock(self):
     if not self.balance_lock:
         raise ValidationError(
             f'{self.humanized_class_name} balance lock must be set')