From 80a271256c17d422820e058b049aeecd8b948bf9 Mon Sep 17 00:00:00 2001 From: Kevaundray Wedderburn Date: Sun, 1 Mar 2026 19:09:21 +0000 Subject: [PATCH 1/5] use hashlib --- .../src/execution_testing/base_types/base_types.py | 5 ++--- .../testing/src/execution_testing/test_types/trie.py | 5 ++--- src/ethereum/crypto/hash.py | 9 ++++----- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/testing/src/execution_testing/base_types/base_types.py b/packages/testing/src/execution_testing/base_types/base_types.py index 0bb63e69eca..27fc87136cf 100644 --- a/packages/testing/src/execution_testing/base_types/base_types.py +++ b/packages/testing/src/execution_testing/base_types/base_types.py @@ -1,5 +1,6 @@ """Basic type primitives used to define other types.""" +import hashlib from abc import ABCMeta from hashlib import sha256 from re import sub @@ -13,7 +14,6 @@ TypeVar, ) -from Crypto.Hash import keccak from pydantic import GetCoreSchemaHandler, StringConstraints from pydantic_core.core_schema import ( PlainValidatorFunctionSchema, @@ -201,8 +201,7 @@ def hex(self, *args: Any, **kwargs: Any) -> str: def keccak256(self) -> "Hash": """Return the keccak256 hash of the opcode byte representation.""" - k = keccak.new(digest_bits=256) - return Hash(k.update(bytes(self)).digest()) + return Hash(hashlib.new("keccak-256", bytes(self)).digest()) def sha256(self) -> "Hash": """Return the sha256 hash of the opcode byte representation.""" diff --git a/packages/testing/src/execution_testing/test_types/trie.py b/packages/testing/src/execution_testing/test_types/trie.py index 4eb9d4288b2..919de8e8f8f 100644 --- a/packages/testing/src/execution_testing/test_types/trie.py +++ b/packages/testing/src/execution_testing/test_types/trie.py @@ -3,6 +3,7 @@ """ import copy +import hashlib from dataclasses import dataclass, field from typing import ( Callable, @@ -18,7 +19,6 @@ cast, ) -from Crypto.Hash import keccak from ethereum_rlp import Extended, rlp from ethereum_types.bytes import Bytes, Bytes20, Bytes32 from ethereum_types.frozen import slotted_freezable @@ -38,8 +38,7 @@ class FrontierAccount: def keccak256(buffer: Bytes) -> Bytes32: """Compute the keccak256 hash of the input `buffer`.""" - k = keccak.new(digest_bits=256) - return Bytes32(k.update(buffer).digest()) + return Bytes32(hashlib.new("keccak-256", buffer).digest()) def encode_account( diff --git a/src/ethereum/crypto/hash.py b/src/ethereum/crypto/hash.py index 67b9053f3a0..e5d70e6fe32 100644 --- a/src/ethereum/crypto/hash.py +++ b/src/ethereum/crypto/hash.py @@ -11,7 +11,8 @@ Cryptographic hashing functions. """ -from Crypto.Hash import keccak +import hashlib + from ethereum_types.bytes import Bytes, Bytes32, Bytes64 Hash32 = Bytes32 @@ -33,8 +34,7 @@ def keccak256(buffer: Bytes | bytearray) -> Hash32: Output of the hash function. """ - k = keccak.new(digest_bits=256) - return Hash32(k.update(buffer).digest()) + return Hash32(hashlib.new("keccak-256", buffer).digest()) def keccak512(buffer: Bytes | bytearray) -> Hash64: @@ -52,5 +52,4 @@ def keccak512(buffer: Bytes | bytearray) -> Hash64: Output of the hash function. """ - k = keccak.new(digest_bits=512) - return Hash64(k.update(buffer).digest()) + return Hash64(hashlib.new("keccak-512", buffer).digest()) From fd40b108a073d6876e0ec08723bdaed968a98b5a Mon Sep 17 00:00:00 2001 From: danceratopz Date: Mon, 11 May 2026 13:03:06 +0200 Subject: [PATCH 2/5] test(crypto): cover keccak backend dispatch in `ethereum.crypto.hash` Add tests that verify the active backend matches published Keccak-256 and Keccak-512 vectors, both backends produce byte-identical output, and the pycryptodome fallback engages cleanly when `hashlib.new("keccak-256")` is patched to raise. Also covers EEST `Bytes.keccak256` and `trie.keccak256` routing. Without the fallback dispatch these tests fail (or, on Pythons whose hashlib lacks Keccak entirely, the test session aborts at collection), so the suite drives the fix that follows. --- .../base_types/tests/test_keccak_dispatch.py | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py diff --git a/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py b/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py new file mode 100644 index 00000000000..e795fa6f285 --- /dev/null +++ b/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py @@ -0,0 +1,175 @@ +""" +Tests for the Keccak backend dispatch in `ethereum.crypto.hash`. + +The module decides at import time whether to use hashlib (linked OpenSSL) +or pycryptodome, depending on whether `hashlib.new("keccak-256", ...)` +succeeds. These tests verify: + +* both backends produce byte-identical output (cross-backend equivalence); +* the fallback engages cleanly when hashlib raises, simulated via + monkeypatch so we can exercise the path on a Python whose OpenSSL does + expose Keccak; +* on a Python where hashlib supports Keccak, the fast path is selected + (guards against a regression where `algorithms_available` lies and the + module silently forces every user onto pycryptodome). +""" + +import hashlib +import importlib +import sys +from collections.abc import Iterator +from typing import Any +from unittest.mock import patch + +import pytest + +# Pre-NIST Keccak vectors. Empty-input digests are widely published; the +# `hashme` vector was confirmed against a working hashlib build during +# PR #2370 review. +KECCAK256_VECTORS: list[tuple[bytes, str]] = [ + ( + b"", + "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + ), + ( + b"abc", + "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45", + ), + ( + b"hashme", + "7f98885dc9cf152c0bb08eaf056668f99c47cabd8fe01b1276f9a305b1389646", + ), +] + +KECCAK512_VECTORS: list[tuple[bytes, str]] = [ + ( + b"", + "0eab42de4c3ceb9235fc91acffe746b29c29a8c366b7c60e4e67c466f36a4304" + "c00fa9caf9d87976ba469bcbe06713b435f091ef2769fb160cdab33d3670680e", + ), +] + + +def _hashlib_has_keccak() -> bool: + """Return True if `hashlib.new("keccak-256", ...)` succeeds here.""" + try: + hashlib.new("keccak-256", b"") + except ValueError: + return False + return True + + +def _clean_reimport_hash() -> Any: + """Drop and reimport `ethereum.crypto.hash` for a fresh dispatch run.""" + sys.modules.pop("ethereum.crypto.hash", None) + return importlib.import_module("ethereum.crypto.hash") + + +@pytest.fixture +def restore_hash_module() -> Iterator[None]: + """Restore the natural-state `ethereum.crypto.hash` after each test.""" + yield + _clean_reimport_hash() + + +@pytest.mark.parametrize("buffer, expected_hex", KECCAK256_VECTORS) +def test_keccak256_known_vectors(buffer: bytes, expected_hex: str) -> None: + """Active backend produces published Keccak-256 digests.""" + from ethereum.crypto.hash import keccak256 + + assert keccak256(buffer).hex() == expected_hex + + +@pytest.mark.parametrize("buffer, expected_hex", KECCAK512_VECTORS) +def test_keccak512_known_vectors(buffer: bytes, expected_hex: str) -> None: + """Active backend produces published Keccak-512 digests.""" + from ethereum.crypto.hash import keccak512 + + assert keccak512(buffer).hex() == expected_hex + + +def test_both_backends_agree() -> None: + """Hashlib and pycryptodome produce byte-identical Keccak-256 output.""" + if not _hashlib_has_keccak(): + pytest.skip("hashlib lacks keccak-256 on this OpenSSL build") + + from Crypto.Hash import keccak as pyc_keccak + + inputs = [ + b"", + b"x", + b"\x00" * 64, + bytes(range(256)), + b"a" * 4096, + b"\xff" * 65536, + ] + for buf in inputs: + hl = hashlib.new("keccak-256", buf).digest() + pc = pyc_keccak.new(digest_bits=256).update(buf).digest() + assert hl == pc, f"backends disagree on input of length {len(buf)}" + + +def test_fallback_engages_when_hashlib_lacks_keccak( + restore_hash_module: None, +) -> None: + """If hashlib raises for Keccak, the module uses pycryptodome instead.""" + del restore_hash_module + real_new = hashlib.new + + def mocked_new(name: str, *args: Any, **kwargs: Any) -> Any: + if name in ("keccak-256", "keccak-512"): + raise ValueError(f"unsupported hash type {name}") + return real_new(name, *args, **kwargs) + + with patch.object(hashlib, "new", side_effect=mocked_new): + h = _clean_reimport_hash() + + assert hasattr(h, "_pycryptodome_keccak"), ( + "module did not engage pycryptodome fallback" + ) + for buffer, expected_hex in KECCAK256_VECTORS: + assert h.keccak256(buffer).hex() == expected_hex + for buffer, expected_hex in KECCAK512_VECTORS: + assert h.keccak512(buffer).hex() == expected_hex + + +def test_native_path_used_when_hashlib_has_keccak( + restore_hash_module: None, +) -> None: + """ + Verify hashlib path is selected when keccak-256 is supported. + + Guards against a regression where a bogus availability check (e.g. + one that relied on `hashlib.algorithms_available`) would silently + force every user onto the slower pycryptodome path. + """ + del restore_hash_module + if not _hashlib_has_keccak(): + pytest.skip("hashlib lacks keccak-256 on this OpenSSL build") + + h = _clean_reimport_hash() + assert not hasattr(h, "_pycryptodome_keccak"), ( + "module engaged pycryptodome fallback despite hashlib having keccak" + ) + + +def test_eest_bytes_keccak256_matches_eels() -> None: + """`Bytes.keccak256()` returns the same digest as EELS `keccak256`.""" + from ethereum.crypto.hash import keccak256 + + from ..base_types import Bytes + + for buffer in (b"", b"hashme", bytes(range(256))): + from_eest = bytes(Bytes(buffer).keccak256()) + from_eels = bytes(keccak256(buffer)) + assert from_eest == from_eels + + +def test_eest_trie_keccak256_matches_eels() -> None: + """`trie.keccak256` and EELS `keccak256` return identical digests.""" + from ethereum.crypto.hash import keccak256 as eels + + from ...test_types.trie import keccak256 as trie + + for buffer in (b"", b"hashme", bytes(range(256))): + assert bytes(trie(buffer)) == bytes(eels(buffer)) From ad1440bb89e62d5447c27674a143013af48a26e8 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Mon, 11 May 2026 13:03:21 +0200 Subject: [PATCH 3/5] fix(crypto): fall back to pycryptodome when hashlib lacks Keccak OpenSSL only gained default-provider Keccak in 3.2.0 (Nov 2023). LTS 3.0.x and 3.1.x, still shipped by Debian 12 and RHEL 9, do not provide it, so `hashlib.new("keccak-256", ...)` raises `ValueError` there and every call to `keccak256` crashes. At module import time, probe with `hashlib.new("keccak-256", b"")`. On `ValueError`, rebind the digest helpers to pycryptodome's bundled implementation. The probe is more reliable than checking `hashlib.algorithms_available`, which omits some OpenSSL-provider digests on python-build-standalone builds. Route the EEST keccak helpers in `base_types.Bytes.keccak256` and `test_types.trie.keccak256` through `ethereum.crypto.hash`, so the dispatch lives in one place. Widen `keccak256` and `keccak512` parameter types from `Bytes | bytearray` to `bytes | bytearray` so the EEST `Bytes` subclass passes through without coercion. --- .../base_types/base_types.py | 4 +- .../src/execution_testing/test_types/trie.py | 7 +--- src/ethereum/crypto/hash.py | 39 ++++++++++++++++--- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/testing/src/execution_testing/base_types/base_types.py b/packages/testing/src/execution_testing/base_types/base_types.py index 27fc87136cf..b42fb315485 100644 --- a/packages/testing/src/execution_testing/base_types/base_types.py +++ b/packages/testing/src/execution_testing/base_types/base_types.py @@ -1,6 +1,5 @@ """Basic type primitives used to define other types.""" -import hashlib from abc import ABCMeta from hashlib import sha256 from re import sub @@ -14,6 +13,7 @@ TypeVar, ) +from ethereum.crypto.hash import keccak256 as _keccak256 from pydantic import GetCoreSchemaHandler, StringConstraints from pydantic_core.core_schema import ( PlainValidatorFunctionSchema, @@ -201,7 +201,7 @@ def hex(self, *args: Any, **kwargs: Any) -> str: def keccak256(self) -> "Hash": """Return the keccak256 hash of the opcode byte representation.""" - return Hash(hashlib.new("keccak-256", bytes(self)).digest()) + return Hash(_keccak256(self)) def sha256(self) -> "Hash": """Return the sha256 hash of the opcode byte representation.""" diff --git a/packages/testing/src/execution_testing/test_types/trie.py b/packages/testing/src/execution_testing/test_types/trie.py index 919de8e8f8f..16fd4d5709d 100644 --- a/packages/testing/src/execution_testing/test_types/trie.py +++ b/packages/testing/src/execution_testing/test_types/trie.py @@ -3,7 +3,6 @@ """ import copy -import hashlib from dataclasses import dataclass, field from typing import ( Callable, @@ -19,6 +18,7 @@ cast, ) +from ethereum.crypto.hash import keccak256 from ethereum_rlp import Extended, rlp from ethereum_types.bytes import Bytes, Bytes20, Bytes32 from ethereum_types.frozen import slotted_freezable @@ -36,11 +36,6 @@ class FrontierAccount: code: Bytes -def keccak256(buffer: Bytes) -> Bytes32: - """Compute the keccak256 hash of the input `buffer`.""" - return Bytes32(hashlib.new("keccak-256", buffer).digest()) - - def encode_account( raw_account_data: FrontierAccount, storage_root: Bytes ) -> Bytes: diff --git a/src/ethereum/crypto/hash.py b/src/ethereum/crypto/hash.py index e5d70e6fe32..1861f1686ad 100644 --- a/src/ethereum/crypto/hash.py +++ b/src/ethereum/crypto/hash.py @@ -13,13 +13,42 @@ import hashlib -from ethereum_types.bytes import Bytes, Bytes32, Bytes64 +from ethereum_types.bytes import Bytes32, Bytes64 Hash32 = Bytes32 Hash64 = Bytes64 +# Prefer hashlib (linked OpenSSL) when it exposes the pre-NIST Keccak digests; +# fall back to pycryptodome's self-contained implementation otherwise. +# OpenSSL only gained default-provider Keccak in 3.2.0 (Nov 2023); LTS 3.0.x +# and 3.1.x (still shipped by Debian 12, RHEL 9, etc.) do not provide it. +# We probe with `hashlib.new()` because `hashlib.algorithms_available` omits +# some OpenSSL-provider digests on common builds (e.g. python-build-standalone +# / uv-managed CPython on OpenSSL 3.5.x), yielding false negatives. +try: + hashlib.new("keccak-256", b"") -def keccak256(buffer: Bytes | bytearray) -> Hash32: + def _keccak256_digest(buffer: bytes | bytearray) -> bytes: + return hashlib.new("keccak-256", buffer).digest() + + def _keccak512_digest(buffer: bytes | bytearray) -> bytes: + return hashlib.new("keccak-512", buffer).digest() + +except ValueError: + from Crypto.Hash import keccak as _pycryptodome_keccak + + def _keccak256_digest(buffer: bytes | bytearray) -> bytes: + return ( + _pycryptodome_keccak.new(digest_bits=256).update(buffer).digest() + ) + + def _keccak512_digest(buffer: bytes | bytearray) -> bytes: + return ( + _pycryptodome_keccak.new(digest_bits=512).update(buffer).digest() + ) + + +def keccak256(buffer: bytes | bytearray) -> Hash32: """ Computes the keccak256 hash of the input `buffer`. @@ -34,10 +63,10 @@ def keccak256(buffer: Bytes | bytearray) -> Hash32: Output of the hash function. """ - return Hash32(hashlib.new("keccak-256", buffer).digest()) + return Hash32(_keccak256_digest(buffer)) -def keccak512(buffer: Bytes | bytearray) -> Hash64: +def keccak512(buffer: bytes | bytearray) -> Hash64: """ Computes the keccak512 hash of the input `buffer`. @@ -52,4 +81,4 @@ def keccak512(buffer: Bytes | bytearray) -> Hash64: Output of the hash function. """ - return Hash64(hashlib.new("keccak-512", buffer).digest()) + return Hash64(_keccak512_digest(buffer)) From ee60986f12cd7f551ec661c8e606aa8f4388bbb5 Mon Sep 17 00:00:00 2001 From: danceratopz Date: Mon, 11 May 2026 16:23:04 +0200 Subject: [PATCH 4/5] fix(crypto): define keccak digest helpers unconditionally for docc The previous `try/except` block defined `_keccak256_digest` and `_keccak512_digest` inside its branches. `docc`'s static analyzer does not traverse `try/except` at module scope when discovering module-level names, so it could not resolve the references from `keccak256` and `keccak512`, breaking the `Build Documentation` job with: docc.plugins.references.ReferenceError: in `src/ethereum/crypto/hash.py`, undefined identifier: `ethereum.crypto.hash._keccak256_digest` Define the helpers unconditionally at module scope and branch on a module-level `_USE_HASHLIB` flag inside their bodies. The probe runs once at import; the runtime branch adds about 20ns per call, dwarfed by the hash itself. `pycryptodome` is now imported unconditionally, which is free since it is already a hard dep via `elliptic_curve.py`. Update the dispatch tests to assert on `_USE_HASHLIB` instead of the presence of `_pycryptodome_keccak` (now always imported). --- .../base_types/tests/test_keccak_dispatch.py | 4 +- src/ethereum/crypto/hash.py | 51 ++++++++++--------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py b/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py index e795fa6f285..ec5ed60bbc2 100644 --- a/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py +++ b/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py @@ -124,7 +124,7 @@ def mocked_new(name: str, *args: Any, **kwargs: Any) -> Any: with patch.object(hashlib, "new", side_effect=mocked_new): h = _clean_reimport_hash() - assert hasattr(h, "_pycryptodome_keccak"), ( + assert h._USE_HASHLIB is False, ( "module did not engage pycryptodome fallback" ) for buffer, expected_hex in KECCAK256_VECTORS: @@ -148,7 +148,7 @@ def test_native_path_used_when_hashlib_has_keccak( pytest.skip("hashlib lacks keccak-256 on this OpenSSL build") h = _clean_reimport_hash() - assert not hasattr(h, "_pycryptodome_keccak"), ( + assert h._USE_HASHLIB is True, ( "module engaged pycryptodome fallback despite hashlib having keccak" ) diff --git a/src/ethereum/crypto/hash.py b/src/ethereum/crypto/hash.py index 1861f1686ad..03861d4c3df 100644 --- a/src/ethereum/crypto/hash.py +++ b/src/ethereum/crypto/hash.py @@ -13,39 +13,42 @@ import hashlib +from Crypto.Hash import keccak as _pycryptodome_keccak from ethereum_types.bytes import Bytes32, Bytes64 Hash32 = Bytes32 Hash64 = Bytes64 -# Prefer hashlib (linked OpenSSL) when it exposes the pre-NIST Keccak digests; -# fall back to pycryptodome's self-contained implementation otherwise. -# OpenSSL only gained default-provider Keccak in 3.2.0 (Nov 2023); LTS 3.0.x -# and 3.1.x (still shipped by Debian 12, RHEL 9, etc.) do not provide it. -# We probe with `hashlib.new()` because `hashlib.algorithms_available` omits -# some OpenSSL-provider digests on common builds (e.g. python-build-standalone -# / uv-managed CPython on OpenSSL 3.5.x), yielding false negatives. -try: - hashlib.new("keccak-256", b"") - - def _keccak256_digest(buffer: bytes | bytearray) -> bytes: - return hashlib.new("keccak-256", buffer).digest() - def _keccak512_digest(buffer: bytes | bytearray) -> bytes: - return hashlib.new("keccak-512", buffer).digest() +def _hashlib_has_keccak() -> bool: + """Return `True` if `hashlib` can compute pre-NIST Keccak digests.""" + try: + hashlib.new("keccak-256", b"") + except ValueError: + return False + return True -except ValueError: - from Crypto.Hash import keccak as _pycryptodome_keccak - def _keccak256_digest(buffer: bytes | bytearray) -> bytes: - return ( - _pycryptodome_keccak.new(digest_bits=256).update(buffer).digest() - ) +# Decide once at import time whether to dispatch to hashlib (linked OpenSSL) +# or to pycryptodome's bundled implementation. OpenSSL only gained +# default-provider Keccak in 3.2.0 (Nov 2023); LTS 3.0.x and 3.1.x (still +# shipped by Debian 12, RHEL 9, etc.) do not provide it. We probe with +# `hashlib.new()` because `hashlib.algorithms_available` omits some +# OpenSSL-provider digests on common builds (e.g. python-build-standalone / +# uv-managed CPython on OpenSSL 3.5.x), yielding false negatives. +_USE_HASHLIB = _hashlib_has_keccak() - def _keccak512_digest(buffer: bytes | bytearray) -> bytes: - return ( - _pycryptodome_keccak.new(digest_bits=512).update(buffer).digest() - ) + +def _keccak256_digest(buffer: bytes | bytearray) -> bytes: + if _USE_HASHLIB: + return hashlib.new("keccak-256", buffer).digest() + return _pycryptodome_keccak.new(digest_bits=256).update(buffer).digest() + + +def _keccak512_digest(buffer: bytes | bytearray) -> bytes: + if _USE_HASHLIB: + return hashlib.new("keccak-512", buffer).digest() + return _pycryptodome_keccak.new(digest_bits=512).update(buffer).digest() def keccak256(buffer: bytes | bytearray) -> Hash32: From 0a184a87292bdcc6961ad419153f2ae3fafc48cd Mon Sep 17 00:00:00 2001 From: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Date: Wed, 13 May 2026 08:34:38 -0700 Subject: [PATCH 5/5] Fix keccak-256 initialization in test function --- .../execution_testing/base_types/tests/test_keccak_dispatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py b/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py index ec5ed60bbc2..eea3dd8e74f 100644 --- a/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py +++ b/packages/testing/src/execution_testing/base_types/tests/test_keccak_dispatch.py @@ -53,7 +53,7 @@ def _hashlib_has_keccak() -> bool: """Return True if `hashlib.new("keccak-256", ...)` succeeds here.""" try: - hashlib.new("keccak-256", b"") + hashlib.new("keccak-256") except ValueError: return False return True