Skip to content

Commit d9dc3f1

Browse files
committed
Moved SLIP-39 backup of BIP-39 en/decoding into create; pass unit tests
o Ensure that default derivation path is available in from_...
1 parent a985377 commit d9dc3f1

File tree

10 files changed

+283
-92
lines changed

10 files changed

+283
-92
lines changed

README.org

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,17 @@ by entering the mnemonics right on the device.
107107
The account owner might store their First and Second group data in their home and office safes.
108108
These are 1/1 groups (1 required, and only 1 member, so each of these are3 1-card groups.)
109109

110-
If the seed needs to be recovered, collecting the First and Second cards from the home and
111-
office safe is sufficient to recover the seed, and re-generate all of the HD Wallet accounts.
110+
If the Seed needs to be recovered, collecting the First and Second cards from the home and
111+
office safe is sufficient to recover the Seed, and re-generate all of the HD Wallet accounts.
112112

113113
Only 2 Fam group member's cards must be collected to recover the Fam group's data. So, if the HD
114114
Wallet owner loses their home (and the one and only First group card) in a fire, they could get
115115
the one Second group card from the office safe, and also 2 cards from Fam group members, and
116-
recover the seed and all of their wallets.
116+
recover the Seed and all of their wallets.
117117

118118
If catastrophe strikes and the wallet owner dies, and the heirs don't have access to either the
119119
First (at home) or Second (at the office) cards, they can collect 2 Fam cards and 3 Frens cards
120-
(at the funeral, for example), completing the Fam and Frens groups' data, and recover the seed,
120+
(at the funeral, for example), completing the Fam and Frens groups' data, and recover the Seed,
121121
and all derived HD Wallet accounts.
122122

123123
Since Frens are less likely to persist long term, we'll produce more (6) of these cards.
@@ -127,8 +127,8 @@ by entering the mnemonics right on the device.
127127

128128
* SLIP-39 Account Creation, Recovery and Address Generation
129129

130-
Generating a new SLIP-39 encoded seed is easy, with results available as PDF and text. Any number
131-
of derived HD wallet account addresses can be generated from this seed, and the seed (and all
130+
Generating a new SLIP-39 encoded Seed is easy, with results available as PDF and text. Any number
131+
of derived HD wallet account addresses can be generated from this Seed, and the Seed (and all
132132
derived HD wallets, for all cryptocurrencies) can be recovered by collecting the desired groups of
133133
recover card phrases. The default recovery groups are as described above.
134134

@@ -141,7 +141,7 @@ by entering the mnemonics right on the device.
141141
[[./images/slip39-cards.png]]
142142

143143
Run the following to obtain a PDF file containing business cards with the default SLIP-39 groups for
144-
a new account seed named "Personal"; insert a USB drive to collect the output, and run:
144+
a new account Seed named "Personal"; insert a USB drive to collect the output, and run:
145145

146146
#+LATEX: {\scriptsize
147147
#+BEGIN_EXAMPLE
@@ -192,12 +192,12 @@ by entering the mnemonics right on the device.
192192

193193
*** Supported Cryptocurrencies
194194

195-
While the SLIP-39 seed is not cryptocurrency-specific (any wallet for any cryptocurrency can be
195+
While the SLIP-39 Seed is not cryptocurrency-specific (any wallet for any cryptocurrency can be
196196
derived from it), each type of cryptocurrency has its own standard derivation path
197197
(eg. =m/44'/3'/0'/0/0= for DOGE), and its own address representation (eg. Bech32 at
198198
=m/84'/0'/0'/0/0= for BTC eg. =bc1qcupw7k8enymvvsa7w35j5hq4ergtvus3zk8a8s=.
199199

200-
When you import your SLIP-39 seed into a Trezor, you gain access to all derived HD
200+
When you import your SLIP-39 Seed into a Trezor, you gain access to all derived HD
201201
cryptocurrency wallets supported directly by that hardware wallet, and *indirectly*, to any coin
202202
and/or blockchain network supported by any wallet software (eg. Metamask).
203203

@@ -255,7 +255,7 @@ by entering the mnemonics right on the device.
255255

256256
** The Python =slip39= CLI
257257

258-
From the command line, you can create SLIP-39 seed Mnemonic card PDFs.
258+
From the command line, you can create SLIP-39 Seed Mnemonic card PDFs.
259259

260260
*** =slip39= Synopsis
261261

slip39/api.py

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from functools import wraps
1313
from collections import namedtuple
14-
from typing import Dict, List, Sequence, Tuple, Union, Callable
14+
from typing import Dict, List, Sequence, Tuple, Optional, Union, Callable
1515

1616
from shamir_mnemonic import generate_mnemonics
1717

@@ -20,6 +20,7 @@
2020

2121
from .defaults import BITS_DEFAULT, BITS, MNEM_ROWS_COLS, GROUP_REQUIRED_RATIO, CRYPTO_PATHS
2222
from .util import ordinal
23+
from .recovery import produce_bip39, recover_bip39
2324

2425
log = logging.getLogger( __package__ )
2526

@@ -72,7 +73,7 @@ def path_edit(
7273
if edit.startswith( '.' ):
7374
new_segs = edit.lstrip( './' ).split( '/' )
7475
cur_segs = path.split( '/' )
75-
log.info( f"Using {edit} to replace last {len(new_segs)} of {path} with {'/'.join(new_segs)}" )
76+
log.debug( f"Using {edit} to replace last {len(new_segs)} of {path} with {'/'.join(new_segs)}" )
7677
if len( new_segs ) >= len( cur_segs ):
7778
raise ValueError( f"Cannot use {edit} to replace last {len(new_segs)} of {path} with {'/'.join(new_segs)}" )
7879
res_segs = cur_segs[:len(cur_segs)-len(new_segs)] + new_segs
@@ -267,7 +268,7 @@ class Account:
267268
| XRP | Legacy | m/44'/144'/0'/0/0 | r... | Beta |
268269
269270
"""
270-
CRYPTO_NAMES = dict( # Currently supported (in order of visibility)
271+
CRYPTO_NAMES = dict( # Currently supported (in order of visibility)
271272
ethereum = 'ETH',
272273
bitcoin = 'BTC',
273274
litecoin = 'LTC',
@@ -371,14 +372,26 @@ def supported( cls, crypto ):
371372
for it, or raises an a ValueError. Eg. "Ethereum" --> "ETH"
372373
373374
"""
374-
validated = cls.CRYPTO_NAMES.get(
375+
validated = cls.CRYPTO_NAMES.get(
375376
crypto.lower(),
376377
crypto.upper() if crypto.upper() in cls.CRYPTOCURRENCIES else None
377378
)
378379
if validated:
379380
return validated
380381
raise ValueError( f"{crypto} not presently supported; specify {', '.join( cls.CRYPTOCURRENCIES )}" )
381382

383+
def __str__( self ):
384+
"""Until from_seed/from_path are invoked, may not have an address or derivation path."""
385+
address = None
386+
try:
387+
address = self.address
388+
except Exception:
389+
pass
390+
return f"{self.crypto}: {address}"
391+
392+
def __repr__( self ):
393+
return f"{self.__class__.__name__}({self} @{self.path})"
394+
382395
def __init__( self, crypto, format=None ):
383396
crypto = Account.supported( crypto )
384397
cryptocurrency = self.CRYPTO_LOCAL.get( crypto )
@@ -416,15 +429,14 @@ def from_path( self, path: str = None ) -> "Account":
416429
"""Change the Account to derive from the provided path.
417430
418431
If a partial path is provided (eg "...1'/0/3"), then use it to replace the given segments in
419-
current account path, leaving the remainder alone.
432+
current (or the default) account path, leaving the remainder alone.
420433
421434
"""
435+
from_path = self.path or Account.path_default( self.crypto, self.format )
422436
if path:
423-
path = path_edit( self.path, path )
424-
else:
425-
path = Account.path_default( self.crypto, self.format )
437+
from_path = path_edit( from_path, path )
426438
self.hdwallet.clean_derivation()
427-
self.hdwallet.from_path( path )
439+
self.hdwallet.from_path( from_path )
428440
return self
429441

430442
@property
@@ -656,15 +668,19 @@ def path_sequence(
656668

657669

658670
def cryptopaths_parser( cryptocurrency, edit=None ):
659-
"""
660-
Generate a standard cryptopaths list, from the given list of "<crypto>[:<paths>]"
661-
cryptocurrencies (default: CRYPTO_PATHS).
671+
"""Generate a standard cryptopaths list, from the given sequnce of (<crypto>,<paths>) or
672+
"<crypto>[:<paths>]" cryptocurrencies (default: CRYPTO_PATHS).
662673
663-
Adjusts the provided derivation paths by an optional eg. "../-" path adjustment."""
674+
Adjusts the provided derivation paths by an optional eg. "../-" path adjustment.
675+
676+
"""
664677
cryptopaths = []
665678
for crypto in cryptocurrency or CRYPTO_PATHS:
666679
try:
667-
crypto,paths = crypto.split( ':' )
680+
if type(crypto) is str:
681+
crypto,paths = crypto.split( ':' ) # A sequence of str
682+
else:
683+
crypto,paths = crypto # A sequence of tuples
668684
except ValueError:
669685
crypto,paths = crypto,None
670686
crypto = Account.supported( crypto )
@@ -770,33 +786,65 @@ def create(
770786
name: str,
771787
group_threshold: int,
772788
groups: Dict[str,Tuple[int, int]],
773-
master_secret: bytes = None, # Default: 128-bit seeds
789+
master_secret: bytes = None, # Default: 128-bit seeds
774790
passphrase: bytes = b"",
791+
using_bip39: bool = False, # Generate wallet Seed from master_secret Entropy using BIP-39 generation
775792
iteration_exponent: int = 1,
776-
cryptopaths: Sequence[Tuple[str,str]] = None, # default: ETH, BTC at default paths
793+
cryptopaths: Optional[Sequence[Union[str,Tuple[str,str]]]] = None, # default: ETH, BTC at default paths
777794
strength: int = 128,
778795
) -> Tuple[str,int,Dict[str,Tuple[int,List[str]]], Sequence[Sequence[Account]]]:
779-
"""Creates a SLIP-39 encoding and 1 or more Ethereum accounts. Returns the details, in a form
780-
directly compatible with the layout.produce_pdf API.
796+
"""Creates a SLIP-39 encoding for supplied master_secret Entropy, and 1 or more Cryptocurrency
797+
accounts. Returns the details, in a form directly compatible with the layout.produce_pdf API.
798+
799+
Creates accountgroups derived from the Seed Entropy. By default, this is done in the SLIP-39
800+
standard, using the master_secret Entropy directly. If a passphrase is supplied, this is also
801+
used in the SLIP-39 standard fashion (not recommended -- not Trezor "Model T" compatible).
802+
803+
If using_bip39, creates the Cryptocurrency accountgroups from the supplied master_secret
804+
Entropy, but by generating the Seed from a BIP-38 Mnemonic produced from the provided entropy
805+
(or generated, default 128 bits), plus the supplied passphrase.
781806
782807
"""
808+
783809
if master_secret is None:
784810
assert strength in BITS, f"Invalid {strength}-bit secret length specified"
785811
master_secret = random_secret( strength // 8 )
812+
786813
g_names,g_dims = list( zip( *groups.items() ))
814+
815+
# Derive the desired account(s) at the specified derivation paths, or the default, using either
816+
# BIP-39 Seed generation, or directly from Entropy for SLIP-39.
817+
if using_bip39:
818+
# For BIP-39, the passphrase is consumed here, and Cryptocurrency accounts are generated
819+
# using the BIP-39 Seed generated from entropy + passphrase
820+
bip39_mnem = produce_bip39( entropy=master_secret )
821+
bip39_seed = recover_bip39(
822+
mnemonic = bip39_mnem,
823+
passphrase = passphrase,
824+
)
825+
accts = list( accountgroups(
826+
master_secret = bip39_seed,
827+
cryptopaths = cryptopaths,
828+
allow_unbounded = False,
829+
))
830+
passphrase = b""
831+
else:
832+
# For SLIP-39, accounts are generated directly from supplied Entropy, and passphrase
833+
# encrypts the SLIP-39 Mnemonics, below.
834+
accts = list( accountgroups(
835+
master_secret = master_secret,
836+
cryptopaths = cryptopaths,
837+
allow_unbounded = False,
838+
))
839+
840+
# Generate the SLIP-39 Mnemonics representing the supplied
787841
mnems = mnemonics(
788842
group_threshold = group_threshold,
789843
groups = g_dims,
790844
master_secret = master_secret,
791845
passphrase = passphrase,
792846
iteration_exponent= iteration_exponent
793847
)
794-
# Derive the desired account(s) at the specified derivation paths, or the default
795-
accts = list( accountgroups(
796-
master_secret = master_secret,
797-
cryptopaths = cryptopaths or [('ETH',None), ('BTC',None)],
798-
allow_unbounded = False,
799-
))
800848

801849
groups = {
802850
g_name: (g_of, g_mnems)
@@ -859,6 +907,7 @@ def account(
859907
seed = master_secret,
860908
path = path,
861909
)
910+
log.debug( f"Created {acct} from {len(master_secret)*8}-bit seed, at derivation path {path}" )
862911
return acct
863912

864913

@@ -879,7 +928,7 @@ def accounts(
879928

880929
def accountgroups(
881930
master_secret: bytes,
882-
cryptopaths: Sequence[Tuple[str,str]],
931+
cryptopaths: Optional[Sequence[Union[str,Tuple[str,str]]]] = None, # Default: ETH, BTC
883932
allow_unbounded: bool = True,
884933
) -> Sequence[Sequence[Account]]:
885934
"""Generate the desired cryptocurrency account(s) at each crypto's given path(s). This is useful
@@ -910,7 +959,7 @@ def accountgroups(
910959
crypto = crypto,
911960
allow_unbounded = allow_unbounded,
912961
)
913-
for crypto,paths in cryptopaths
962+
for crypto,paths in cryptopaths_parser( cryptopaths )
914963
])
915964

916965

@@ -947,7 +996,7 @@ def addresses(
947996

948997
def addressgroups(
949998
master_secret: bytes,
950-
cryptopaths: Sequence[Tuple[str,str]],
999+
cryptopaths: Optional[Sequence[Union[str,Tuple[str,str]]]] = None, # Default ETH, BTC
9511000
allow_unbounded: bool = True,
9521001
) -> Sequence[str]:
9531002
"""Yields account (<crypto>, <path>, <address>) records for the desired cryptocurrencies at paths.
@@ -960,5 +1009,5 @@ def addressgroups(
9601009
crypto = crypto,
9611010
allow_unbounded = allow_unbounded,
9621011
)
963-
for crypto,paths in cryptopaths
1012+
for crypto,paths in cryptopaths_parser( cryptopaths )
9641013
])

0 commit comments

Comments
 (0)