-
Notifications
You must be signed in to change notification settings - Fork 112
Part 3 - pow: remove pyethash C extension, always use pure Python ethash #973
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ping-ke
wants to merge
17
commits into
upgrade/py313-baseline
Choose a base branch
from
upgrade/ethash
base: upgrade/py313-baseline
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
15bcfbf
remove pyethash C extension, use pure Python ethash always
572ebf9
add bench for hashimoto
ping-ke 1db9d53
add comment for pyethash removal in ethpow.py
ping-ke b517486
resolve comment
ping-ke 52cb993
resolve upgrade/ethash comment
ping-ke a1162b9
Merge branch 'master' into upgrade/ethash
ping-ke b57a48c
optimize ethash pow verification: struct-based hashing and numpy fnv …
ping-ke c6c5e6a
clean up ethereum/pow: remove unused code, simplify ethpow.py
ping-ke a05bef0
rename ethash_sha3_512/256_np -> ethash_sha3_512/256; refactor bench …
ping-ke e477a64
bench_before_after: rename mid -> R1 consistently
ping-ke 520ccae
add Cython inner loop for calc_dataset_item (R3): ~20x speedup over n…
ping-ke 7897277
gitignore: add Cython generated .c and .pyd files
ping-ke 13eb9e8
add Cython to requirements, update README install docs, add Cython vs…
ping-ke 4bcfae8
add R2/R3/R4 ethash implementations and refactor bench/test
ping-ke 5f2a045
add Rust ethash extension and refactor ethash.py dispatch
ping-ke 04d3740
add R5 (Rust) to bench_hashimoto_compare and fix ethash_rs auto-detect
ping-ke c7fd0f0
ethash: scope np.errstate, fix little-endian dtype, restore lru_cache…
ping-ke File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,94 +1,166 @@ | ||
| import copy | ||
| import importlib | ||
| import os | ||
| import numpy as np | ||
| from functools import lru_cache | ||
| from typing import Callable, Dict, List | ||
|
|
||
| from ethereum.pow.ethash_utils import * | ||
| from ethereum.pow.ethash_utils import ( | ||
| ethash_sha3_512, ethash_sha3_256, | ||
| FNV_PRIME, HASH_BYTES, WORD_BYTES, MIX_BYTES, | ||
| DATASET_PARENTS, CACHE_ROUNDS, ACCESSES, EPOCH_LENGTH, | ||
| ) | ||
|
|
||
| _FNV_PRIME = np.uint32(FNV_PRIME) | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # ETHASH_LIB selects the implementation used for PoW verification. | ||
| # "ethash" — pure-Python + numpy (always available) | ||
| # "ethash_cy" — Cython + C keccak (requires python setup.py build_ext) | ||
| # "ethash_rs" — Rust + tiny-keccak (requires python setup.py build_ext) | ||
| # Default: auto-detect best available (ethash_rs → ethash_cy → ethash) | ||
| # --------------------------------------------------------------------------- | ||
| _impl_hashimoto_light = None | ||
| _impl_mkcache = None | ||
| _mix_parents_fn = None | ||
|
|
||
| ETHASH_LIB = os.environ.get("ETHASH_LIB", "auto") | ||
|
|
||
| if ETHASH_LIB == "auto": | ||
| # Check symbol presence to avoid false-positives from namespace packages | ||
| # (e.g. the ethash_rs/ Cargo source directory looks like a package but has | ||
| # no compiled symbols until the extension is built). | ||
| _REQUIRED = {"ethash_rs": "rs_hashimoto_light", "ethash_cy": "cy_hashimoto_light"} | ||
| for _candidate in ("ethash_rs", "ethash_cy"): | ||
| try: | ||
| _mod = importlib.import_module(f"ethereum.pow.{_candidate}") | ||
| if hasattr(_mod, _REQUIRED[_candidate]): | ||
| ETHASH_LIB = _candidate | ||
| break | ||
| except ImportError: | ||
| continue | ||
| else: | ||
| ETHASH_LIB = "ethash" | ||
|
|
||
| if ETHASH_LIB == "ethash_cy": | ||
| from ethereum.pow.ethash_cy import cy_hashimoto_light as _impl_hashimoto_light | ||
| from ethereum.pow.ethash_cy import cy_mkcache as _impl_mkcache | ||
| from ethereum.pow.ethash_cy import mix_parents as _mix_parents_fn | ||
|
|
||
| elif ETHASH_LIB == "ethash_rs": | ||
| from ethereum.pow.ethash_rs import rs_hashimoto_light as _impl_hashimoto_light | ||
| from ethereum.pow.ethash_rs import rs_mkcache as _impl_mkcache | ||
| from ethereum.pow.ethash_rs import mix_parents as _mix_parents_fn | ||
|
|
||
| elif ETHASH_LIB != "ethash": | ||
| raise ValueError(f"Unknown ETHASH_LIB={ETHASH_LIB!r}. " | ||
| f"Use 'ethash', 'ethash_cy', 'ethash_rs', or 'auto'.") | ||
|
|
||
| print(f"[ethash] using implementation: {ETHASH_LIB}") | ||
|
|
||
|
|
||
| cache_seeds = [b"\x00" * 32] # type: List[bytes] | ||
|
|
||
|
|
||
| def mkcache(cache_size: int, block_number) -> List[List[int]]: | ||
| @lru_cache(10) | ||
| def _get_cache(seed: bytes, n: int) -> np.ndarray: | ||
| """Returns cache as uint32 ndarray of shape (n, 16).""" | ||
| if _impl_mkcache is not None: | ||
| return _impl_mkcache(np.frombuffer(seed, dtype=np.uint8), n) | ||
| o = np.empty((n, 16), dtype=np.uint32) | ||
| o[0] = ethash_sha3_512(seed) | ||
| for i in range(1, n): | ||
| o[i] = ethash_sha3_512(o[i - 1]) | ||
| for _ in range(CACHE_ROUNDS): | ||
| for i in range(n): | ||
| v = int(o[i, 0]) % n | ||
| xored = o[(i - 1 + n) % n] ^ o[v] | ||
| o[i] = ethash_sha3_512(xored) | ||
| return o | ||
|
|
||
|
|
||
| def mkcache(cache_size: int, block_number) -> np.ndarray: | ||
| while len(cache_seeds) <= block_number // EPOCH_LENGTH: | ||
| new_seed = serialize_hash(ethash_sha3_256(cache_seeds[-1])) | ||
| new_seed = ethash_sha3_256(cache_seeds[-1]).tobytes() | ||
| cache_seeds.append(new_seed) | ||
|
|
||
| seed = cache_seeds[block_number // EPOCH_LENGTH] | ||
| return _get_cache(seed, cache_size // HASH_BYTES) | ||
|
|
||
|
|
||
| @lru_cache(10) | ||
| def _get_cache(seed, n) -> List[List[int]]: | ||
| # Sequentially produce the initial dataset | ||
| o = [ethash_sha3_512(seed)] | ||
| for i in range(1, n): | ||
| o.append(ethash_sha3_512(o[-1])) | ||
|
|
||
| # Use a low-round version of randmemohash | ||
| for _ in range(CACHE_ROUNDS): | ||
| for i in range(n): | ||
| v = o[i][0] % n | ||
| o[i] = ethash_sha3_512(list(map(xor, o[(i - 1 + n) % n], o[v]))) | ||
|
|
||
| return o | ||
| def hashimoto_light( | ||
| full_size: int, cache: np.ndarray, header: bytes, nonce: bytes | ||
| ) -> Dict: | ||
| if _impl_hashimoto_light is not None: | ||
| return _impl_hashimoto_light( | ||
| full_size, cache, | ||
| np.frombuffer(header, dtype=np.uint8), | ||
| np.frombuffer(nonce, dtype=np.uint8), | ||
| ) | ||
| return hashimoto(header, nonce, full_size, lambda x: calc_dataset_item(cache, x)) | ||
|
|
||
|
|
||
| def calc_dataset_item(cache: List[List[int]], i: int) -> List[int]: | ||
| def calc_dataset_item(cache: np.ndarray, i: int) -> np.ndarray: | ||
| n = len(cache) | ||
| r = HASH_BYTES // WORD_BYTES | ||
| # initialize the mix | ||
| mix = copy.copy(cache[i % n]) # type: List[int] | ||
| mix = cache[i % n].copy() | ||
| mix[0] ^= i | ||
| mix = ethash_sha3_512(mix) | ||
| # fnv it with a lot of random cache nodes based on i | ||
| for j in range(DATASET_PARENTS): | ||
| cache_index = fnv(i ^ j, mix[j % r]) | ||
| mix = list(map(fnv, mix, cache[cache_index % n])) | ||
| if _mix_parents_fn is not None: | ||
| _mix_parents_fn(mix, cache, i) | ||
| else: | ||
| r = HASH_BYTES // WORD_BYTES # 16 | ||
| # uint32 overflow is intentional in FNV arithmetic | ||
| with np.errstate(over="ignore"): | ||
| for j in range(DATASET_PARENTS): | ||
| cache_index = ((i ^ j) * FNV_PRIME ^ int(mix[j % r])) & 0xFFFFFFFF | ||
| mix *= _FNV_PRIME | ||
| mix ^= cache[cache_index % n] | ||
| return ethash_sha3_512(mix) | ||
|
|
||
|
|
||
| def calc_dataset(full_size, cache) -> List[List[int]]: | ||
| o = [] | ||
| for i in range(full_size // HASH_BYTES): | ||
| o.append(calc_dataset_item(cache, i)) | ||
| return o | ||
| def calc_dataset(full_size, cache: np.ndarray) -> np.ndarray: | ||
| rows = full_size // HASH_BYTES | ||
| out = np.empty((rows, 16), dtype=np.uint32) | ||
| for i in range(rows): | ||
| out[i] = calc_dataset_item(cache, i) | ||
| return out | ||
|
|
||
|
|
||
| def hashimoto( | ||
| header: bytes, | ||
| nonce: bytes, | ||
| full_size: int, | ||
| dataset_lookup: Callable[[int], List[int]], | ||
| dataset_lookup: Callable[[int], np.ndarray], | ||
| ) -> Dict: | ||
| n = full_size // HASH_BYTES | ||
| w = MIX_BYTES // WORD_BYTES | ||
| mixhashes = MIX_BYTES // HASH_BYTES | ||
| # combine header+nonce into a 64 byte seed | ||
|
|
||
| s = ethash_sha3_512(header + nonce[::-1]) | ||
| mix = [] | ||
| for _ in range(MIX_BYTES // HASH_BYTES): | ||
| mix.extend(s) | ||
| # mix in random dataset nodes | ||
| for i in range(ACCESSES): | ||
| p = fnv(i ^ s[0], mix[i % w]) % (n // mixhashes) * mixhashes | ||
| newdata = [] | ||
| for j in range(mixhashes): | ||
| newdata.extend(dataset_lookup(p + j)) | ||
| mix = list(map(fnv, mix, newdata)) | ||
| # compress mix | ||
| cmix = [] | ||
| for i in range(0, len(mix), 4): | ||
| cmix.append(fnv(fnv(fnv(mix[i], mix[i + 1]), mix[i + 2]), mix[i + 3])) | ||
| mix = np.tile(s, mixhashes) | ||
| s0 = int(s[0]) | ||
| newdata = np.empty(w, dtype=np.uint32) | ||
|
|
||
| # uint32 overflow is intentional in FNV arithmetic | ||
| with np.errstate(over="ignore"): | ||
| for i in range(ACCESSES): | ||
| p = ((i ^ s0) * FNV_PRIME ^ int(mix[i % w])) & 0xFFFFFFFF | ||
| p = p % (n // mixhashes) * mixhashes | ||
| for j in range(mixhashes): | ||
| newdata[j * 16:(j + 1) * 16] = dataset_lookup(p + j) | ||
| mix *= _FNV_PRIME | ||
| mix ^= newdata | ||
|
|
||
| mix_r = mix.reshape(-1, 4) | ||
| cmix = mix_r[:, 0] * _FNV_PRIME ^ mix_r[:, 1] | ||
| cmix = cmix * _FNV_PRIME ^ mix_r[:, 2] | ||
| cmix = cmix * _FNV_PRIME ^ mix_r[:, 3] | ||
|
|
||
| s_cmix = np.concatenate([s, cmix]) | ||
| return { | ||
| b"mix digest": serialize_hash(cmix), | ||
| b"result": serialize_hash(ethash_sha3_256(s + cmix)), | ||
| b"mix digest": cmix.tobytes(), | ||
| b"result": ethash_sha3_256(s_cmix).tobytes(), | ||
| } | ||
|
|
||
|
|
||
| def hashimoto_light( | ||
| full_size: int, cache: List[List[int]], header: bytes, nonce: bytes | ||
| ) -> Dict: | ||
| return hashimoto(header, nonce, full_size, lambda x: calc_dataset_item(cache, x)) | ||
|
|
||
|
|
||
| def hashimoto_full(dataset: List[List[int]], header: bytes, nonce: bytes) -> Dict: | ||
| def hashimoto_full(dataset: np.ndarray, header: bytes, nonce: bytes) -> Dict: | ||
| return hashimoto(header, nonce, len(dataset) * HASH_BYTES, lambda x: dataset[x]) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to the endianness fix above:
mix digestis an externally compared byte string, so it should probably also be serialized explicitly as little-endian instead of relying on native-endiantobytes(). A minimal change here would becmix.astype("<u4", copy=False).tobytes().