From cbbe5ab1b557f8f9f90cb70430c32faa27af456b Mon Sep 17 00:00:00 2001 From: m0wer Date: Fri, 15 May 2026 05:01:14 +0200 Subject: [PATCH] perf(ripemd160): use hashlib fast path when available The pure-Python RIPEMD160 implementation is hot on every Hash160 and script-evaluation call. When the host OpenSSL build exposes ripemd160 (via the legacy provider on OpenSSL 3, or natively on older builds), delegate to hashlib for a significant speedup. Probe once at import time with hashlib.new('ripemd160', b'') and keep the pure-Python path as _ripemd160_pure so it remains the source of truth for correctness and continues to work where OpenSSL no longer provides RIPEMD160. Expand the test module with three cases: vectors via the public ripemd160 entry point, vectors via _ripemd160_pure, and a cross-check that both paths agree on the same input. --- bitcointx/core/_ripemd160.py | 36 ++++++++++++++++++ bitcointx/tests/test_ripemd160.py | 62 ++++++++++++++++++++----------- 2 files changed, 77 insertions(+), 21 deletions(-) diff --git a/bitcointx/core/_ripemd160.py b/bitcointx/core/_ripemd160.py index eaeb4311..3744fac4 100644 --- a/bitcointx/core/_ripemd160.py +++ b/bitcointx/core/_ripemd160.py @@ -16,14 +16,38 @@ Runtime performance will be slow, but the alternative is having a compiled dependency, which is too heavy. +When ``hashlib`` exposes a working ``ripemd160`` implementation (typically +via the OpenSSL legacy provider) we transparently delegate to it, which is +orders of magnitude faster than the pure-Python compression loop below. The +pure-Python fallback remains the source of truth for correctness and is the +implementation that is exercised by the test vectors. + IMPORTANT: code is not constant-time! This should NOT be used for working with secret data, such as, for example building a MAC (message authentication code), etc. (btw, you cannot expect constant-time behavior from python code at all) """ +import hashlib from typing import Tuple + +def _hashlib_ripemd160_available() -> bool: + """Probe whether hashlib can produce a working ripemd160 digest. + + On OpenSSL 3 without the legacy provider, ``hashlib.new('ripemd160')`` + raises at construction time, so this check both verifies presence and that + the algorithm is actually usable. + """ + try: + hashlib.new('ripemd160', b'').digest() + except (ValueError, AttributeError): # pragma: no cover - environment dependent + return False + return True + + +_USE_HASHLIB = _hashlib_ripemd160_available() + # Message schedule indexes for the left path. ML = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, @@ -120,6 +144,18 @@ def ripemd160(data: bytes) -> bytes: secret data, such as, for example building a MAC (message authentication code), etc. """ + if _USE_HASHLIB: + return hashlib.new('ripemd160', data).digest() + return _ripemd160_pure(data) + + +def _ripemd160_pure(data: bytes) -> bytes: + """Pure-Python reference implementation of RIPEMD-160. + + Kept as the verifiable source of truth and as the fallback used when + ``hashlib`` does not provide a usable ripemd160 (for example, OpenSSL 3 + without the legacy provider). + """ # Initialize state. state = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0) # Process full 64-byte blocks in the input. diff --git a/bitcointx/tests/test_ripemd160.py b/bitcointx/tests/test_ripemd160.py index 0445c659..ff8d5e30 100644 --- a/bitcointx/tests/test_ripemd160.py +++ b/bitcointx/tests/test_ripemd160.py @@ -2,29 +2,49 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. import unittest -from bitcointx.core._ripemd160 import ripemd160 + +from bitcointx.core._ripemd160 import _ripemd160_pure, ripemd160 + +# RIPEMD-160 test vectors. +# See https://homes.esat.kuleuven.be/~bosselae/ripemd160.html +# +# The following table is Copyright (c) 2021 Pieter Wuille +# (taken from test/functional/test_framework/ripemd160.py +# in Bitcoin Core source) +_VECTORS = [ + (b"", "9c1185a5c5e9fc54612808977ee8f548b2258d31"), + (b"a", "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe"), + (b"abc", "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"), + (b"message digest", "5d0689ef49d2fae572b881b123a85ffa21595f36"), + (b"abcdefghijklmnopqrstuvwxyz", + "f71c27109c692c1b56bbdceb5b9d2865b3708dbc"), + (b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", + "12a053384a9c0c88e405a06c27dcf49ada62eb2b"), + (b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", + "b0e20b6e3116640286ed3a87a5713079b21f5189"), + (b"1234567890" * 8, "9b752e45573d4b39f4dbd3323cab82bf63326bfb"), + (b"a" * 1000000, "52783243c1697bdbe16d37f97f68f08325dc1528"), +] class Test_RIPEMD160(unittest.TestCase): def test_ripemd160(self) -> None: - """RIPEMD-160 test vectors.""" - # See https://homes.esat.kuleuven.be/~bosselae/ripemd160.html - # - # The following code is Copyright (c) 2021 Pieter Wuille - # (taken from test/functional/test_framework/ripemd160.py - # in Bitcoin Core source) - for msg, hexout in [ - (b"", "9c1185a5c5e9fc54612808977ee8f548b2258d31"), - (b"a", "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe"), - (b"abc", "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"), - (b"message digest", "5d0689ef49d2fae572b881b123a85ffa21595f36"), - (b"abcdefghijklmnopqrstuvwxyz", - "f71c27109c692c1b56bbdceb5b9d2865b3708dbc"), - (b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", - "12a053384a9c0c88e405a06c27dcf49ada62eb2b"), - (b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", - "b0e20b6e3116640286ed3a87a5713079b21f5189"), - (b"1234567890" * 8, "9b752e45573d4b39f4dbd3323cab82bf63326bfb"), - (b"a" * 1000000, "52783243c1697bdbe16d37f97f68f08325dc1528") - ]: + """RIPEMD-160 test vectors against the public API.""" + for msg, hexout in _VECTORS: self.assertEqual(ripemd160(msg).hex(), hexout) + + def test_ripemd160_pure_matches_vectors(self) -> None: + """The pure-Python fallback implementation matches the spec vectors. + + This guarantees correctness of the fallback path even on systems + where ``hashlib`` provides RIPEMD-160 (and therefore the public + ``ripemd160`` function delegates to OpenSSL). + """ + for msg, hexout in _VECTORS: + self.assertEqual(_ripemd160_pure(msg).hex(), hexout) + + def test_public_and_pure_agree(self) -> None: + """Public entry point and pure-Python fallback agree byte for byte.""" + for msg, _hexout in _VECTORS: + self.assertEqual(ripemd160(msg), _ripemd160_pure(msg)) +