def validate_no_extra_keys(value, allowed_keys): extra_keys = tuple(sorted(set(value.keys()).difference(allowed_keys))) if extra_keys: raise ValidationError( "Only the keys '{}' are allowed. Got extra keys: '{}'".format( "/".join(tuple(sorted(allowed_keys))), "/".join(extra_keys), ) )
def time_travel(self, to_timestamp): self.validator.validate_inbound_timestamp(to_timestamp) # make sure we are not traveling back in time as this is not possible. current_timestamp = self.get_block_by_number('pending')['timestamp'] if to_timestamp <= current_timestamp: raise ValidationError( "Space time continuum distortion detected. Traveling backwards " "in time violates interdimensional ordinance 31415-926.") self.backend.time_travel(to_timestamp)
def validate_timestamp(value): validate_positive_integer(value) if value >= MAX_TIMESTAMP: raise ValidationError( "Timestamp values must be less than {0}. Got {1}".format( MAX_TIMESTAMP, value, ))
def validate_has_required_keys(value, required_keys): missing_keys = tuple(sorted(set(required_keys).difference(value.keys()))) if missing_keys: raise ValidationError( "Blocks must contain all of the keys '{}'. Missing the keys: '{}'" .format( "/".join(tuple(sorted(required_keys))), "/".join(missing_keys), ))
def _get_normalized_and_signed_evm_transaction(self, transaction, block_number='latest'): if transaction['from'] not in self._key_lookup: raise ValidationError( 'No valid "from" key was provided in the transaction ' 'which is required for transaction signing.' ) signing_key = self._key_lookup[transaction['from']] normalized_transaction = self._normalize_transaction(transaction, block_number) evm_transaction = self.chain.create_unsigned_transaction(**normalized_transaction) return evm_transaction.as_signed_transaction(signing_key)
def validate_array(value, validator): validate_is_list_like(value) item_errors = _accumulate_array_errors(value, validator) if item_errors: item_messages = tuple("[{}]: {}".format(index, str(err)) for index, err in sorted(item_errors)) error_message = ("The following items failed to validate\n" "- {}".format("\n - ".join(item_messages))) raise ValidationError(error_message)
def _validate_outbound_access_list(access_list): if not is_list_like(access_list): raise ValidationError('access_list is not list-like.') for entry in access_list: if not is_list_like(entry) and len(entry) != 2: raise ValidationError( f'access_list entry not properly formatted: {entry}') address = entry[0] storage_keys = entry[1] if not (is_bytes(address) and len(address) == 20): raise ValidationError( f'access_list address not properly formatted: {address}') if not is_list_like(storage_keys): raise ValidationError( f'access_list storage keys are not list-like: {storage_keys}') if len(storage_keys) > 0 and (not all( is_integer(k) for k in storage_keys)): raise ValidationError( f'one or more access list storage keys not formatted properly: {storage_keys}' )
def unlock_account(self, account, password, unlock_seconds=None): self.validator.validate_inbound_account(account) raw_account = self.normalizer.normalize_inbound_account(account) try: account_password = self._account_passwords[raw_account] except KeyError: raise ValidationError("Unknown account") if account_password is None: raise ValidationError("Account does not have a password") if account_password != password: raise ValidationError("Wrong password") if unlock_seconds is None: unlock_until = None else: unlock_until = time.time() + unlock_seconds self._account_unlock[raw_account] = unlock_until
def validate_dict(value, key_validators): validate_is_dict(value) validate_no_extra_keys(value, key_validators.keys()) validate_has_required_keys(value, key_validators.keys()) key_errors = _accumulate_dict_errors(value, key_validators) if key_errors: key_messages = tuple("{}: {}".format(key, str(err)) for key, err in sorted(key_errors.items())) error_message = ("The following keys failed to validate\n" "- {}".format("\n - ".join(key_messages))) raise ValidationError(error_message)
def validate_transaction(transaction): # TODO: refactor this so that it's not gross. if 'from' not in transaction: raise ValidationError("Transactions must specify a 'from' address") elif not is_address(transaction['from']): raise ValidationError( "transaction[from]: Unrecognized address format: {0}".format( transaction['from'], )) elif 'to' in transaction and not is_address(transaction['to']): raise ValidationError( "transaction[to]: Unrecognized address format: {0}".format( transaction['to'], )) extra_keys = set(transaction.keys()).difference(TRANSACTION_KEYS) if extra_keys: raise ValidationError( "Transactions may only include the keys {0}. The following extra " "keys were found: {1}".format( ",".join(sorted(TRANSACTION_KEYS)), ",".join(sorted(extra_keys)), ))
def serialize_transaction(block, transaction, transaction_index, is_pending): txn_type = _extract_transaction_type(transaction) common_transaction_params = { "type": txn_type, "hash": transaction.hash, "nonce": transaction.nonce, "block_hash": None if is_pending else block.hash, "block_number": None if is_pending else block.number, "transaction_index": None if is_pending else transaction_index, "from": transaction.sender, "to": transaction.to, "value": transaction.value, "gas": transaction.gas, "data": transaction.data, "r": transaction.r, "s": transaction.s, "v": transaction.v if _field_in_transaction(transaction, 'v') else transaction.y_parity, } if _field_in_transaction(transaction, 'gas_price'): type_specific_params = {'gas_price': transaction.gas_price} if _field_in_transaction(transaction, 'access_list'): # access list transaction type_specific_params = merge( type_specific_params, { 'chain_id': transaction.chain_id, 'access_list': transaction.access_list or (), } ) elif any(_field_in_transaction(transaction, _) for _ in ( 'max_fee_per_gas' and 'max_priority_fee_per_gas' )): # dynamic fee transaction type_specific_params = { 'chain_id': transaction.chain_id, 'max_fee_per_gas': transaction.max_fee_per_gas, 'max_priority_fee_per_gas': transaction.max_priority_fee_per_gas, 'access_list': transaction.access_list or (), # TODO: Sometime in 2022 the inclusion of gas_price may be removed from dynamic fee # transactions and we can get rid of this behavior. # https://github.com/ethereum/execution-specs/pull/251 'gas_price': ( transaction.max_fee_per_gas if is_pending else _calculate_effective_gas_price(transaction, block, txn_type) ), } else: raise ValidationError('Invariant: code path should be unreachable') return merge(common_transaction_params, type_specific_params)
def _validate_inbound_access_list(access_list): """ Validates the structure of an inbound access list. This is similar to the JSON-RPC structure for an access list only with `under_score` keys rather than `camelCase`. >>> _access_list = ( ... { ... 'address': '0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae', ... 'storage_keys': ( ... '0x0000000000000000000000000000000000000000000000000000000000000003', ... '0x0000000000000000000000000000000000000000000000000000000000000007', ... ) ... }, ... { ... 'address': '0xbb9bc244d798123fde783fcc1c72d3bb8c189413', ... 'storage_keys': () ... }, ... ) """ if not is_list_like(access_list): raise ValidationError('access_list is not list-like') for entry in access_list: if not is_dict(entry) and len(entry) != 2: raise ValidationError( f'access_list entry not properly formatted: {entry}') address = entry.get('address') storage_keys = entry.get('storage_keys') if not is_hex_address(address): raise ValidationError( f'access_list address must be a hexadecimal address: {address}' ) if not is_list_like(storage_keys): raise ValidationError( f'access_list storage keys are not list-like: {storage_keys}') if len(storage_keys) > 0 and not all( is_32byte_hex_string(k) for k in storage_keys): raise ValidationError( f'one or more access list storage keys not formatted properly: {storage_keys}' )
def add_account(self, private_key, password=None): # TODO: validation self.validator.validate_inbound_private_key(private_key) raw_private_key = self.normalizer.normalize_inbound_private_key(private_key) raw_account = private_key_to_address(raw_private_key) account = self.normalizer.normalize_outbound_account(raw_account) if any((is_same_address(account, value) for value in self.get_accounts())): raise ValidationError("Account already present in account list") self.backend.add_account(raw_private_key) self._account_passwords[raw_account] = password # TODO: outbound normalization return account
def get_transaction_receipt(self, transaction_hash): self.validator.validate_inbound_transaction_hash(transaction_hash) raw_transaction_hash = self.normalizer.normalize_inbound_transaction_hash( transaction_hash, ) raw_receipt = self.backend.get_transaction_receipt(raw_transaction_hash) self.validator.validate_outbound_receipt(raw_receipt) receipt = self.normalizer.normalize_outbound_receipt(raw_receipt) # Assume backend supports Byzantium status = to_int(receipt.pop('state_root')) if status > 1: raise ValidationError('Invalid status value: only 0 or 1 are valid') return assoc(receipt, 'status', status)
def validate_transaction(value, txn_type): if txn_type not in ALLOWED_TRANSACTION_TYPES: raise TypeError("the `txn_type` parameter must be one of send/call/estimate") if not is_dict(value): raise ValidationError("Transaction must be a dictionary. Got: {0}".format(type(value))) unknown_keys = tuple(sorted(set(value.keys()).difference(TRANSACTION_KEYS))) if unknown_keys: raise ValidationError( "Only the keys '{0}' are allowed. Got extra keys: '{1}'".format( "/".join(tuple(sorted(TRANSACTION_KEYS))), "/".join(unknown_keys), ) ) if txn_type == 'send': required_keys = {'from', 'gas'} elif txn_type in {'estimate', 'call'}: required_keys = set(['from']) else: raise Exception("Invariant: code path should be unreachable") missing_required_keys = tuple(sorted(required_keys.difference(value.keys()))) if missing_required_keys: raise ValidationError( "Transaction is missing the required keys: '{0}'".format( "/".join(missing_required_keys), ) ) if 'from' in value: validate_account(value['from']) if 'to' in value and value['to'] != '': validate_account(value['to']) if 'gas' in value: validate_uint256(value['gas']) if 'gas_price' in value: validate_uint256(value['gas_price']) if 'value' in value: validate_uint256(value['value']) if 'data' in value: bad_data_message = ( "Transaction data must be a hexidecimal encoded string. Got: " "{0}".format(value['data']) ) if not is_text(value['data']): raise ValidationError(bad_data_message) elif not remove_0x_prefix(value['data']): pass elif not is_hex(value['data']): raise ValidationError(bad_data_message) try: decode_hex(value['data']) except (binascii.Error, TypeError): # TypeError is for python2 # binascii.Error is for python3 raise ValidationError(bad_data_message)
def validate_any(value, validators): errors = _accumulate_errors(value, validators) if len(errors) == len(validators): item_error_messages = tuple( " - [{}]: {}".format(idx, str(err)) for idx, err in errors ) error_message = ( "Value did not pass any of the provided validators:\n" "{}".format( "\n".join(item_error_messages) ) ) raise ValidationError(error_message)
def validate_filter_params(from_block, to_block, address, topics): # blocks if from_block is not None: validate_block_number(from_block) if to_block is not None: validate_block_number(to_block) # address if address is None: pass elif is_list_like(address): if not address: raise ValidationError( "Address must be either a single hexidecimal encoded address or " "a non-empty list of hexidecimal encoded addresses" ) for sub_address in address: validate_account(sub_address) elif not is_hex_address(address): validate_account(address) invalid_topics_message = ( "Topics must be one of `None`, an array of 32 byte hexidecimal encoded " "strings, or an array of arrays of 32 byte hexidecimal strings" ) # topics if topics is None: pass elif not is_list_like(topics): raise ValidationError(invalid_topics_message) elif _is_flat_topic_array(topics): return True elif all(_is_flat_topic_array(item) for item in topics): return True else: raise ValidationError(invalid_topics_message)
def mine_blocks(self, num_blocks=1, coinbase=None): if coinbase is None: raw_coinbase = None else: self.validator.validate_inbound_account(coinbase) raw_coinbase = self.normalizer.normalize_inbound_account(coinbase) if not self.auto_mine_transactions: self._pop_pending_transactions_to_pending_block() raw_block_hashes = self.backend.mine_blocks(num_blocks, raw_coinbase) if len(raw_block_hashes) != num_blocks: raise ValidationError( "Invariant: tried to mine {0} blocks. Got {1} mined block hashes.".format( num_blocks, len(raw_block_hashes), ) ) for raw_block_hash in raw_block_hashes: self.validator.validate_outbound_block_hash(raw_block_hash) block_hashes = [ self.normalizer.normalize_outbound_block_hash(raw_block_hash) for raw_block_hash in raw_block_hashes ] # feed the block hashes to any block filters for block_hash in block_hashes: block = self.get_block_by_hash(block_hash) for _, block_filter in self._block_filters.items(): raw_block_hash = self.normalizer.normalize_inbound_block_hash(block_hash) block_filter.add(raw_block_hash) self._process_block_logs(block) return block_hashes
def get_transaction_receipt(self, transaction_hash): self.validator.validate_inbound_transaction_hash(transaction_hash) raw_transaction_hash = self.normalizer.normalize_inbound_transaction_hash( transaction_hash, ) raw_receipt = self.backend.get_transaction_receipt(raw_transaction_hash) self.validator.validate_outbound_receipt(raw_receipt) receipt = self.normalizer.normalize_outbound_receipt(raw_receipt) if 'FORK_BYZANTIUM' in self.get_supported_forks(): byzantium_fork_block = self.get_fork_block('FORK_BYZANTIUM') transaction = self.get_transaction_by_hash(transaction_hash) else: byzantium_fork_block = None if((byzantium_fork_block is not None) and (transaction['block_number'] is not None) and (transaction['block_number'] >= byzantium_fork_block) ): status = to_int(receipt.pop('state_root')) if status > 1: raise ValidationError('Invalid status value: only 0 or 1 are valid') return assoc(receipt, 'status', status) return receipt
def validate_is_list_like(value): if not is_list_like(value): raise ValidationError("Value must be a sequence type. Got: {}".format( type(value)))
def validate_is_dict(value): if not is_dict(value): raise ValidationError("Value must be a dictionary. Got: {}".format( type(value)))
def validate_text(value): if not is_text(value): raise ValidationError( "Value must be a text string. Got type: {}".format(type(value)))
def validate_bytes(value): if not is_bytes(value): raise ValidationError( "Value must be a byte string. Got type: {}".format(type(value)))
def validate_uint(max_val, value): validate_positive_integer(value) if value > max_val: bitsize = int(math.log2(max_val)) raise ValidationError( f"Value exceeds maximum {bitsize:d} bit integer size: {value}")
def validate_32_byte_hex_value(value, name): error_message = ("{0} must be a hexidecimal encoded 32 byte string. Got: " "{1}".format(name, value)) if not is_32byte_hex_string(value): raise ValidationError(error_message)
def validate_step(step): if step < 0: raise ValidationError("Step number must be non-negative")
def validate_raw_transaction(raw_transaction): if not is_text(raw_transaction) or not is_hex(raw_transaction): raise ValidationError( "Raw Transaction must be a hexidecimal encoded string. Got: " "{}".format(raw_transaction) )
def validate_private_key(value): if not is_text(value) or not is_hex(value) or not len(remove_0x_prefix(value)) == 64: raise ValidationError("Private keys must be 32 bytes encoded as hexidecimal")
def validate_positive_integer(value): error_message = "Value must be positive integers. Got: {}".format(value, ) if not is_integer(value) or is_boolean(value): raise ValidationError(error_message) elif value < 0: raise ValidationError(error_message)
def patched_validate_signature_v(value): validate_positive_integer(value) if value not in {0, 1, 27, 28, 37, 38}: raise ValidationError( "The `v` portion of the signature must be 0, 1, 27, 28, 37 or 38, not %d" % value)