forked from madacol/btcrecover
-
Notifications
You must be signed in to change notification settings - Fork 277
Expand file tree
/
Copy pathbenchmark_crypto_backends.py
More file actions
179 lines (150 loc) · 6.71 KB
/
Copy pathbenchmark_crypto_backends.py
File metadata and controls
179 lines (150 loc) · 6.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#!/usr/bin/env python3
"""
benchmark_crypto_backends.py -- compares the speed of the three secp256k1
backends supported by btcrecover.crypto_backends:
1. coincurve (C, libsecp256k1)
2. wallycore (C, libwally-core)
3. purepython (bundled ecpy, slow)
For each available backend it measures the throughput of the operations that
matter most to wallet recovery: deriving a compressed/uncompressed public key
from a private key, the P2TR tap-tweak, and the ECIES point multiplication used
by Electrum 2.8 wallets.
Usage:
python benchmark_crypto_backends.py [iterations] [--backend NAME]
[--output FILE] [--comment TEXT]
iterations number of operations per timed block (default: 2000)
--backend force a single backend: coincurve, wallycore, or purepython.
When omitted, every available backend is benchmarked.
--output write the results as JSON to FILE.
--comment free-text note recorded in the JSON output (e.g. the host).
The default iteration count is chosen so the whole benchmark finishes in a few
seconds for the C backends and a little longer for the pure-Python one.
"""
import argparse
import datetime
import json
import os
import sys
import time
# Make sure the repo root (parent of btcrecover/) is importable
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from btcrecover import crypto_backends as cb
VALID_BACKENDS = ("coincurve", "wallycore", "purepython")
# Number of operations per timed block. Kept modest because the pure-Python
# backend is intentionally slow.
DEFAULT_ITERATIONS = 2000
def _make_backend(name):
"""Build a single backend's function dict directly (bypassing auto-select)."""
if name == "coincurve":
try:
return cb._make_coincurve_backend()
except Exception as e: # pragma: no cover
print("coincurve unavailable: %s" % e)
return None
if name == "wallycore":
try:
return cb._make_wallycore_backend()
except Exception as e: # pragma: no cover
print("wallycore unavailable: %s" % e)
return None
if name == "purepython":
try:
return cb._make_purepython_backend()
except Exception as e: # pragma: no cover
print("purepython unavailable: %s" % e)
return None
raise ValueError(name)
def _time_it(fn, iters):
# warm up
fn()
start = time.perf_counter()
for _ in range(iters):
fn()
elapsed = time.perf_counter() - start
return elapsed, iters / elapsed if elapsed else float("inf")
def main():
parser = argparse.ArgumentParser(
description="Benchmark BTCRecover secp256k1 backends.")
parser.add_argument("iterations", nargs="?", type=int,
default=DEFAULT_ITERATIONS,
help="operations per timed block (default: %d)" % DEFAULT_ITERATIONS)
parser.add_argument("--backend", choices=VALID_BACKENDS, default=None,
help="force a single backend instead of benchmarking all")
parser.add_argument("--output", default=None,
help="write results as JSON to this file")
parser.add_argument("--comment", default=None,
help="free-text note recorded in the JSON output")
args = parser.parse_args()
iters = args.iterations
forced = args.backend
priv = (0x1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF).to_bytes(32, "big")
epub = cb.privkey_to_pubkey(priv, compressed=True)
# Electrum 2.8 multiplies by a PBKDF2 output reduced mod the group order, so
# this has to be full width: the pure-python ladder iterates over the
# scalar's bits, and a small value here overstates that backend several-fold.
ecies_scalar = bytes.fromhex(
"0fedcba9876543210fedcba9876543210fedcba9876543210fedcba987654321")
h = b"\x11" * 32
if forced is not None:
be = _make_backend(forced)
if be is None:
print("Requested backend '%s' is not available; aborting." % forced)
sys.exit(1)
backends = [(forced, be)]
else:
backends = []
for name in VALID_BACKENDS:
be = _make_backend(name)
if be is not None:
backends.append((name, be))
if not backends:
print("No secp256k1 backend available!")
sys.exit(1)
scope = forced if forced is not None else "all available"
print("Benchmarking %d operations per backend (%s)\n" % (iters, scope))
header = "%-12s | %12s | %12s | %12s | %12s" % (
"backend", "pubkey(comp)", "pubkey(unc)", "p2tr tweak", "ecies mult")
print(header)
print("-" * len(header))
results = {}
for name, be in backends:
comp_t, comp_ops = _time_it(lambda: be["privkey_to_pubkey"](priv, True), iters)
unc_t, unc_ops = _time_it(lambda: be["privkey_to_pubkey"](priv, False), iters)
tweak_t, tweak_ops = _time_it(lambda: be["tweak_pubkey"](be["lift_x"](epub), h), iters)
mult_t, mult_ops = _time_it(lambda: be["multiply_pubkey"](epub, ecies_scalar), iters)
results[name] = (comp_ops, unc_ops, tweak_ops, mult_ops)
print("%-12s | %12.1f | %12.1f | %12.1f | %12.1f" % (
name, comp_ops, unc_ops, tweak_ops, mult_ops))
# Relative speed-up of the fastest C backend vs pure-python (if both present)
if "purepython" in results:
fastest = max((n for n in results if n != "purepython"),
key=lambda n: results[n][0], default=None)
if fastest:
speedup = results[fastest][0] / results["purepython"][0]
print("\nFastest C backend (%s) is ~%.1fx faster than pure-python "
"for pubkey derivation." % (fastest, speedup))
print("\nActive backend selected at import time: %s" % cb.BACKEND_NAME)
if forced is not None:
print("Forced backend for this run: %s" % forced)
if args.output:
payload = {
"timestamp": datetime.datetime.now().isoformat(timespec="seconds"),
"iterations": iters,
"forced_backend": forced,
"active_backend_at_import": cb.BACKEND_NAME,
"comment": args.comment,
"results": {
name: {
"pubkey_comp": comp_ops,
"pubkey_unc": unc_ops,
"p2tr_tweak": tweak_ops,
"ecies_mult": mult_ops,
}
for name, (comp_ops, unc_ops, tweak_ops, mult_ops) in results.items()
},
}
with open(args.output, "w") as fh:
json.dump(payload, fh, indent=2)
print("\nWrote results to %s" % args.output)
if __name__ == "__main__":
main()