4
4
# "bip32",
5
5
# "hypothesis",
6
6
# "pytest",
7
+ # "verystable",
7
8
# ]
8
9
# ///
10
+ import sys
9
11
import random
10
12
import logging
11
13
import time
12
14
from contextlib import contextmanager
13
15
16
+ import bip32 as py_bip32
17
+ from verystable import bip32 as vs_bip32
18
+
14
19
import pytest
15
20
from hypothesis import given , strategies as st , target , settings
16
- from bip32 import BIP32
17
21
18
22
from bindings import derive
19
23
@@ -32,16 +36,48 @@ def timer(label):
32
36
target (time .perf_counter () - start , label = label )
33
37
34
38
35
- def their_derive (seed_hex_str : str , bip32_path : str ) -> str :
36
- bip32 = BIP32 .from_seed (bytes .fromhex (seed_hex_str ))
39
+ def py_derive (seed_hex_str : str , bip32_path : str ) -> str :
40
+ bip32 = py_bip32 . BIP32 .from_seed (bytes .fromhex (seed_hex_str ))
37
41
try :
38
42
return bip32 .get_xpriv_from_path (bip32_path )
39
43
except Exception :
40
44
return INVALID_KEY
41
45
42
46
43
- def their_xpub_derive (base58 : str , bip32_path : str ) -> str :
44
- bip32 = BIP32 .from_xpub (base58 )
47
+ def _path_str_to_ints (bip32_path ) -> list [int ] | None :
48
+ if not bip32_path .startswith ('m' ):
49
+ return None
50
+
51
+ path_ints = []
52
+ components = filter (None , bip32_path .lstrip ('m' ).split ('/' ))
53
+
54
+ for comp in components :
55
+ if comp .endswith ('h' ):
56
+ path_ints .append (int (comp [:- 1 ]) | vs_bip32 .HARDENED_INDEX )
57
+ else :
58
+ path_ints .append (int (comp ))
59
+
60
+ return path_ints
61
+
62
+
63
+ def verystable_derive (seed_hex_str : str , bip32_path : str ) -> str :
64
+ bip32 = vs_bip32 .BIP32 .from_bytes (bytes .fromhex (seed_hex_str ), True )
65
+ path_ints = _path_str_to_ints (bip32_path )
66
+
67
+ if path_ints is None :
68
+ return INVALID_KEY
69
+ elif not path_ints :
70
+ return bip32 .serialize ()
71
+
72
+ try :
73
+ derived , _ = bip32 .derive (* path_ints )
74
+ return derived .serialize ()
75
+ except Exception :
76
+ return INVALID_KEY
77
+
78
+
79
+ def py_xpub_derive (base58 : str , bip32_path : str ) -> str :
80
+ bip32 = py_bip32 .BIP32 .from_xpub (base58 )
45
81
try :
46
82
return bip32 .get_xpub_from_path (bip32_path )
47
83
except Exception :
@@ -56,38 +92,78 @@ def our_derive(hex_str, path) -> str:
56
92
return INVALID_KEY
57
93
58
94
95
+ @st .composite
96
+ def py_compatible_bip32_paths (draw ):
97
+ """
98
+ Given python-bip32's too-large path issue, clamp the max_values that we can fuzz:
99
+ https://github.com/darosior/python-bip32/issues/46
100
+ """
101
+ MAX_ALLOWED_DEPTH = 255
102
+ MAX_UNHARDENED_IDX = 2 ** 31 - 1
103
+
104
+ depth = draw (st .integers (min_value = 0 , max_value = (MAX_ALLOWED_DEPTH + 3 )))
105
+ path_parts = ["m" ]
106
+ for _ in range (depth ):
107
+ index = draw (st .integers (min_value = - 2 , max_value = MAX_UNHARDENED_IDX ))
108
+ hardened = draw (st .booleans ())
109
+ path_parts .append (f"{ index } { "h" if hardened else '' } " )
110
+ return "/" .join (path_parts )
111
+
112
+
59
113
@st .composite
60
114
def bip32_paths (draw ):
61
- depth = draw (st .integers (min_value = 0 , max_value = 255 ))
115
+ """
116
+ Generate BIP32 paths with some out of bound values.
117
+ """
118
+ MAX_ALLOWED_DEPTH = 255
119
+ MAX_UNHARDENED_IDX = 2 ** 31 - 1
120
+
121
+ depth = draw (st .integers (min_value = 0 , max_value = (MAX_ALLOWED_DEPTH + 3 )))
62
122
path_parts = ["m" ]
63
123
for _ in range (depth ):
64
- index = draw (st .integers (min_value = 0 , max_value = (2 ** 31 - 1 )))
124
+ index = draw (st .integers (min_value = - 2 , max_value = (MAX_UNHARDENED_IDX + 2 )))
65
125
hardened = draw (st .booleans ())
66
126
path_parts .append (f"{ index } { "h" if hardened else '' } " )
67
127
return "/" .join (path_parts )
68
128
69
129
70
- @given (seed_hex_str = valid_seeds , bip32_path = bip32_paths ())
130
+ @given (seed_hex_str = valid_seeds , bip32_path = py_compatible_bip32_paths ())
71
131
@settings (max_examples = 2_000 )
72
- def test_impls (seed_hex_str , bip32_path ):
132
+ def test_versus_py (seed_hex_str , bip32_path ):
133
+ """Compare implementations of BIP32 on a random seed and path."""
73
134
with timer ('ours' ):
74
135
ours = our_derive (seed_hex_str , bip32_path )
75
136
with timer ('python-bip32' ):
76
- theirs = their_derive (seed_hex_str , bip32_path )
77
- assert ours == theirs
137
+ pys = py_derive (seed_hex_str , bip32_path )
138
+
139
+ assert ours == pys
140
+
141
+
142
+ @given (seed_hex_str = valid_seeds , bip32_path = py_compatible_bip32_paths ())
143
+ @settings (max_examples = 100 , deadline = 5000 ) # verstable is slooooww, so allow 5s tests
144
+ def test_versus_vs (seed_hex_str , bip32_path ):
145
+ """
146
+ Since the verystable implemention is VERY slow (100x+), limit the number of cases.
147
+ """
148
+ with timer ('ours' ):
149
+ ours = our_derive (seed_hex_str , bip32_path )
150
+ with timer ('verystable' ):
151
+ vs = verystable_derive (seed_hex_str , bip32_path )
152
+
153
+ assert ours == vs
78
154
79
155
80
- @given (bip32_path = bip32_paths ())
156
+ @given (bip32_path = py_compatible_bip32_paths ())
81
157
@settings (max_examples = 200 )
82
158
def test_xpub_impls (bip32_path ):
83
159
xpub = 'xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ'
84
160
85
161
with timer ('ours' ):
86
162
ours = our_derive (xpub , bip32_path )
87
163
with timer ('python-bip32' ):
88
- theirs = their_xpub_derive (xpub , bip32_path )
89
- assert ours == theirs
164
+ pys = py_xpub_derive (xpub , bip32_path )
165
+ assert ours == pys
90
166
91
167
92
168
if __name__ == "__main__" :
93
- pytest .main ([__file__ , "-v" , "--capture=no" , "--hypothesis-show-statistics" ])
169
+ pytest .main ([__file__ , "-v" , "--capture=no" , "--hypothesis-show-statistics" , "-x" ] + sys . argv [ 1 : ])
0 commit comments