Skip to content

Commit 0cf37f6

Browse files
committed
Merge PR #42
2 parents 1492d39 + a1cb75a commit 0cf37f6

5 files changed

Lines changed: 211 additions & 20 deletions

File tree

.github/workflows/python-package.yml

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ jobs:
1010
tests:
1111
strategy:
1212
matrix:
13-
python-version: [3.7, 3.8, 3.9, '3.10']
13+
python-version: [3.8, 3.9, '3.10', 3.11, 3.12]
1414
os:
1515
- ubuntu-latest
16-
- macOS-latest
16+
- macos-13 # (non-M1)
17+
- macos-latest # (M1)
1718
- windows-latest
1819

1920
runs-on: ${{ matrix.os }}
@@ -23,14 +24,20 @@ jobs:
2324
uses: actions/setup-python@v2
2425
with:
2526
python-version: ${{ matrix.python-version }}
26-
- name: Installation (deps and package)
27+
- name: Install pip and setuptools
2728
run: |
2829
python -m pip install --upgrade pip
29-
pip install pytest
30-
pip install -r requirements.txt -r tests/requirements.txt
31-
python setup.py install
30+
pip install setuptools
31+
- name: On MacOS, install coincurve's dependencies and install it from wheels # FIXME: installing from source fails for some reason.
32+
if: matrix.os == 'macos-latest' || matrix.os == 'macos-13'
33+
run: |
34+
brew install autoconf automake libffi libtool pkg-config python
35+
pip install -r requirements.txt
36+
- name: Install python-bip32 from source
37+
run: python setup.py install
3238
- name: Test with pytest
3339
run: |
40+
pip install -r tests/requirements.txt
3441
pytest -vvv
3542
3643
linter:
@@ -58,11 +65,11 @@ jobs:
5865
- name: Set up Python 3.10
5966
uses: actions/setup-python@v2
6067
with:
61-
python-version: '3.10'
68+
python-version: 3.12
6269
- name: Testing with Coincurve ${{ matrix.coincurve-version }}
6370
run: |
6471
python -m pip install --upgrade pip
65-
pip install pytest
66-
pip install -r requirements.txt -r tests/requirements.txt
72+
pip install setuptools
73+
pip install -r tests/requirements.txt
6774
pip install -I coincurve==${{ matrix.coincurve-version }}
6875
python setup.py install

bip32/base58.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
"""Base58 encoding
2+
3+
Implementations of Base58 and Base58Check encodings that are compatible
4+
with the bitcoin network.
5+
6+
This file was copied over and added to the bip32 project from David Keijser's https://github.com/keis/base58 (https://pypi.org/project/base58/). This
7+
package is released under an MIT licensed. The code was copied in this file and left untouched. Here is a copy of the MIT license accompanying the
8+
code:
9+
Copyright (c) 2015 David Keijser
10+
11+
Permission is hereby granted, free of charge, to any person obtaining a copy
12+
of this software and associated documentation files (the "Software"), to deal
13+
in the Software without restriction, including without limitation the rights
14+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15+
copies of the Software, and to permit persons to whom the Software is
16+
furnished to do so, subject to the following conditions:
17+
18+
The above copyright notice and this permission notice shall be included in
19+
all copies or substantial portions of the Software.
20+
21+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27+
THE SOFTWARE.
28+
"""
29+
30+
# This module is based upon base58 snippets found scattered over many bitcoin
31+
# tools written in python. From what I gather the original source is from a
32+
# forum post by Gavin Andresen, so direct your praise to him.
33+
# This module adds shiny packaging and support for python3.
34+
35+
from functools import lru_cache
36+
from hashlib import sha256
37+
from typing import Mapping, Union
38+
39+
# 58 character alphabet used
40+
BITCOIN_ALPHABET = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
41+
RIPPLE_ALPHABET = b"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"
42+
XRP_ALPHABET = RIPPLE_ALPHABET
43+
44+
# Retro compatibility
45+
alphabet = BITCOIN_ALPHABET
46+
47+
48+
def scrub_input(v: Union[str, bytes]) -> bytes:
49+
if isinstance(v, str):
50+
v = v.encode("ascii")
51+
52+
return v
53+
54+
55+
def b58encode_int(
56+
i: int, default_one: bool = True, alphabet: bytes = BITCOIN_ALPHABET
57+
) -> bytes:
58+
"""
59+
Encode an integer using Base58
60+
"""
61+
if not i and default_one:
62+
return alphabet[0:1]
63+
string = b""
64+
base = len(alphabet)
65+
while i:
66+
i, idx = divmod(i, base)
67+
string = alphabet[idx : idx + 1] + string
68+
return string
69+
70+
71+
def b58encode(v: Union[str, bytes], alphabet: bytes = BITCOIN_ALPHABET) -> bytes:
72+
"""
73+
Encode a string using Base58
74+
"""
75+
v = scrub_input(v)
76+
77+
origlen = len(v)
78+
v = v.lstrip(b"\0")
79+
newlen = len(v)
80+
81+
acc = int.from_bytes(v, byteorder="big") # first byte is most significant
82+
83+
result = b58encode_int(acc, default_one=False, alphabet=alphabet)
84+
return alphabet[0:1] * (origlen - newlen) + result
85+
86+
87+
@lru_cache()
88+
def _get_base58_decode_map(alphabet: bytes, autofix: bool) -> Mapping[int, int]:
89+
invmap = {char: index for index, char in enumerate(alphabet)}
90+
91+
if autofix:
92+
groups = [b"0Oo", b"Il1"]
93+
for group in groups:
94+
pivots = [c for c in group if c in invmap]
95+
if len(pivots) == 1:
96+
for alternative in group:
97+
invmap[alternative] = invmap[pivots[0]]
98+
99+
return invmap
100+
101+
102+
def b58decode_int(
103+
v: Union[str, bytes], alphabet: bytes = BITCOIN_ALPHABET, *, autofix: bool = False
104+
) -> int:
105+
"""
106+
Decode a Base58 encoded string as an integer
107+
"""
108+
if b" " not in alphabet:
109+
v = v.rstrip()
110+
v = scrub_input(v)
111+
112+
map = _get_base58_decode_map(alphabet, autofix=autofix)
113+
114+
decimal = 0
115+
base = len(alphabet)
116+
try:
117+
for char in v:
118+
decimal = decimal * base + map[char]
119+
except KeyError as e:
120+
raise ValueError("Invalid character {!r}".format(chr(e.args[0]))) from None
121+
return decimal
122+
123+
124+
def b58decode(
125+
v: Union[str, bytes], alphabet: bytes = BITCOIN_ALPHABET, *, autofix: bool = False
126+
) -> bytes:
127+
"""
128+
Decode a Base58 encoded string
129+
"""
130+
v = v.rstrip()
131+
v = scrub_input(v)
132+
133+
origlen = len(v)
134+
v = v.lstrip(alphabet[0:1])
135+
newlen = len(v)
136+
137+
acc = b58decode_int(v, alphabet=alphabet, autofix=autofix)
138+
139+
return acc.to_bytes(origlen - newlen + (acc.bit_length() + 7) // 8, "big")
140+
141+
142+
def b58encode_check(v: Union[str, bytes], alphabet: bytes = BITCOIN_ALPHABET) -> bytes:
143+
"""
144+
Encode a string using Base58 with a 4 character checksum
145+
"""
146+
v = scrub_input(v)
147+
148+
digest = sha256(sha256(v).digest()).digest()
149+
return b58encode(v + digest[:4], alphabet=alphabet)
150+
151+
152+
def b58decode_check(
153+
v: Union[str, bytes], alphabet: bytes = BITCOIN_ALPHABET, *, autofix: bool = False
154+
) -> bytes:
155+
"""Decode and verify the checksum of a Base58 encoded string"""
156+
157+
result = b58decode(v, alphabet=alphabet, autofix=autofix)
158+
result, check = result[:-4], result[-4:]
159+
digest = sha256(sha256(result).digest()).digest()
160+
161+
if check != digest[:4]:
162+
raise ValueError("Invalid checksum")
163+
164+
return result

bip32/bip32.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import base58
21
import hashlib
32
import hmac
43

4+
from .base58 import b58encode_check, b58decode_check
55
from .utils import (
66
HARDENED_INDEX,
77
_derive_hardened_private_child,
@@ -211,7 +211,7 @@ def get_xpriv_from_path(self, path):
211211
self.network,
212212
)
213213

214-
return base58.b58encode_check(extended_key).decode()
214+
return b58encode_check(extended_key).decode()
215215

216216
def get_xpub_from_path(self, path):
217217
"""Get an encoded extended pubkey from a derivation path.
@@ -242,11 +242,11 @@ def get_xpub_from_path(self, path):
242242
self.network,
243243
)
244244

245-
return base58.b58encode_check(extended_key).decode()
245+
return b58encode_check(extended_key).decode()
246246

247247
def get_xpriv(self):
248248
"""Get the base58 encoded extended private key."""
249-
return base58.b58encode_check(self.get_xpriv_bytes()).decode()
249+
return b58encode_check(self.get_xpriv_bytes()).decode()
250250

251251
def get_xpriv_bytes(self):
252252
"""Get the encoded extended private key."""
@@ -263,7 +263,7 @@ def get_xpriv_bytes(self):
263263

264264
def get_xpub(self):
265265
"""Get the encoded extended public key."""
266-
return base58.b58encode_check(self.get_xpub_bytes()).decode()
266+
return b58encode_check(self.get_xpub_bytes()).decode()
267267

268268
def get_xpub_bytes(self):
269269
"""Get the encoded extended public key."""
@@ -285,7 +285,7 @@ def from_xpriv(cls, xpriv):
285285
if not isinstance(xpriv, str):
286286
raise InvalidInputError("'xpriv' must be a string")
287287

288-
extended_key = base58.b58decode_check(xpriv)
288+
extended_key = b58decode_check(xpriv)
289289
(
290290
network,
291291
depth,
@@ -313,7 +313,7 @@ def from_xpub(cls, xpub):
313313
if not isinstance(xpub, str):
314314
raise InvalidInputError("'xpub' must be a string")
315315

316-
extended_key = base58.b58decode_check(xpub)
316+
extended_key = b58decode_check(xpub)
317317
(
318318
network,
319319
depth,

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
coincurve>=15.0,<19
2-
base58~=2.0

setup.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
1-
from setuptools import setup
2-
import bip32
31
import io
2+
import os
3+
4+
from setuptools import setup
5+
6+
7+
# Taken from https://github.com/pypa/pip/blob/003c7ac56b4da80235d4a147fbcef84b6fbc8248/setup.py#L7-L21
8+
def read(rel_path: str) -> str:
9+
here = os.path.abspath(os.path.dirname(__file__))
10+
# intentionally *not* adding an encoding option to open, See:
11+
# https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690
12+
with open(os.path.join(here, rel_path)) as fp:
13+
return fp.read()
14+
15+
16+
# Taken from https://github.com/pypa/pip/blob/003c7ac56b4da80235d4a147fbcef84b6fbc8248/setup.py#L7-L21
17+
def get_version(rel_path: str) -> str:
18+
for line in read(rel_path).splitlines():
19+
if line.startswith("__version__"):
20+
# __version__ = "0.9"
21+
delim = '"' if '"' in line else "'"
22+
return line.split(delim)[1]
23+
raise RuntimeError("Unable to find version string.")
424

525

626
with io.open("README.md", encoding="utf-8") as f:
@@ -10,7 +30,8 @@
1030
requirements = [r for r in f.read().split('\n') if len(r)]
1131

1232
setup(name="bip32",
13-
version=bip32.__version__,
33+
# We use the first approach from https://packaging.python.org/en/latest/guides/single-sourcing-package-version
34+
version=get_version("bip32/__init__.py"),
1435
description="Minimalistic implementation of the BIP32 key derivation scheme",
1536
long_description=long_description,
1637
long_description_content_type="text/markdown",

0 commit comments

Comments
 (0)