def validate_registry_uri(uri: str) -> None: """ Raise an exception if the URI does not conform to the registry URI scheme. """ parsed = parse.urlparse(uri) scheme, authority, pkg_path = ( parsed.scheme, parsed.netloc, parsed.path, ) pkg_id = pkg_path.strip("/") if "@" in pkg_id: if len(pkg_id.split("@")) != 2: raise EthPMValidationError( "Registry URI: {pkg_id} is not properly escaped") pkg_name, pkg_version = pkg_id.split("@") else: pkg_name, pkg_version = (pkg_id, None) validate_registry_uri_scheme(scheme) validate_registry_uri_authority(authority) if pkg_name: validate_package_name(pkg_name) if not pkg_name and pkg_version: raise EthPMValidationError( "Registry URIs cannot provide a version without a package name.") if pkg_version: validate_escaped_string(pkg_version)
def validate_build_dependencies_are_present(manifest: Dict[str, Any]) -> None: if "build_dependencies" not in manifest: raise EthPMValidationError( "Manifest doesn't have any build dependencies.") if not manifest["build_dependencies"]: raise EthPMValidationError( "Manifest's build dependencies key is empty.")
def validate_deployments_tx_receipt( deployments: Dict[str, Any], w3: "Web3", allow_missing_data: bool = False ) -> None: """ Validate that address and block hash found in deployment data match what is found on-chain. :allow_missing_data: by default, enforces validation of address and blockHash. """ # todo: provide hook to lazily look up tx receipt via binary search if missing data for name, data in deployments.items(): if "transaction" in data: tx_hash = data["transaction"] tx_receipt = w3.eth.get_transaction_receipt(tx_hash) # tx_address will be None if contract created via contract factory tx_address = tx_receipt["contractAddress"] if tx_address is None and allow_missing_data is False: raise EthPMValidationError( "No contract address found in tx receipt. Unable to verify " "address found in tx receipt matches address in manifest's deployment data. " "If this validation is not necessary, please enable `allow_missing_data` arg. " ) if tx_address is not None and not is_same_address( tx_address, data["address"] ): raise EthPMValidationError( f"Error validating tx_receipt for {name} deployment. " f"Address found in manifest's deployment data: {data['address']} " f"Does not match address found on tx_receipt: {tx_address}." ) if "block" in data: if tx_receipt["blockHash"] != to_bytes(hexstr=data["block"]): raise EthPMValidationError( f"Error validating tx_receipt for {name} deployment. " f"Block found in manifest's deployment data: {data['block']!r} does not " f"Does not match block found on tx_receipt: {tx_receipt['blockHash']!r}." ) elif allow_missing_data is False: raise EthPMValidationError( "No block hash found in deployment data. " "Unable to verify block hash on tx receipt. " "If this validation is not necessary, please enable `allow_missing_data` arg." ) elif allow_missing_data is False: raise EthPMValidationError( "No transaction hash found in deployment data. " "Unable to validate tx_receipt. " "If this validation is not necessary, please enable `allow_missing_data` arg." )
def validate_raw_manifest_format(raw_manifest: str) -> None: """ Raise a EthPMValidationError if a manifest ... - is not tightly packed (i.e. no linebreaks or extra whitespace) - does not have alphabetically sorted keys - has duplicate keys - is not UTF-8 encoded - has a trailing newline """ try: manifest_dict = json.loads(raw_manifest, encoding="UTF-8") except json.JSONDecodeError as err: raise json.JSONDecodeError( "Failed to load package data. File is not a valid JSON document.", err.doc, err.pos, ) compact_manifest = json.dumps(manifest_dict, sort_keys=True, separators=(",", ":")) if raw_manifest != compact_manifest: raise EthPMValidationError( "The manifest appears to be malformed. Please ensure that it conforms to the " "EthPM-Spec for document format. " "http://ethpm.github.io/ethpm-spec/package-spec.html#document-format " )
def validate_manifest_exists(manifest_id: str) -> None: """ Validate that manifest with manifest_id exists in ASSETS_DIR """ if not (ASSETS_DIR / manifest_id).is_file(): raise EthPMValidationError( f"Manifest not found in ASSETS_DIR with id: {manifest_id}")
def validate_escaped_string(string: str) -> None: unsafe = parse.unquote(string) safe = parse.quote(unsafe) if string != safe: raise EthPMValidationError( f"String: {string} is not properly escaped, and contains url unsafe characters." )
def validate_registry_uri_scheme(scheme: str) -> None: """ Raise an exception if the scheme is not the valid registry URI scheme ('ercXXX'). """ if scheme != REGISTRY_URI_SCHEME: raise EthPMValidationError( f"{scheme} is not a valid registry URI scheme.")
def __init__( self, manifest: Dict[str, Any], w3: Web3, uri: Optional[str] = None ) -> None: """ A package should be created using one of the available classmethods and a valid w3 instance. """ if not isinstance(manifest, dict): raise TypeError( "Package object must be initialized with a dictionary. " f"Got {type(manifest)}" ) if "manifest" not in manifest or manifest["manifest"] != "ethpm/3": raise EthPMValidationError( "Py-Ethpm currently only supports v3 ethpm manifests. " "Please use the CLI to update or re-generate a v3 manifest. " ) validate_manifest_against_schema(manifest) validate_manifest_deployments(manifest) validate_w3_instance(w3) self.w3 = w3 self.w3.eth.defaultContractFactory = cast(Type[Contract], LinkableContract) self.manifest = manifest self._uri = uri
def validate_package_name(pkg_name: str) -> None: """ Raise an exception if the value is not a valid package name as defined in the EthPM-Spec. """ if not bool(re.match(PACKAGE_NAME_REGEX, pkg_name)): raise EthPMValidationError(f"{pkg_name} is not a valid package name.")
def _build_dependency(package_name: str, uri: URI, manifest: Manifest) -> Manifest: validate_package_name(package_name) if not is_supported_content_addressed_uri(uri): raise EthPMValidationError( f"{uri} is not a supported content-addressed URI. " "Currently only IPFS and Github blob uris are supported.") return assoc_in(manifest, ("buildDependencies", package_name), uri)
def validate_registry_uri_version(query: str) -> None: """ Raise an exception if the version param is malformed. """ query_dict = parse.parse_qs(query, keep_blank_values=True) if "version" not in query_dict: raise EthPMValidationError( f"{query} is not a correctly formatted version param.")
def validate_manifest_version(version: str) -> None: """ Raise an exception if the version is not "ethpm/3". """ if not version == "ethpm/3": raise EthPMValidationError( f"Py-EthPM does not support the provided specification version: {version}" )
def validate_package_version(version: Any) -> None: """ Validates that a package version is of text type. """ if not is_text(version): raise EthPMValidationError( f"Expected a version of text type, instead received {type(version)}." )
def validate_link_ref(offset: int, length: int, bytecode: str) -> str: slot_length = offset + length slot = bytecode[offset:slot_length] if slot[:2] != "__" and slot[-2:] != "__": raise EthPMValidationError( f"Slot: {slot}, at offset: {offset} of length: {length} is not a valid " "link_ref that can be replaced.") return bytecode
def validate_build_dependency(key: str, uri: str) -> None: """ Raise an exception if the key in dependencies is not a valid package name, or if the value is not a valid IPFS URI. """ validate_package_name(key) # validate is supported content-addressed uri if not is_ipfs_uri(uri): raise EthPMValidationError(f"URI: {uri} is not a valid IPFS URI.")
def validate_registry_uri_scheme(scheme: str) -> None: """ Raise an exception if the scheme is not a valid registry URI scheme: - 'erc1319' - 'ethpm' """ if scheme not in REGISTRY_URI_SCHEMES: raise EthPMValidationError( f"{scheme} is not a valid registry URI scheme. " f"Valid schemes include: {REGISTRY_URI_SCHEMES}")
def validate_meta_object(meta: Dict[str, Any], allow_extra_meta_fields: bool) -> None: """ Validates that every key is one of `META_FIELDS` and has a value of the expected type. """ for key, value in meta.items(): if key in META_FIELDS: if type(value) is not META_FIELDS[key]: raise EthPMValidationError( f"Values for {key} are expected to have the type {META_FIELDS[key]}, " f"instead got {type(value)}.") elif allow_extra_meta_fields: if key[:2] != "x-": raise EthPMValidationError( "Undefined meta fields need to begin with 'x-', " f"{key} is not a valid undefined meta field.") else: raise EthPMValidationError( f"{key} is not a permitted meta field. To allow undefined fields, " "set `allow_extra_meta_fields` to True.")
def validate_single_matching_uri(all_blockchain_uris: List[str], w3: "Web3") -> str: """ Return a single block URI after validating that it is the *only* URI in all_blockchain_uris that matches the w3 instance. """ from ethpm.uri import check_if_chain_matches_chain_uri matching_uris = [ uri for uri in all_blockchain_uris if check_if_chain_matches_chain_uri(w3, uri) ] if not matching_uris: raise EthPMValidationError("Package has no matching URIs on chain.") elif len(matching_uris) != 1: raise EthPMValidationError( f"Package has too many ({len(matching_uris)}) matching URIs: {matching_uris}." ) return matching_uris[0]
def validate_empty_bytes(offset: int, length: int, bytecode: bytes) -> None: """ Validates that segment [`offset`:`offset`+`length`] of `bytecode` is comprised of empty bytes (b'\00'). """ slot_length = offset + length slot = bytecode[offset:slot_length] if slot != bytearray(length): raise EthPMValidationError( f"Bytecode segment: [{offset}:{slot_length}] is not comprised of empty bytes, " f"rather: {slot}.")
def _get_all_contract_instances(self, deployments): for deployment_name, deployment_data in deployments.items(): if deployment_data['contract_type'] not in self.contract_types: raise EthPMValidationError( f"Contract type: {deployment_data['contract_type']} for alias: " f"{deployment_name} not found. Available contract types include: " f"{self.contract_types}.") contract_instance = self.get_contract_instance( deployment_data['contract_type'], deployment_data['address'], ) yield deployment_name, contract_instance
def fetch_uri_contents(self, uri: str) -> bytes: ipfs_hash = extract_ipfs_path_from_uri(uri) contents = self.client.cat(ipfs_hash) # Local validation of hashed contents only works for non-chunked files ~< 256kb # Improved validation WIP @ https://github.com/ethpm/py-ethpm/pull/165 if len(contents) <= 262144: validation_hash = generate_file_hash(contents) if validation_hash != ipfs_hash: raise EthPMValidationError( f"Hashed IPFS contents retrieved from uri: {uri} do not match its content hash." ) return contents
def validate_manifest_against_schema(manifest: Dict[str, Any]) -> None: """ Load and validate manifest against schema located at MANIFEST_SCHEMA_PATH. """ schema_data = _load_schema_data() try: validate(manifest, schema_data) except jsonValidationError as e: raise EthPMValidationError( f"Manifest invalid for schema version {schema_data['version']}. " f"Reason: {e.message}")
def validate_registry_uri_authority(auth: str) -> None: """ Raise an exception if the authority is not a valid ENS domain or a valid checksummed contract address. """ try: address, chain_id = auth.split(':') except ValueError: raise EthPMValidationError( f"{auth} is not a valid registry URI authority. " "Please try again with a valid registry URI.") if is_ens_domain(address) is False and not is_checksum_address(address): raise EthPMValidationError( f"{address} is not a valid registry address. " "Please try again with a valid registry URI.") if not is_supported_chain_id(to_int(text=chain_id)): raise EthPMValidationError( f"Chain ID: {chain_id} is not supported. Supported chain ids include: " "1 (mainnet), 3 (ropsten), 4 (rinkeby), 5 (goerli) and 42 (kovan). " "Please try again with a valid registry URI.")
def _process_pkg_path( raw_pkg_path: str ) -> Tuple[Optional[str], Optional[str], Optional[str]]: pkg_path = raw_pkg_path.strip("/") if not pkg_path: return None, None, None pkg_id, namespaced_asset = _parse_pkg_path(pkg_path) pkg_name, pkg_version = _parse_pkg_id(pkg_id) if not pkg_version and namespaced_asset: raise EthPMValidationError( "Invalid registry URI, missing package version." "Version is required if namespaced assets are defined.") return pkg_name, pkg_version, namespaced_asset
def _get_all_contract_instances( self, deployments: Dict[str, DeploymentData] ) -> Iterable[Tuple[str, Contract]]: for deployment_name, deployment_data in deployments.items(): if deployment_data['contractType'] not in self.contract_types: raise EthPMValidationError( f"Contract type: {deployment_data['contractType']} for alias: " f"{deployment_name} not found. Available contract types include: " f"{self.contract_types}." ) contract_instance = self.get_contract_instance( ContractName(deployment_data['contractType']), deployment_data['address'], ) yield deployment_name, contract_instance
def validate_manifest_against_schema(manifest: Dict[str, Any]) -> None: """ Load and validate manifest against schema located at v3_schema_path. """ schema_data = _load_schema_data() try: validate(manifest, schema_data, cls=validator_for(schema_data, Draft7Validator)) except jsonValidationError as e: raise EthPMValidationError( f"Manifest invalid for schema version {schema_data['version']}. " f"Reason: {e.message}" f"{e}")
def validate_manifest_deployments(manifest: Dict[str, Any]) -> None: """ Validate that a manifest's deployments contracts reference existing contract_types. """ if set(("contract_types", "deployments")).issubset(manifest): all_contract_types = list(manifest["contract_types"].keys()) all_deployments = list(manifest["deployments"].values()) all_deployment_names = extract_contract_types_from_deployments(all_deployments) missing_contract_types = set(all_deployment_names).difference( all_contract_types ) if missing_contract_types: raise EthPMValidationError( f"Manifest missing references to contracts: {missing_contract_types}." )
def _validate_name_and_references(self, name: str) -> None: validate_contract_name(name) if name not in self.deployment_data: raise KeyError( "Contract name not found in deployment data. " f"Available deployments include: {list(sorted(self.deployment_data.keys()))}." ) contract_type = self.deployment_data[name]["contract_type"] if contract_type not in self.contract_factories: raise EthPMValidationError( f"Contract type: {contract_type} for alias: {name} not found. " f"Available contract types include: {list(sorted(self.contract_factories.keys()))}." )
def validate_blob_uri_contents(contents: bytes, blob_uri: str) -> None: """ Raises an exception if the sha1 hash of the contents does not match the hash found in te blob_uri. Formula for how git calculates the hash found here: http://alblue.bandlem.com/2011/08/git-tip-of-week-objects.html """ blob_path = parse.urlparse(blob_uri).path blob_hash = blob_path.split("/")[-1] contents_str = to_text(contents) content_length = len(contents_str) hashable_contents = "blob " + str(content_length) + "\0" + contents_str hash_object = hashlib.sha1(to_bytes(text=hashable_contents)) if hash_object.hexdigest() != blob_hash: raise EthPMValidationError( f"Hash of contents fetched from {blob_uri} do not match its hash: {blob_hash}." )
def validate_linked_references(link_deps: Tuple[Tuple[int, bytes], ...], bytecode: HexBytes) -> None: """ Validates that normalized linked_references (offset, expected_bytes) match the corresponding bytecode. """ offsets, values = zip(*link_deps) for idx, offset in enumerate(offsets): value = values[idx] # https://github.com/python/mypy/issues/4975 offset_value = int(offset) dep_length = len(value) end_of_bytes = offset_value + dep_length # Ignore b/c whitespace around ':' conflict b/w black & flake8 actual_bytes = bytecode[offset_value:end_of_bytes] # noqa: E203 if actual_bytes != values[idx]: raise EthPMValidationError("Error validating linked reference. " f"Offset: {offset} " f"Value: {values[idx]} " f"Bytecode: {bytecode} .")