Skip to content

Commit 9eb23ba

Browse files
committed
Unit tests pass; major updates and improvements
1 parent b2204ef commit 9eb23ba

24 files changed

+2075
-1093
lines changed

README.org

Lines changed: 428 additions & 307 deletions
Large diffs are not rendered by default.

README.pdf

602 KB
Binary file not shown.

README.txt

Lines changed: 1380 additions & 648 deletions
Large diffs are not rendered by default.
File renamed without changes.
File renamed without changes.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@
216216
217217
On an secure (ideally air-gapped) computer, new seeds can /safely/ be generated (*without*
218218
trusting this program) and the PDF saved to a USB drive for printing (or directly printed without
219-
the file being saved to disk.). Presently, =slip39= can output example ETH, BTC, LTC, DOGE, BNB,
219+
the file being saved to disk.). Presently, =slip39= can output example ETH, BTC, LTC, DOGE, BSC,
220220
and XRP addresses derived from the seed, to /illustrate/ what accounts are associated with the
221221
backed-up seed. Recovery of the seed to a [trezor-model-t][3] is simple, by entering the mnemonics
222222
right on the device.

slip39/api.py

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,15 @@
3333
from typing import Dict, List, Sequence, Tuple, Optional, Union, Callable
3434

3535
from shamir_mnemonic import generate_mnemonics
36+
from shamir_mnemonic.shamir import RANDOM_BYTES
3637

3738
import hdwallet
3839
from hdwallet import cryptocurrencies
3940

40-
from .defaults import BITS_DEFAULT, BITS, MNEM_ROWS_COLS, GROUP_REQUIRED_RATIO, CRYPTO_PATHS
41-
from .util import ordinal, commas
41+
from .defaults import (
42+
BITS_DEFAULT, BITS, MNEM_ROWS_COLS, GROUPS, GROUP_REQUIRED_RATIO, GROUP_THRESHOLD_RATIO, CRYPTO_PATHS
43+
)
44+
from .util import ordinal, commas, is_listlike, is_mapping
4245
from .recovery import produce_bip39, recover_bip39, recover as recover_slip39
4346

4447
__author__ = "Perry Kundert"
@@ -48,6 +51,7 @@
4851

4952
log = logging.getLogger( __package__ )
5053

54+
5155
# Support for private key encryption via BIP-38 and Ethereum JSON wallet is optional; pip install slip39[wallet]
5256
paper_wallet_issues = []
5357
try:
@@ -73,9 +77,6 @@
7377
paper_wallet_issues.append( message )
7478

7579

76-
RANDOM_BYTES = secrets.token_bytes
77-
78-
7980
def paper_wallet_available():
8081
"""Determine if encrypted BIP-38 and Ethereum JSON Paper Wallets are available."""
8182
available = AES and scrypt and eth_account
@@ -118,7 +119,7 @@ def path_edit(
118119

119120
class BinanceMainnet( cryptocurrencies.Cryptocurrency ):
120121
NAME = "Binance"
121-
SYMBOL = "BNB"
122+
SYMBOL = "BSC"
122123
NETWORK = "mainnet"
123124
SOURCE_CODE = "https://github.com/bnb-chain/bsc"
124125
COIN_TYPE = cryptocurrencies.CoinType({
@@ -249,7 +250,7 @@ class Account:
249250
| Crypto | Semantic | Path | Address | Support |
250251
|--------+----------+-------------------+---------+---------|
251252
| ETH | Legacy | m/44'/ 60'/0'/0/0 | 0x... | |
252-
| BNB | Legacy | m/44'/ 60'/0'/0/0 | 0x... | Beta |
253+
| BSC | Legacy | m/44'/ 60'/0'/0/0 | 0x... | Beta |
253254
| BTC | Legacy | m/44'/ 0'/0'/0/0 | 1... | |
254255
| | SegWit | m/49'/ 0'/0'/0/0 | 3... | |
255256
| | Bech32 | m/84'/ 0'/0'/0/0 | bc1... | |
@@ -267,7 +268,7 @@ class Account:
267268
BTC = 'Bitcoin',
268269
LTC = 'Litecoin',
269270
DOGE = 'Dogecoin',
270-
BNB = 'Binance',
271+
BSC = 'Binance',
271272
XRP = 'Ripple',
272273
)
273274
CRYPTO_DECIMALS = dict(
@@ -283,7 +284,7 @@ class Account:
283284
BTC = 24,
284285
LTC = 24,
285286
DOGE = 24,
286-
BNB = 18,
287+
BSC = 18,
287288
XRP = 6,
288289
)
289290
CRYPTO_NAMES = dict(
@@ -293,21 +294,21 @@ class Account:
293294
bitcoin = 'BTC',
294295
litecoin = 'LTC',
295296
dogecoin = 'DOGE',
296-
binance = 'BNB',
297+
binance = 'BSC',
297298
ripple = 'XRP',
298299
)
299300
CRYPTOCURRENCIES = set( CRYPTO_NAMES.values() )
300-
CRYPTOCURRENCIES_BETA = set( ('BNB', 'XRP') )
301+
CRYPTOCURRENCIES_BETA = set( ('BSC', 'XRP') )
301302

302-
ETHJS_ENCRYPT = set( ('ETH', 'BNB') ) # Can be encrypted w/ Ethereum JSON wallet
303+
ETHJS_ENCRYPT = set( ('ETH', 'BSC') ) # Can be encrypted w/ Ethereum JSON wallet
303304
BIP38_ENCRYPT = CRYPTOCURRENCIES - ETHJS_ENCRYPT # Can be encrypted w/ BIP-38
304305

305306
CRYPTO_FORMAT = dict(
306307
ETH = "legacy",
307308
BTC = "bech32",
308309
LTC = "bech32",
309310
DOGE = "legacy",
310-
BNB = "legacy",
311+
BSC = "legacy",
311312
XRP = "legacy",
312313
)
313314

@@ -317,11 +318,11 @@ class Account:
317318
XRP = XRPHDWallet,
318319
)
319320
CRYPTO_LOCAL = dict(
320-
BNB = BinanceMainnet,
321+
BSC = BinanceMainnet,
321322
XRP = RippleMainnet,
322323
)
323324
CRYPTO_LOCAL_SYMBOL = dict(
324-
BNB = "ETH"
325+
BSC = "ETH"
325326
)
326327

327328
# The available address formats and default derivation paths.
@@ -331,7 +332,7 @@ class Account:
331332
ETH = dict(
332333
legacy = "m/44'/60'/0'/0/0",
333334
),
334-
BNB = dict(
335+
BSC = dict(
335336
legacy = "m/44'/60'/0'/0/0",
336337
),
337338
BTC = dict(
@@ -356,7 +357,7 @@ class Account:
356357
ETH = dict(
357358
legacy = "p2pkh",
358359
),
359-
BNB = dict(
360+
BSC = dict(
360361
legacy = "p2pkh",
361362
),
362363
BTC = dict(
@@ -444,12 +445,6 @@ def __init__( self, crypto, format=None ):
444445
self.format = format.lower() if format else Account.address_format( crypto )
445446
semantic = self.CRYPTO_FORMAT_SEMANTIC[crypto][self.format]
446447
hdwallet_cls = self.CRYPTO_WALLET_CLS.get( crypto, hdwallet.HDWallet )
447-
# if hdwallet_cls is None and self.format in ("legacy",):
448-
# hdwallet_cls = hdwallet.HDWallet # hdwallet.BIP44HDWallet
449-
# if hdwallet_cls is None and self.format in ("segwit",):
450-
# hdwallet_cls = hdwallet.HDWALLET # hdwallet.BIP49HDWallet
451-
# if hdwallet_cls is None and self.format in ("bech32",):
452-
# hdwallet_cls = hdwallet.BIP84HDWallet
453448
if hdwallet_cls is None:
454449
raise ValueError( f"{crypto} does not support address format {self.format}" )
455450
self.hdwallet = hdwallet_cls( symbol=crypto, cryptocurrency=cryptocurrency, semantic=semantic )
@@ -894,11 +889,11 @@ def cryptopaths_parser(
894889

895890
cry,*pth = crypto
896891
pth,*fmt = pth or (None,)
897-
fmt, = fmt or (None,)
892+
fmt, = fmt or (format,)
898893

899894
cry = Account.supported( cry )
900895
if not pth:
901-
pth = Account.path_default( cry, fmt or format )
896+
pth = Account.path_default( cry, fmt )
902897
if hardened_defaults:
903898
pth,_ = path_hardened( pth )
904899
if edit:
@@ -907,7 +902,7 @@ def cryptopaths_parser(
907902

908903

909904
def random_secret(
910-
seed_length = BITS_DEFAULT // 8
905+
seed_length: Optional[int]
911906
) -> bytes:
912907
"""Generates a new random secret.
913908
@@ -930,6 +925,8 @@ def random_secret(
930925
2**128 / 1e9 / 7e9 / (60*60*24*365) == 1,541,469,010,115.145
931926
932927
"""
928+
assert seed_length, \
929+
f"Must supply a non-zero length in bytes, not {seed_length}"
933930
return RANDOM_BYTES( seed_length )
934931

935932

@@ -1002,21 +999,32 @@ def group_parser( group_spec ):
1002999

10031000
def create(
10041001
name: str,
1005-
group_threshold: int,
1006-
groups: Dict[str,Tuple[int, int]],
1007-
master_secret: Optional[bytes] = None, # Default: generate 128-bit Seed Entropy
1002+
group_threshold: Optional[Union[int,float]] = None, # Default: 1/2 of groups, rounded up
1003+
groups: Optional[Union[List[str],Dict[str,Tuple[int, int]]]] = None, # Default: 4 groups (see defaults.py)
1004+
master_secret: Optional[Union[str,bytes]] = None, # Default: generate 128-bit Seed Entropy
10081005
passphrase: Optional[Union[bytes,str]] = None,
1009-
using_bip39: bool = False, # Produce wallet Seed from master_secret Entropy using BIP-39 generation
1006+
using_bip39: Optional[bool] = None, # Produce wallet Seed from master_secret Entropy using BIP-39 generation
10101007
iteration_exponent: int = 1,
10111008
cryptopaths: Optional[Sequence[Union[str,Tuple[str,str],Tuple[str,str,str]]]] = None, # default: ETH, BTC at default path, format
1012-
strength: int = 128,
1009+
strength: Optional[int] = None, # Default: 128
10131010
) -> Tuple[str,int,Dict[str,Tuple[int,List[str]]], Sequence[Sequence[Account]], bool]:
10141011
"""Creates a SLIP-39 encoding for supplied master_secret Entropy, and 1 or more Cryptocurrency
10151012
accounts. Returns the Details, in a form directly compatible with the layout.produce_pdf API.
10161013
10171014
The master_secret Seed Entropy is discarded (because it is, of course, always recoverable from
10181015
the SLIP-39 mnemonics).
10191016
1017+
We strive to default to "do the right thing", here. If you supply BIP-39 Mnemonics, we'll
1018+
round-trip them via SLIP-39, and produce compatible crypto account addresses. This should be
1019+
the "typical" case, as most people already have BIP-39 Mnemonics. If you don't have a BIP-39
1020+
Mnemonic, make sure you set using_bip39 = True; this *also* implies that you *MUST* have already
1021+
converted your entropy to BIP-39 (or are going to recover it and do so, later), so we'll warn
1022+
you to do that. If a passphrase is provided, it is assumed to be a BIP-39 passphrase, and is used
1023+
to generate the crypto account addresses -- it will *not* be used to "encrypt" the SLIP-39!
1024+
1025+
If you supply raw entropy, we'll assume you have SLIP-39 compatible wallet and want to use it
1026+
directly.
1027+
10201028
Creates accountgroups derived from the Seed Entropy. By default, this is done in the SLIP-39
10211029
standard, using the master_secret Entropy directly. If a passphrase is supplied, this is also
10221030
used in the SLIP-39 standard fashion (not recommended -- not Trezor "Model T" compatible).
@@ -1027,16 +1035,33 @@ def create(
10271035
10281036
"""
10291037
if master_secret is None:
1030-
assert strength in BITS, f"Invalid {strength}-bit secret length specified"
1038+
if not strength:
1039+
strength = BITS_DEFAULT
10311040
master_secret = random_secret( strength // 8 )
1032-
1033-
g_names,g_dims = list( zip( *groups.items() ))
1041+
if isinstance( master_secret, bytes ) and not ( 128 <= len( master_secret ) * 8 <= 512 ):
1042+
log.warning( f"Strangely sized {len(master_secret) * 8}-bit entropy provided; may be weak" )
1043+
1044+
# If a non-hex str is passed as entropy, assume it is a BIP-39 Mnemonic, and that we want to use
1045+
# SLIP-39 to round-trip the underlying BIP-39 entropy AND derive compatible wallets.
1046+
if isinstance( master_secret, str ) and all( c in '0123456789abcdef' for c in master_secret.lower() ):
1047+
master_secret = codecs.decode( master_secret, 'hex_codec' )
1048+
if using_bip39 is None and isinstance( master_secret, str ):
1049+
# Assume it must be a BIP-39 Mnemonic
1050+
using_bip39 = True
1051+
else:
1052+
# Assume caller knows; default False (use SLIP-39 directly)
1053+
using_bip39 = bool( using_bip39 )
10341054

10351055
# Derive the desired account(s) at the specified derivation paths, or the default, using either
10361056
# BIP-39 Seed generation, or directly from Entropy for SLIP-39.
10371057
if using_bip39:
10381058
# For BIP-39, the passphrase is consumed here, and Cryptocurrency accounts are generated
1039-
# using the BIP-39 Seed generated from entropy + passphrase
1059+
# using the BIP-39 Seed generated from entropy + passphrase. This should be the "typical"
1060+
# use-case, where someone already has a BIP-39 Mnemonic and/or wants to use a "standard"
1061+
# BIP-39 compatible hardware wallet.
1062+
log.warning( f"Assuming BIP-39 seed entropy: Ensure you recover and use via a BIP-39 Mnemonic" )
1063+
if isinstance( master_secret, str ):
1064+
master_secret = recover_bip39( mnemonic=master_secret, as_entropy=True )
10401065
bip39_mnem = produce_bip39( entropy=master_secret )
10411066
bip39_seed = recover_bip39(
10421067
mnemonic = bip39_mnem,
@@ -1053,8 +1078,10 @@ def create(
10531078
passphrase = None # Consumed by BIP-39; not used for SLIP-39 Mnemonics "backup"!
10541079
else:
10551080
# For SLIP-39, accounts are generated directly from supplied Entropy, and passphrase
1056-
# encrypts the SLIP-39 Mnemonics, below.
1057-
log.info(
1081+
# encrypts the SLIP-39 Mnemonics, below. Using a SLIP-39 with a passphrase is so unlikely
1082+
# to be correct that we will warn about it! You almost *always* want to use SLIP-39
1083+
# *without* a passphase; use eg. Trezor "hidden wallets" instead.
1084+
(log.warning if passphrase else log.info)(
10581085
f"SLIP-39 for {name} from {len(master_secret)*8}-bit Entropy directly{' w/ SLIP-39 Passphrase' if passphrase else ''}"
10591086
)
10601087
accts = list( accountgroups(
@@ -1063,6 +1090,15 @@ def create(
10631090
allow_unbounded = False,
10641091
))
10651092

1093+
# Deduce groups, using defaults
1094+
if not groups:
1095+
groups = GROUPS
1096+
if not is_mapping( groups ):
1097+
if isinstance( groups, str ):
1098+
groups = groups.split( "," )
1099+
groups = dict( map( group_parser, groups ))
1100+
g_names,g_dims = list( zip( *groups.items() ))
1101+
10661102
# Generate the SLIP-39 Mnemonics representing the supplied master_secret Seed Entropy. This
10671103
# always recovers the Seed Entropy; if not using_bip39, this is also the wallet derivation Seed;
10681104
# if using_bip39, the wallet derivation Seed was produced from the BIP-39 Seed generation
@@ -1094,7 +1130,7 @@ def create(
10941130

10951131

10961132
def mnemonics(
1097-
group_threshold: int,
1133+
group_threshold: Optional[int], # Default: 1/2 of groups, rounded up
10981134
groups: Sequence[Tuple[int, int]],
10991135
master_secret: Optional[Union[str,bytes]] = None,
11001136
passphrase: Optional[Union[bytes,str]] = None,
@@ -1120,6 +1156,10 @@ def mnemonics(
11201156
f"Only {commas( BITS, final='and' )}-bit seeds supported; {len(master_secret)*8}-bit seed supplied" )
11211157
if isinstance( passphrase, str ):
11221158
passphrase = passphrase.encode( 'UTF-8' )
1159+
1160+
groups = list( groups )
1161+
if not group_threshold:
1162+
group_threshold = math.ceil( len( groups ) * GROUP_THRESHOLD_RATIO )
11231163
return generate_mnemonics(
11241164
group_threshold = group_threshold,
11251165
groups = groups,

slip39/api_test.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def test_account_format():
124124
crypto = 'Bitcoin',
125125
format = "legacy",
126126
)
127+
assert acct.format == 'legacy'
127128
assert acct.hdwallet.seed() == "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4"
128129
assert acct.path == "m/44'/0'/0'/0/0"
129130
assert acct.address == '1LqBGSKuX5yYUonjxT5qGfpUsXKYYWeabA'
@@ -232,9 +233,9 @@ def test_account_format():
232233

233234
def test_account_from_mnemonic():
234235
"""Test all the ways the entropy 0xffff...ffff can be encoded and HD Wallets derived."""
235-
# Raw 0xffff...ffff entropy as Seed. Not BIP-39 decoded (hashed) via mnemonic to produce Seed. This is
236-
# how SLIP-39 encodes and decodes entropy. The rot HD Wallet Seed directly uses the entropy,
237-
# un-molested.
236+
# Raw 0xffff...ffff entropy as Seed. Not BIP-39 decoded (hashed) via mnemonic to produce Seed.
237+
# This is how SLIP-39 encodes and decodes entropy. The raw HD Wallet Seed directly uses the
238+
# entropy, un-molested.
238239
acct_ones = account( SEED_ONES, crypto='Bitcoin' )
239240
assert acct_ones.path == "m/84'/0'/0'/0/0" # Default, BTC
240241
assert acct_ones.address == 'bc1q9yscq3l2yfxlvnlk3cszpqefparrv7tk24u6pl'
@@ -288,6 +289,14 @@ def test_account_from_mnemonic():
288289
assert acct.path == "m/44'/144'/0'/0/0" # Default
289290
assert acct.address == 'rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV'
290291

292+
# And straight from BIP-39 Mnemonics
293+
details_zoos_using_bip39 = create(
294+
"SLIP39 Wallet: From zoo ... BIP-39",
295+
master_secret = BIP39_ZOO,
296+
)
297+
[(eth_zoo,btc_zoo)] = details_zoos_using_bip39.accounts
298+
btc_zoo.address == 'bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2'
299+
291300

292301
@pytest.mark.skipif( not scrypt or not eth_account,
293302
reason="pip install slip39[wallet] to support private key encryption" )
@@ -550,31 +559,31 @@ def test_addressgroups():
550559
('Ripple', ".../-3"),
551560
],
552561
)))
553-
print( repr( addrgrps ))
562+
# print( repr( addrgrps ))
554563
assert addrgrps == [ # Verified
555564
(0,(('ETH', "m/44'/60'/0'/0/0", '0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E'), # Ledger
556565
('BTC', "m/84'/0'/0'/0/0", 'bc1qk0a9hr7wjfxeenz9nwenw9flhq0tmsf6vsgnn2'), # Ledger
557566
('LTC', "m/84'/2'/0'/0/0", 'ltc1qnreu4d88p5tvh33anptujvcvn3xmfhh43yg0am'), # Ledger
558567
('DOGE', "m/44'/3'/0'/0/0", 'DTMaJd8wqye1fymnjxZ5Cc5QkN1w4pMgXT'), # Ledger
559-
('BNB', "m/44'/60'/0'/0/0", '0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E'), # Ledger
568+
('BSC', "m/44'/60'/0'/0/0", '0xfc2077CA7F403cBECA41B1B0F62D91B5EA631B5E'), # Ledger
560569
('XRP', "m/44'/144'/0'/0/0", 'rUPzi4ZwoYxi7peKCqUkzqEuSrzSRyLguV'))), # Ledger
561570
(1, (('ETH', "m/44'/60'/0'/0/1", '0xd1a7451beB6FE0326b4B78e3909310880B781d66'),
562571
('BTC', "m/84'/0'/0'/0/1", 'bc1qkd33yck74lg0kaq4tdcmu3hk4yruhjayxpe9ug'),
563572
('LTC', "m/84'/2'/0'/0/1", 'ltc1qm4yc8vgxyv0xeu8p4vtq2wls245y2ueqpfrp4d'),
564573
('DOGE', "m/44'/3'/0'/0/1", 'DGkL2LD5FfccAaKtx8G7TST5iZwrNkecTY'),
565-
('BNB', "m/44'/60'/0'/0/1", '0xd1a7451beB6FE0326b4B78e3909310880B781d66'),
574+
('BSC', "m/44'/60'/0'/0/1", '0xd1a7451beB6FE0326b4B78e3909310880B781d66'),
566575
('XRP', "m/44'/144'/0'/0/1", 'ravkJwvQBuW4P5TG1qK5WDAgBxbPhdyPh1'))),
567576
(2, (('ETH', "m/44'/60'/0'/0/2", '0x578270B5E5B53336baC354756b763b309eCA90Ef'),
568577
('BTC', "m/84'/0'/0'/0/2", 'bc1qvr7e5aytd0hpmtaz2d443k364hprvqpm3lxr8w'),
569578
('LTC', "m/84'/2'/0'/0/2", 'ltc1qstkxz076qdyg0r08eszf0rrxsmfcgj62lkqaj2'),
570579
('DOGE', "m/44'/3'/0'/0/2", 'DQa3SpFZH3fFpEFAJHTXZjam4hWiv9muJX'),
571-
('BNB', "m/44'/60'/0'/0/2", '0x578270B5E5B53336baC354756b763b309eCA90Ef'),
580+
('BSC', "m/44'/60'/0'/0/2", '0x578270B5E5B53336baC354756b763b309eCA90Ef'),
572581
('XRP', "m/44'/144'/0'/0/2", 'rpzdHCsqVLppnUAUvgYDd6ADZFeKE6QoHR'))),
573582
(3, (('ETH', "m/44'/60'/0'/0/3", '0x909f59835A5a120EafE1c60742485b7ff0e305da'),
574583
('BTC', "m/84'/0'/0'/0/3", 'bc1q6t9vhestkcfgw4nutnm8y2z49n30uhc0kyjl0d'),
575584
('LTC', "m/84'/2'/0'/0/3", 'ltc1qts5sde8st3x6qt2t0xhtf9uactg7nnztamuehk'),
576585
('DOGE', "m/44'/3'/0'/0/3", 'DTW5tqLwspMY3NpW3RrgMfjWs5gnpXtfwe'),
577-
('BNB', "m/44'/60'/0'/0/3", '0x909f59835A5a120EafE1c60742485b7ff0e305da'),
586+
('BSC', "m/44'/60'/0'/0/3", '0x909f59835A5a120EafE1c60742485b7ff0e305da'),
578587
('XRP', "m/44'/144'/0'/0/3", 'r9czvGVozoKTAnP1G17RG9DfWK272ZExvX')))
579588
]
580589

0 commit comments

Comments
 (0)