diff --git a/.github/workflows/trunk_check.yml b/.github/workflows/trunk_check.yml index c5bea96e..0f1d9484 100644 --- a/.github/workflows/trunk_check.yml +++ b/.github/workflows/trunk_check.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - run: rustup update - name: Rust cache diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index cc3251be..6a82e2cb 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -2,18 +2,18 @@ # To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml version: 0.1 cli: - version: 1.22.7 + version: 1.22.8 # Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins) plugins: sources: - id: trunk - ref: v1.6.4 + ref: v1.6.6 uri: https://github.com/trunk-io/plugins # Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes) runtimes: enabled: - go@1.21.0 - - node@18.12.1 + - node@18.20.5 - python@3.10.8 downloads: - name: rust @@ -61,28 +61,28 @@ lint: read_output_from: stdout run_linter_from: workspace enabled: - - cspell@8.15.4 + - cspell@8.17.1 - dotenv-linter@3.3.0 - - actionlint@1.7.3 + - actionlint@1.7.4 - black@24.10.0 - cairo@SYSTEM - - rustfmt@2024-10-31 - - clippy@2024-10-31 - - checkov@3.2.269 + - rustfmt@1.65.0 + - clippy@1.65.0 + - checkov@3.2.342 - git-diff-check - - hadolint@2.12.0 + - hadolint@2.12.1-beta - isort@5.13.2 - - markdownlint@0.42.0 - - osv-scanner@1.9.0 - - oxipng@9.1.2 - - prettier@3.3.3 - - ruff@0.7.1 + - markdownlint@0.43.0 + - osv-scanner@1.9.2 + - oxipng@9.1.3 + - prettier@3.4.2 + - ruff@0.8.3 - shellcheck@0.10.0 - shfmt@3.6.0 - solidity@SYSTEM - taplo@0.9.3 - - trivy@0.56.2 - - trufflehog@3.82.13 + - trivy@0.58.0 + - trufflehog@3.87.2 - yamllint@1.35.1 actions: diff --git a/cairo/ethereum/cancun/fork_types.cairo b/cairo/ethereum/cancun/fork_types.cairo index 5ae20770..2249b8c5 100644 --- a/cairo/ethereum/cancun/fork_types.cairo +++ b/cairo/ethereum/cancun/fork_types.cairo @@ -1,10 +1,25 @@ from starkware.cairo.common.alloc import alloc from ethereum_types.bytes import Bytes20, Bytes256, Bytes, BytesStruct -from ethereum_types.numeric import Uint, U256, U256Struct +from ethereum_types.numeric import Uint, U256, U256Struct, bool from ethereum.crypto.hash import Hash32 using Address = Bytes20; + +struct SetAddressDictAccess { + key: Address, + prev_value: bool, + new_value: bool, +} + +struct SetAddressStruct { + dict_ptr_start: SetAddressDictAccess*, + dict_ptr: SetAddressDictAccess*, +} + +struct SetAddress { + value: SetAddressStruct*, +} using Root = Hash32; using VersionedHash = Hash32; diff --git a/cairo/ethereum/cancun/utils/address.cairo b/cairo/ethereum/cancun/utils/address.cairo index 0d36b6ed..3e071cad 100644 --- a/cairo/ethereum/cancun/utils/address.cairo +++ b/cairo/ethereum/cancun/utils/address.cairo @@ -4,11 +4,42 @@ from starkware.cairo.common.cairo_builtins import BitwiseBuiltin, KeccakBuiltin from starkware.cairo.common.math_cmp import is_nn, is_not_zero from ethereum_types.bytes import Bytes32, Bytes, BytesStruct -from ethereum_types.numeric import Uint +from ethereum_types.numeric import Uint, UnionUintU256 from ethereum.cancun.fork_types import Address from ethereum.crypto.hash import keccak256 from ethereum.utils.numeric import divmod -from src.utils.bytes import felt_to_bytes20_little, felt_to_bytes, uint256_to_bytes32_little + +from src.utils.bytes import ( + felt_to_bytes20_little, + bytes_to_felt, + felt_to_bytes, + uint256_to_bytes32_little, +) + +func to_address{range_check_ptr}(data: UnionUintU256) -> Address { + alloc_locals; + let (local bytes_data) = alloc(); + + if (cast(data.value.uint, felt) != 0) { + felt_to_bytes20_little(bytes_data, data.value.uint.value); + let res = bytes_to_felt(20, bytes_data); + tempvar address = Address(res); + return address; + } + + if (cast(data.value.u256.value, felt) != 0) { + uint256_to_bytes32_little(bytes_data, [data.value.u256.value]); + let res = bytes_to_felt(20, bytes_data); + tempvar address = Address(res); + return address; + } + + with_attr error_message("Type not valid") { + assert 0 = 1; + tempvar address = Address(0); + return address; + } +} func compute_contract_address{ range_check_ptr, bitwise_ptr: BitwiseBuiltin*, keccak_ptr: KeccakBuiltin* diff --git a/cairo/ethereum/cancun/vm/exceptions.cairo b/cairo/ethereum/cancun/vm/exceptions.cairo new file mode 100644 index 00000000..08851a03 --- /dev/null +++ b/cairo/ethereum/cancun/vm/exceptions.cairo @@ -0,0 +1,9 @@ +from ethereum_types.bytes import BytesStruct + +struct StackUnderflowError { + value: BytesStruct*, +} + +struct StackOverflowError { + value: BytesStruct*, +} diff --git a/cairo/ethereum/cancun/vm/stack.cairo b/cairo/ethereum/cancun/vm/stack.cairo new file mode 100644 index 00000000..dd3fa8e5 --- /dev/null +++ b/cairo/ethereum/cancun/vm/stack.cairo @@ -0,0 +1,64 @@ +from ethereum_types.numeric import U256, U256Struct +from ethereum_types.bytes import BytesStruct +from starkware.cairo.common.dict import DictAccess, dict_read, dict_write +from ethereum.cancun.vm.exceptions import StackOverflowError, StackUnderflowError + +struct Stack { + value: StackStruct*, +} + +struct StackStruct { + dict_ptr_start: StackDictAccess*, + dict_ptr: StackDictAccess*, + len: felt, +} + +struct StackDictAccess { + key: felt, + prev_value: U256, + new_value: U256, +} + +const STACK_MAX_SIZE = 1024; + +func pop{stack: Stack}() -> (U256, StackUnderflowError) { + alloc_locals; + let len = stack.value.len; + if (len == 0) { + tempvar err = StackUnderflowError(new BytesStruct(cast(0, felt*), 0)); + let val = U256(cast(0, U256Struct*)); + return (val, err); + } + + let dict_ptr = cast(stack.value.dict_ptr, DictAccess*); + with dict_ptr { + let (pointer) = dict_read(len - 1); + } + let new_dict_ptr = cast(dict_ptr, StackDictAccess*); + + tempvar stack = Stack(new StackStruct(stack.value.dict_ptr_start, new_dict_ptr, len - 1)); + tempvar value = U256(cast(pointer, U256Struct*)); + + let ok_ = StackUnderflowError(cast(0, BytesStruct*)); + return (value, ok_); +} + +func push{stack: Stack}(value: U256) -> StackOverflowError { + alloc_locals; + let len = stack.value.len; + if (len == STACK_MAX_SIZE) { + tempvar err = StackOverflowError(new BytesStruct(cast(0, felt*), 0)); + return err; + } + + let dict_ptr = cast(stack.value.dict_ptr, DictAccess*); + with dict_ptr { + dict_write(len, cast(value.value, felt)); + } + let new_dict_ptr = cast(dict_ptr, StackDictAccess*); + + tempvar stack = Stack(new StackStruct(stack.value.dict_ptr_start, new_dict_ptr, len + 1)); + let ok_ = StackOverflowError(cast(0, BytesStruct*)); + + return ok_; +} diff --git a/cairo/ethereum_types/numeric.cairo b/cairo/ethereum_types/numeric.cairo index eb3a1705..03d42ec9 100644 --- a/cairo/ethereum_types/numeric.cairo +++ b/cairo/ethereum_types/numeric.cairo @@ -20,6 +20,15 @@ struct U256 { value: U256Struct*, } +struct UnionUintU256Enum { + uint: Uint*, + u256: U256, +} + +struct UnionUintU256 { + value: UnionUintU256Enum*, +} + struct SetUintDictAccess { key: Uint, prev_value: bool, diff --git a/cairo/pyproject.toml b/cairo/pyproject.toml index b0bb39ce..978a9983 100644 --- a/cairo/pyproject.toml +++ b/cairo/pyproject.toml @@ -154,7 +154,7 @@ profile = "black" src_paths = ["src", "tests"] [tool.uv.sources] -ethereum = { git = "https://github.com/ethereum/execution-specs.git", rev = "1adcc1bfe774798bcacc685aebc17bd9935078c3" } +ethereum = { git = "https://github.com/kkrt-labs/execution-specs.git", branch = "dev/change-type-branch-nodes" } [build-system] requires = ["hatchling"] diff --git a/cairo/tests/ethereum/cancun/utils/test_address.py b/cairo/tests/ethereum/cancun/utils/test_address.py index 7fabc63a..9d151b0e 100644 --- a/cairo/tests/ethereum/cancun/utils/test_address.py +++ b/cairo/tests/ethereum/cancun/utils/test_address.py @@ -1,15 +1,23 @@ +from typing import Union + from ethereum_types.bytes import Bytes32 -from ethereum_types.numeric import Uint +from ethereum_types.numeric import U256, Uint from hypothesis import given from ethereum.cancun.fork_types import Address from ethereum.cancun.utils.address import ( compute_contract_address, compute_create2_contract_address, + to_address, ) class TestAddress: + + @given(data=...) + def test_to_address(self, cairo_run, data: Union[Uint, U256]): + assert to_address(data) == cairo_run("to_address", data) + @given(address=..., nonce=...) def test_compute_contract_address(self, cairo_run, address: Address, nonce: Uint): assert compute_contract_address(address, nonce) == cairo_run( diff --git a/cairo/tests/ethereum/cancun/vm/test_stack.py b/cairo/tests/ethereum/cancun/vm/test_stack.py new file mode 100644 index 00000000..3a9bd456 --- /dev/null +++ b/cairo/tests/ethereum/cancun/vm/test_stack.py @@ -0,0 +1,40 @@ +from typing import List + +import pytest +from ethereum_types.numeric import U256 +from hypothesis import assume, given + +from ethereum.cancun.vm.exceptions import StackOverflowError, StackUnderflowError +from ethereum.cancun.vm.stack import pop, push + + +class TestStack: + def test_pop_underflow(self, cairo_run): + stack = [] + with pytest.raises(StackUnderflowError): + cairo_run("pop", stack) + with pytest.raises(StackUnderflowError): + pop(stack) + + @given(stack=...) + def test_pop_success(self, cairo_run, stack: List[U256]): + assume(len(stack) > 0) + + (new_stack_cairo, popped_value_cairo) = cairo_run("pop", stack) + popped_value_py = pop(stack) + assert new_stack_cairo == stack + assert popped_value_cairo == popped_value_py + + @given(value=...) + def test_push_overflow(self, cairo_run, value: U256): + stack = [U256(0)] * 1024 + with pytest.raises(StackOverflowError): + cairo_run("push", stack, value) + with pytest.raises(StackOverflowError): + push(stack, value) + + @given(stack=..., value=...) + def test_push_success(self, cairo_run, stack: List[U256], value: U256): + new_stack_cairo = cairo_run("push", stack, value) + push(stack, value) + assert new_stack_cairo == stack diff --git a/cairo/tests/fixtures/runner.py b/cairo/tests/fixtures/runner.py index 840c5064..c4f8cef5 100644 --- a/cairo/tests/fixtures/runner.py +++ b/cairo/tests/fixtures/runner.py @@ -38,7 +38,7 @@ from tests.utils.args_gen import to_cairo_type, to_python_type from tests.utils.hints import debug_info, get_op, oracle from tests.utils.reporting import profile_from_tracer_data -from tests.utils.serde import Serde +from tests.utils.serde import NO_ERROR_FLAG, Serde logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" @@ -332,12 +332,13 @@ def _factory(entrypoint, *args, **kwargs): final_output = serde.serialize_list(output_ptr) cumulative_retdata_offsets = serde.get_offsets(return_data_types) - function_output = [ + unfiltered_output = [ serde.serialize(return_data_type, runner.vm.run_context.ap, offset) for offset, return_data_type in zip( cumulative_retdata_offsets, return_data_types ) ] + function_output = [x for x in unfiltered_output if x is not NO_ERROR_FLAG] if final_output is not None: if len(function_output) > 0: diff --git a/cairo/tests/test_serde.py b/cairo/tests/test_serde.py index 17dd290b..a936efc3 100644 --- a/cairo/tests/test_serde.py +++ b/cairo/tests/test_serde.py @@ -1,4 +1,4 @@ -from typing import Any, Mapping, Optional, Set, Tuple, Type, Union +from typing import Annotated, Any, Mapping, Optional, Set, Tuple, Type, Union import pytest from ethereum_types.bytes import Bytes, Bytes0, Bytes8, Bytes20, Bytes32, Bytes256 @@ -168,6 +168,9 @@ def test_type( Tuple[Mapping[Bytes, Bytes], ...], Set[Uint], Mapping[Address, Account], + Union[Uint, U256], + Set[Address], + Annotated[Tuple[VersionedHash, ...], 16], ], ): assume(no_empty_sequence(b)) diff --git a/cairo/tests/utils/args_gen.py b/cairo/tests/utils/args_gen.py index a4bee1ba..9da7f60c 100644 --- a/cairo/tests/utils/args_gen.py +++ b/cairo/tests/utils/args_gen.py @@ -53,10 +53,13 @@ from dataclasses import fields, is_dataclass from functools import partial from typing import ( + Annotated, Any, Dict, ForwardRef, + List, Mapping, + Optional, Sequence, Set, Tuple, @@ -103,6 +106,7 @@ Transaction, ) from ethereum.cancun.trie import BranchNode, ExtensionNode, InternalNode, LeafNode, Node +from ethereum.cancun.vm.exceptions import StackOverflowError, StackUnderflowError from ethereum.cancun.vm.gas import MessageCallGas from ethereum.crypto.hash import Hash32 from ethereum.exceptions import EthereumException @@ -116,6 +120,7 @@ ("ethereum_types", "numeric", "Uint"): Uint, ("ethereum_types", "numeric", "U256"): U256, ("ethereum_types", "numeric", "SetUint"): Set[Uint], + ("ethereum_types", "numeric", "UnionUintU256"): Union[Uint, U256], ("ethereum_types", "bytes", "Bytes0"): Bytes0, ("ethereum_types", "bytes", "Bytes1"): Bytes1, ("ethereum_types", "bytes", "Bytes8"): Bytes8, @@ -138,6 +143,7 @@ ("ethereum", "cancun", "blocks", "TupleLog"): Tuple[Log, ...], ("ethereum", "cancun", "blocks", "Receipt"): Receipt, ("ethereum", "cancun", "fork_types", "Address"): Address, + ("ethereum", "cancun", "fork_types", "SetAddress"): Set[Address], ("ethereum", "cancun", "fork_types", "Root"): Root, ("ethereum", "cancun", "fork_types", "Account"): Account, ("ethereum", "cancun", "fork_types", "Bloom"): Bloom, @@ -182,6 +188,21 @@ ], ("ethereum", "exceptions", "EthereumException"): EthereumException, ("ethereum", "cancun", "vm", "memory", "Bytearray"): bytearray, + ("ethereum", "cancun", "vm", "stack", "Stack"): List[U256], + ( + "ethereum", + "cancun", + "vm", + "exceptions", + "StackUnderflowError", + ): StackUnderflowError, + ( + "ethereum", + "cancun", + "vm", + "exceptions", + "StackOverflowError", + ): StackOverflowError, } # In the EELS, some functions are annotated with Sequence while it's actually just Bytes. @@ -215,7 +236,11 @@ def gen_arg(dict_manager: DictManager, segments: MemorySegmentManager): def _gen_arg( - dict_manager: DictManager, segments: MemorySegmentManager, arg_type: Type, arg: Any + dict_manager: DictManager, + segments: MemorySegmentManager, + arg_type: Type, + arg: Any, + annotations: Optional[Any] = None, ): """ Generate a Cairo argument from a Python argument. @@ -234,6 +259,11 @@ def _gen_arg( if arg_type is type(None): return 0 + arg_type_origin = get_origin(arg_type) + if arg_type_origin is Annotated: + base_type, *annotations = get_args(arg_type) + return _gen_arg(dict_manager, segments, base_type, arg, annotations) + arg_type_origin = get_origin(arg_type) or arg_type if isinstance_with_generic(arg_type_origin, ForwardRef): arg_type = arg_type_origin._evaluate(globals(), locals(), frozenset()) @@ -261,14 +291,33 @@ def _gen_arg( segments.load_data(struct_ptr, data) return struct_ptr - if arg_type_origin in (tuple, list, Sequence, abc.Sequence): - if arg_type_origin is tuple and Ellipsis not in get_args(arg_type): + if arg_type_origin is list: + # A `list` is represented as a Dict[felt, V] along with a length field. + value_type = get_args(arg_type)[0] # Get the concrete type parameter + data = defaultdict(int, {k: v for k, v in enumerate(arg)}) + base = _gen_arg(dict_manager, segments, Dict[Uint, value_type], data) + segments.load_data(base + 2, [len(arg)]) + return base + + if arg_type_origin in (tuple, Sequence, abc.Sequence): + if arg_type_origin is tuple and ( + Ellipsis not in get_args(arg_type) or annotations + ): # Case a tuple with a fixed number of elements, all of different types. # These are represented as a pointer to a struct with a pointer to each element. + element_types = get_args(arg_type) + + # Handle fixed-size tuples with size annotation (e.g. Annotated[Tuple[T], N]) + if annotations and len(annotations) == 1 and len(element_types) == 1: + element_types = element_types * annotations[0] + elif annotations: + raise ValueError( + f"Invalid tuple size annotation for {arg_type} with annotations {annotations}" + ) struct_ptr = segments.add() data = [ - _gen_arg(dict_manager, segments, x_type, x) - for x_type, x in zip(get_args(arg_type), arg) + _gen_arg(dict_manager, segments, element_type, value) + for element_type, value in zip(element_types, arg) ] segments.load_data(struct_ptr, data) return struct_ptr @@ -392,6 +441,9 @@ def to_cairo_type(program: Program, type_name: Type): if type_name is int: return TypeFelt() + if get_origin(type_name) is Annotated: + type_name = get_args(type_name)[0] + _python_type_to_cairo_struct = { v: k for k, v in _cairo_struct_to_python_type.items() } diff --git a/cairo/tests/utils/serde.py b/cairo/tests/utils/serde.py index 39f7edf6..b0f13173 100644 --- a/cairo/tests/utils/serde.py +++ b/cairo/tests/utils/serde.py @@ -23,6 +23,7 @@ from itertools import accumulate from pathlib import Path from typing import ( + Annotated, Any, List, Mapping, @@ -132,6 +133,9 @@ def serialize_type(self, path: Tuple[str, ...], ptr) -> Any: full_path = self.main_part + full_path[full_path.index("__main__") + 1 :] python_cls = to_python_type(full_path) + if get_origin(python_cls) is Annotated: + python_cls, _ = get_args(python_cls) + if get_origin(python_cls) is Union: value_ptr = self.serialize_pointers(path, ptr)["value"] if value_ptr is None: @@ -158,6 +162,36 @@ def serialize_type(self, path: Tuple[str, ...], ptr) -> Any: return self._serialize(variant.cairo_type, value_ptr + variant.offset) + if get_origin(python_cls) is list: + mapping_struct_ptr = self.serialize_pointers(path, ptr)["value"] + mapping_struct_path = ( + get_struct_definition(self.program, path) + .members["value"] + .cairo_type.pointee.scope.path + ) + dict_access_path = ( + get_struct_definition(self.program, mapping_struct_path) + .members["dict_ptr"] + .cairo_type.pointee.scope.path + ) + dict_access_types = get_struct_definition( + self.program, dict_access_path + ).members + key_type = dict_access_types["key"].cairo_type + value_type = dict_access_types["new_value"].cairo_type + pointers = self.serialize_pointers(mapping_struct_path, mapping_struct_ptr) + segment_size = pointers["dict_ptr"] - pointers["dict_ptr_start"] + dict_ptr = pointers["dict_ptr_start"] + stack_len = pointers["len"] + + dict_repr = { + self._serialize(key_type, dict_ptr + i): self._serialize( + value_type, dict_ptr + i + 2 + ) + for i in range(0, segment_size, 3) + } + return [dict_repr[i] for i in range(stack_len)] + if get_origin(python_cls) in (tuple, list, Sequence, abc.Sequence): # Tuple and list are represented as structs with a pointer to the first element and the length. # The value field is a list of Relocatable (pointers to each element) or Felt (tuple of felts). @@ -287,14 +321,19 @@ def serialize_type(self, path: Tuple[str, ...], ptr) -> Any: if python_cls is None: return kwargs + value = kwargs.get("value") + if isinstance(members["value"].cairo_type, TypePointer) and value is None: + # A None pointer is valid for pointer types, meaning just that the struct is not present. + return None + if python_cls in (U256, Hash32, Bytes32): - value = kwargs["value"]["low"] + kwargs["value"]["high"] * 2**128 + value = value["low"] + value["high"] * 2**128 if python_cls == U256: return U256(value) return python_cls(value.to_bytes(32, "little")) if python_cls in (Bytes0, Bytes1, Bytes8, Bytes20): - return python_cls(kwargs["value"].to_bytes(python_cls.LENGTH, "little")) + return python_cls(value.to_bytes(python_cls.LENGTH, "little")) # Because some types are wrapped in a value field, e.g. Account{ value: AccountStruct } # this may not work, so that we catch the error and try to fallback. @@ -304,10 +343,6 @@ def serialize_type(self, path: Tuple[str, ...], ptr) -> Any: except TypeError: pass - value = kwargs.get("value") - if isinstance(members["value"].cairo_type, TypePointer) and value is None: - # A None pointer is valid for pointer types, meaning just that the struct is not present. - return None if isinstance(value, dict): signature(python_cls.__init__).bind(None, **value) return python_cls(**value) diff --git a/uv.lock b/uv.lock index e110f28f..33f450ef 100644 --- a/uv.lock +++ b/uv.lock @@ -389,7 +389,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "cairo-lang", specifier = ">=0.13.2" }, - { name = "ethereum", git = "https://github.com/ethereum/execution-specs.git?rev=1adcc1bfe774798bcacc685aebc17bd9935078c3" }, + { name = "ethereum", git = "https://github.com/kkrt-labs/execution-specs.git?branch=dev%2Fchange-type-branch-nodes" }, { name = "marshmallow-dataclass", specifier = ">=8.6.1" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "toml", specifier = ">=0.10.2" }, @@ -975,7 +975,7 @@ wheels = [ [[package]] name = "ethereum" version = "0.1.0" -source = { git = "https://github.com/ethereum/execution-specs.git?rev=1adcc1bfe774798bcacc685aebc17bd9935078c3#1adcc1bfe774798bcacc685aebc17bd9935078c3" } +source = { git = "https://github.com/kkrt-labs/execution-specs.git?branch=dev%2Fchange-type-branch-nodes#678325284038d9aafca8a7594e97f8544d698947" } dependencies = [ { name = "coincurve" }, { name = "ethereum-types" }, @@ -2213,8 +2213,6 @@ version = "6.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/26/10/2a30b13c61e7cf937f4adf90710776b7918ed0a9c434e2c38224732af310/psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a", size = 508565 } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/2b/f4dea5d993d9cd22ad958eea828a41d5d225556123d372f02547c29c4f97/psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e", size = 246648 }, - { url = "https://files.pythonhosted.org/packages/9f/14/4aa97a7f2e0ac33a050d990ab31686d651ae4ef8c86661fef067f00437b9/psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85", size = 249905 }, { url = "https://files.pythonhosted.org/packages/01/9e/8be43078a171381953cfee33c07c0d628594b5dbfc5157847b85022c2c1b/psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688", size = 247762 }, { url = "https://files.pythonhosted.org/packages/1d/cb/313e80644ea407f04f6602a9e23096540d9dc1878755f3952ea8d3d104be/psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e", size = 248777 }, { url = "https://files.pythonhosted.org/packages/65/8e/bcbe2025c587b5d703369b6a75b65d41d1367553da6e3f788aff91eaf5bd/psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38", size = 284259 },