#+title: Ethereum SLIP-39 Account Generation
#+date: 2021-12-20 10:55:00
#+draft: false
#+EXPORT_FILE_NAME: README.pdf
#+STARTUP: org-startup-with-inline-images inlineimages
#+STARTUP: org-latex-tables-centered nil
#+OPTIONS: ^:nil # Disable sub/superscripting with bare _; _{...} still works
#+OPTIONS: toc:nil

#+BEGIN_SRC emacs-lisp :noweb no-export :exports results
;; Tables not centered
(setq org-latex-tables-centered nil)
nil
#+END_SRC

#+RESULTS:

#+BEGIN_ABSTRACT
Creating Ethereum accounts is complex and fraught with potential for loss of funds.

A BIP-39 seed recovery phrase helps, but a *single* lapse in security dooms the account.  If someone
finds your recovery phrase, the account is /gone/.

The SLIP-39 standard allows you to split the seed between 1 or more groups of multiple recovery
phrases.  This is better, but creating such accounts is difficult; presently, only the Trezor
supports these, and they can only be created "manually".  Writing down 5 or more sets of 20 words is
difficult and time consuming.

The [[https://github.com/pjkundert/=python-slip39.git][python-slip39]] project exists to assist in the safe creation and documentation of [[https://wolovim.medium.com/ethereum-201-hd-wallets-11d0c93c87][Ethereum HD
Wallet]] accounts, with various SLIP-39 sharing parameters.  It generates the new wallet seed,
generates standard Ethereum account(s) (at [[https://medium.com/myetherwallet/hd-wallets-and-derivation-paths-explained-865a643c7bf2][derivation path]] =m/66'/40'/0'/0/0= by default) with
Ethereum wallet address and QR code, produces the required SLIP-39 phrases, and outputs a single PDF
containing all the required printable cards to document the account.

On an secure (ideally air-gapped) computer, new accounts can safely be generated and the PDF saved
to a USB drive for printing (or directly printed without the file being saved to disk.)
#+END_ABSTRACT
#+TOC: headlines 2

* Security with Availability

  For both BIP-39 and SLIP-39, a 128-bit random "seed" is the source of an unlimited sequence of
  Ethereum HD Wallet accounts.  Anyone who can obtain this seed gains control of all Ethereum
  accounts derived from it, so it must be securely stored.

  Losing this seed means that all of the HD Wallet accounts are permanently lost.  Therefore, it
  must be backed up reliably, and be readily accessible.

  Therefore, we must:

  - Ensure that nobody untrustworthy can recover the seed, but
  - Store the seed in many places with several (some perhaps untrustworthy) people.

  How can we address these conflicting requirements?

** Shamir's Secret Sharing System (SSSS)

   [[https://github.com/satoshilabs/slips/blob/master/slip-0039.md][Satoshi Lab's (Trezor) SLIP-39]] uses SSSS to distribute the ability to recover the key to 1 or
   more "groups".  Collecting the mnemonics from the required number of groups allows recovery of
   the seed.  For BIP-39, the number of groups is always 1, and the number of mnemonics required for
   that group is always 1.

   For SLIP-39, a "group_threshold" of how many groups must bet successfully collected to recover
   the key.  Then key is (conceptually) split between 1 or more groups (not really; each group's
   data alone gives away no information about the key).

   For example, you might have First, Second, Fam and Frens groups, and decide that any 2 groups can
   be combined to recover the key.  Each group has members with varying levels of trust and
   persistence, so have different number of Members, and differing numbers Required to recover that
   group's data:

   #+LATEX: {\scriptsize
   | Group  | Required |   | Members | Description                           |
   |--------+----------+---+---------+---------------------------------------|
   |        |      <r> |   | <l>     |                                       |
   | First  |        1 | / | 1       | Stored at home                        |
   | Second |        1 | / | 1       | Stored in office safe                 |
   | Fam    |        2 | / | 4       | Distributed to family members         |
   | Frens  |        2 | / | 6       | Distributed to friends and associates |
   #+LATEX: }

   The account owner might store their First and Second group data in their home and office safes.
   These are 1/1 groups (1 required, and only 1 member, so each of these are3 1-card groups.)

   If the account needs to be recovered, collecting the First and Second cards from the home and
   office safe is sufficient to recover the seed, and re-generate the HD Wallet accounts.

   Only 2 Fam member's cards must be collected to recover the Fam group's data.  So, if the HD
   Wallet owner loses their home and First group card in a fire, they could get the Second group
   card from the office safe, and 2 cards from Fam group members, and recover the wallet.

   If catastrophe strikes and the owner dies, and the heirs don't have access to either the First
   (at home) or Second (at the office), they can collect 2 Fam cards and 2 Frens cards (at the
   funeral, for example), completing the Fam and Frens groups' data, and recover the HD Wallet
   account.  Since Frens are less likely to persist long term (and are also less likely to know
   each-other), we'll require a lower proportion of them to be collected.

* SLIP-39 Account Generation and Recovery

  Generating a new SLIP-39 encoded Ethereum wallet is easy, with results available as PDF and text.
  The default groups are as described above.  Run the following to obtain a PDF file similar to this
  example; insert a USB drive to collect the output, and run:

  #+LATEX: {\scriptsize
  #+BEGIN_EXAMPLE
  $ python3 -m pip install slip39  # Install slip39 in Python3
  $ cd /Volumes/USBDRIVE/          # Change current directory to USB
  $ python3 -m slip39 Perry        # Or just run "slip39 Perry"
  2021-12-25 11:10:38 slip39           Wrote SLIP-39-encoded wallet for 'Perry' to:\
    Perry-2021-12-22+15.45.36-0xb44A2011A99596671d5952CdC22816089f142FB3.pdf
  #+END_EXAMPLE
  #+LATEX: }

  The resultant PDF will be output into the designated file.

  This PDF file can be printed on 3x5 cards, or on regular paper or card stock and the cards can be cut out.

  To get the data printed on the terminal as in this example (so you could write it down on cards
  instead), add a =-v=.

  Later, if you need to recover the Ethereum wallet, keep entering SLIP-39 mnemonics until the secret
  is recovered (invalid/duplicate mnemonics will be ignored):

  #+LATEX: {\scriptsize
  #+BEGIN_EXAMPLE
  $ python3 -m slip39.recovery   # (or just "slip39-recovery")
  Enter 1st SLIP-39 mnemonic: ab c
  Enter 2nd SLIP-39 mnemonic: veteran guilt acrobat romp burden campus purple webcam uncover ...
  Enter 3rd SLIP-39 mnemonic: veteran guilt acrobat romp burden campus purple webcam uncover ...
  Enter 4th SLIP-39 mnemonic: veteran guilt beard romp dragon island merit burden aluminum worthy ...
  2021-12-25 11:03:33 slip39.recovery  Recovered SLIP-39 secret; Use:  python3 -m slip39 --secret ...
  383597fd63547e7c9525575decd413f7
  #+END_EXAMPLE
  #+LATEX: }

  Finally, regenerate the Ethereum wallet, perhaps including an encrypted JSON wallet file for
  import into a software wallet:

  #+LATEX: {\scriptsize
  #+BEGIN_EXAMPLE
  $ python3 -m slip39 --secret 383597fd63547e7c9525575decd413f7 --json -
  2021-12-25 11:09:57 slip39           ETH(m/44'/60'/0'/0/0): 0xb44A2011A99596671d5952CdC22816089f142FB3
  ...
  JSON key file password: <enter JSON wallet password>
  2021-12-25 11:10:38 slip39           Wrote JSON encrypted wallet for '' to:\
    SLIP39-2021-12-25+11.09.57-0xb44A2011A99596671d5952CdC22816089f142FB3.json
  2021-12-25 11:10:39 slip39           Wrote SLIP39-encoded wallet for '' to:\
    SLIP39-2021-12-25+11.09.57-0xb44A2011A99596671d5952CdC22816089f142FB3.pdf
  #+END_EXAMPLE
  #+LATEX: }

** The =slip39= module API
   
   Provide SLIP-39 Mnemonic set creation from a 128-bit master secret, and recovery of the secret
   from a subset of the provided Mnemonic set.
   
*** =slip39.create=

    Creates a set of SLIP-39 groups and their mnemonics.

    #+LATEX: {\scriptsize
    | Key                | Description                                                                |
    |--------------------+----------------------------------------------------------------------------|
    | name               | Who/what the account is for                                                |
    | group_threshold    | How many groups' data is required to recover the account(s)                |
    | groups             | Each group's description, as {"<group>":(<required>, <members>), ...}      |
    | master_secret      | 128-bit secret (default: from secrets.token_bytes)                         |
    | passphrase         | An optional additional passphrase required to recover secret (default: "") |
    | iteration_exponent | For encrypted secret, exponentially increase PBKDF2 rounds (default: 1)    |
    | paths              | A number of HD Wallet derivation paths (default: ["m/60'/44'/0'/0/0"]      |
    #+LATEX: }

    Outputs a =slip39.Details= namedtuple containing:
    
    #+LATEX: {\scriptsize
    | Key             | Description                                       |
    |-----------------+---------------------------------------------------|
    | name            | (same)                                            |
    | group_threshold | (same)                                            |
    | groups          | Like groups, w/ <members> =  ["<mnemonics>", ...] |
    | accounts        | Resultant { "path": eth_account.Account, ...}     |
    #+LATEX: }

    This is immediately usable to pass to =slip39.output=.

    #+LATEX: {\scriptsize
    #+BEGIN_SRC ipython :session :exports both :results raw drawer
    import codecs
    import random

    import eth_account
    import slip39
    paths = [ "m/60'/44'/0'/0/0", "m/60'/44'/0'/0/1" ]
    master_secret,passphrase = bytes(range(16)),b"password!" # >8/
    create_details = slip39.create(
        "Test", 2, { "Mine": (1,1), "Fam": (2,3) },
        master_secret=master_secret, passphrase=passphrase, paths=paths )
    [
        [
            f"{g_name}({g_of}/{len(g_mnems)}) #{g_n+1}:" if l_n == 0 else ""
        ] + words
        for g_name,(g_of,g_mnems) in create_details.groups.items()
        for g_n,mnem in enumerate( g_mnems )
        for l_n,(line,words) in enumerate(slip39.organize_mnemonic(
                mnem, rows=7, cols=3, label=f"{g_name}({g_of}/{len(g_mnems)}) #{g_n+1}:" ))
    ]
    #+END_SRC

    #+RESULTS:
    :results:
    # Out[16]:
    | 0             | 1          | 2           | 3           |
    |---------------+------------+-------------+-------------|
    | Mine(1/1) #1: | 1 guest    | 8 harvest   | 15 true     |
    |               | 2 discuss  | 9 research  | 16 hamster  |
    |               | 3 acrobat  | 10 preach   | 17 maiden   |
    |               | 4 easy     | 11 scholar  | 18 therapy  |
    |               | 5 budget   | 12 knife    | 19 capacity |
    |               | 6 username | 13 destroy  | 20 sympathy |
    |               | 7 payment  | 14 racism   |             |
    | Fam(2/3) #1:  | 1 guest    | 8 thumb     | 15 demand   |
    |               | 2 discuss  | 9 favorite  | 16 briefing |
    |               | 3 beard    | 10 coding   | 17 drift    |
    |               | 4 echo     | 11 often    | 18 raisin   |
    |               | 5 cowboy   | 12 mild     | 19 husky    |
    |               | 6 emission | 13 regular  | 20 idea     |
    |               | 7 fangs    | 14 avoid    |             |
    | Fam(2/3) #2:  | 1 guest    | 8 distance  | 15 insect   |
    |               | 2 discuss  | 9 easy      | 16 phantom  |
    |               | 3 beard    | 10 sweater  | 17 venture  |
    |               | 4 email    | 11 taught   | 18 modern   |
    |               | 5 change   | 12 agree    | 19 metric   |
    |               | 6 mustang  | 13 crowd    | 20 diet     |
    |               | 7 mental   | 14 coastal  |             |
    | Fam(2/3) #3:  | 1 guest    | 8 express   | 15 spirit   |
    |               | 2 discuss  | 9 response  | 16 satisfy  |
    |               | 3 beard    | 10 sidewalk | 17 glen     |
    |               | 4 entrance | 11 agency   | 18 violence |
    |               | 5 dish     | 12 angel    | 19 often    |
    |               | 6 weapon   | 13 overall  | 20 company  |
    |               | 7 losing   | 14 game     |             |
    :end:

    #+LATEX: }

    Add the resultant HD Wallet addresses:

    #+LATEX: {\scriptsize
    #+BEGIN_SRC ipython :session :exports both :results raw drawer
    [
        [ path, eth.address ]
        for path,eth in create_details.accounts.items()
    ]
    #+END_SRC

    #+RESULTS:
    :results:
    # Out[17]:
    | 0                | 1                                          |
    |------------------+--------------------------------------------|
    | m/60'/44'/0'/0/0 | 0x7A64E13f00a4502b62d45EED15d6254b9Cc10a34 |
    | m/60'/44'/0'/0/1 | 0xF7dc45001A1F3b26565B1364b03C76553FBEE931 |
    :end:

    #+LATEX: }

*** =slip39.output=

    #+LATEX: {\scriptsize
    | Key             | Description                                       |
    |-----------------+---------------------------------------------------|
    | name            | (same)                                            |
    | group_threshold | (same)                                            |
    | groups          | Like groups, w/ <members> =  ["<mnemonics>", ...] |
    | accounts        | Resultant { "path": eth_account.Account, ...}     |
    #+LATEX: }

    Produce a PDF containing all the SLIP-39 details for the account.

    #+LATEX: {\scriptsize
    #+BEGIN_SRC
    slip32.output( *create_details )
    #+END_SRC
    #+LATEX: }

*** =slip39.recover=

    Takes a number of SLIP-39 mnemonics, and if sufficient =group_threshold= groups' mnemonics are
    present (and the options =passphrase= is supplied), the =master_secret= is recovered.  This can
    be used with =slip39.accounts= to directly obtain any =eth_account.Account= data.
    
    #+LATEX: {\scriptsize
    | Key        | Description                                       |
    |------------+---------------------------------------------------|
    | mnemonics  | ["<mnemonics>", ...]                              |
    | passphrase | Optional passphrase to decrypt secret
    #+LATEX: }
    
    #+LATEX: {\scriptsize
    #+BEGIN_SRC ipython :session :exports both :results raw drawer
    recovery = slip39.recover(
        create_details.groups['Mine'][1] + create_details.groups['Fam'][1][:2],
        passphrase=passphrase
    )
    recoveryhex = codecs.encode( recovery, 'hex_codec' ).decode( 'ascii' )

    [[ f"{len(recovery)*8}-bit secret recovered:" ]] + [
     [ f"{recoveryhex[b*32:b*32+32]}" ]
        for b in range( len( recoveryhex ) // 32 )
    ]
    #+END_SRC

    #+RESULTS:
    :results:
    # Out[18]:
    | 0                                |
    |----------------------------------|
    | 128-bit secret recovered:        |
    | 000102030405060708090a0b0c0d0e0f |
    :end:

    #+LATEX: }


* Dependencies

  Internally, python-slip39 project uses Trezor's [[https://gihub.com/trezor/python-shamir-mnemonic.git][python-shamir-mnemonic]] to encode the seed data,
  and the Ethereum project's [[https://github.com/ethereum/eth-account][eth-account]] to convert seeds to Ethereum accounts.

** The =python-shamir-mnemonic= API

   To use it directly, obtain , and install it, or run =python3 -m pip install shamir-mnemonic=.

#+LATEX: {\scriptsize
#+BEGIN_EXAMPLE
$ shamir create custom --group-threshold 2 --group 1 1 --group 1 1 --group 2 5 --group 3 6
Using master secret: 87e39270d1d1976e9ade9cc15a084c62
Group 1 of 4 - 1 of 1 shares required:
merit aluminum acrobat romp capacity leader gray dining thank rhyme escape genre havoc furl breathe class pitch location render beard
Group 2 of 4 - 1 of 1 shares required:
merit aluminum beard romp briefing email member flavor disaster exercise cinema subject perfect facility genius bike include says ugly package
Group 3 of 4 - 2 of 5 shares required:
merit aluminum ceramic roster already cinema knit cultural agency intimate result ivory makeup lobe jerky theory garlic ending symbolic endorse
merit aluminum ceramic scared beam findings expand broken smear cleanup enlarge coding says destroy agency emperor hairy device rhythm reunion
merit aluminum ceramic shadow cover smith idle vintage mixture source dish squeeze stay wireless likely privacy impulse toxic mountain medal
merit aluminum ceramic sister duke relate elite ruler focus leader skin machine mild envelope wrote amazing justice morning vocal injury
merit aluminum ceramic smug buyer taxi amazing marathon treat clinic rainbow destroy unusual keyboard thumb story literary weapon away move
Group 4 of 4 - 3 of 6 shares required:
merit aluminum decision round bishop wrote belong anatomy spew hour index fishing lecture disease cage thank fantasy extra often nail
merit aluminum decision scatter carpet spine ruin location forward priest cage security careful emerald screw adult jerky flame blanket plot
merit aluminum decision shaft arcade infant argue elevator imply obesity oral venture afraid slice raisin born nervous universe usual racism
merit aluminum decision skin already fused tactics skunk work floral very gesture organize puny hunting voice python trial lawsuit machine
merit aluminum decision snake cage premium aide wealthy viral chemical pharmacy smoking inform work cubic ancestor clay genius forward exotic
merit aluminum decision spider boundary lunar staff inside junior tendency sharp editor trouble legal visual tricycle auction grin spit index
#+END_EXAMPLE
#+LATEX: }

** The =eth-account= API

  To creete Ethereum accounts from seed data, two steps are required.

  First, derive a Private Key from the seed data plus a derivation path:

#+LATEX: {\scriptsize
#+BEGIN_EXAMPLE
>>> seed=codecs.decode("dd0e2f02b1f6c92a1a265561bc164135", 'hex_codec')
>>> key=eth_account.hdaccount.key_from_seed(seed, "m/44'/60'/0'/0/0")
>>> keyhex=codecs.encode(key, 'hex_codec')
>>> keyhex
b'178870009416174c9697777b1d94229504e83f25b1605e7bb132aa5b88da64b6'
#+END_EXAMPLE
#+LATEX: }

  Then, use the private key to obtain the Ethereum account data:

#+LATEX: {\scriptsize
#+BEGIN_EXAMPLE
>>> keyhex.decode('ascii')
'178870009416174c9697777b1d94229504e83f25b1605e7bb132aa5b88da64b6'
>>> keyhex = '0x'+keyhex.decode('ascii')
>>> keyhex
'0x178870009416174c9697777b1d94229504e83f25b1605e7bb132aa5b88da64b6'
>>> account = eth_account.Account.from_key(keyhex)
>>> account
<eth_account.signers.local.LocalAccount object at 0x7fba368ae670>
>>> account.address
'0x336cBeAB83aCCdb2541e43D514B62DC6C53675f4'
#+END_EXAMPLE
#+LATEX: }

* Conversion from BIP-39 to SLIP-39

  If we already have a BIP-39 wallet, it would certainly be nice to be able to create nice, safe
  SLIP-39 mnemonics for it, and discard the unsafe BIP-39 mnemonics we have lying around, just
  waiting to be accidentally discovered and the account compromised!

** BIP-39 vs. SLIP-39 Incompatibility

   Unforunately, it is *not possible* to convert a BIP-39 derived wallet into a SLIP-39 wallet.
   Both of these techniques preserve "entropy" (random) bits, but these bits are used *differently*
   -- and incompatibly -- to derive the resultant Ethereum wallets.

*** BIP-39 Entropy to Mnemonic

    BIP-39 uses a single set of 12, 15, 18, 21 or 24 BIP-39 words to carefully preserve a specific
    128 to 256 bits of initial entropy.

    | "adult cattle ... remind" | 0x9CE8...44                   | Normalized string Extended       |

    #+LATEX: {\scriptsize
    #+BEGIN_SRC ipython :session :exports both :results raw drawer
    from eth_account.hdaccount.mnemonic import Mnemonic
    # bip39 = eth_account.hdaccount.generate_mnemonic( 12, "english" )
    bip39_english = Mnemonic("english")
    entropy = b'\xFF' * 16
    entropy_mnemonic = bip39_english.to_mnemonic( entropy )
    [[entropy_mnemonic]]
    #+END_SRC

    #+RESULTS:
    :results:
    # Out[19]:
    | 0                                                 |
    |---------------------------------------------------|
    | zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong |
    :end:

    #+LATEX: }

    Each word is one of a corpus of 2048 words; therefore, each word encodes 11 bits (2048 == 2**11)
    of entropy.  So, we provided 128 bits, but 12*11 == 132.  So where does the extra 4 bits of data
    come from?  

    It comes from the first few bits of a SHA256 hash of the entropy, which is added to the end of
    the supplied 128 bits, to reach the required 132 bits: 132 / 11 == 12 words.

    This last 4 bits (up to 8 bits, for a 256-bit 24-word BIP-39) is checked, when validating the
    BIP-39 mnemonic.  Therefore, making up a random BIP-39 mnemonic will succeed only 1 / 16 times on
    average, due to an incorrect checksum 4-bit (16 == 2**4) .  Lets check:

    #+LATEX: {\scriptsize
    #+BEGIN_SRC ipython :session :exports both :results raw drawer
    def random_words( n, count=100 ):
        for _ in range( count ):
            yield ' '.join( random.choice( bip39_english.wordlist ) for _ in range( n ))

    successes = sum(
        bip39_english.is_mnemonic_valid( m )
        for i,m in enumerate( random_words( 12, 10000 ))) / 100
    [[ f"Valid random 12-word mnemonics:" ]] + [
     [ f"{successes}%" ]] + [
     [ f"~ 1/{100/successes:.3}" ]]

    #+END_SRC

    #+RESULTS:
    :results:
    # Out[20]:
    | 0                               |
    |---------------------------------|
    | Valid random 12-word mnemonics: |
    | 6.15%                           |
    | ~ 1/16.3                        |
    :end:

    #+LATEX: }

    Sure enough, about 1/16 random 12-word phrases are valid BIP-39 mnemonics.  OK, we've got the
    contents of the BIP-39 phrase dialed in.  How is it used to generate accounts?

*** BIP-39 Mnemonic to Seed

    Unfortunately, we do *not* use the carefully preserved 128-bit entropy to generate the wallet!
    Nope, it is stretched to a 512-bit seed using PBKDF2 HMAC SAH512.  The normalized *text* of the
    12-word mnemonic is then used (with a salt of "mnemonic" plus an optional passphrase, "" by
    default), to obtain the seed:

    #+LATEX: {\scriptsize
    #+BEGIN_SRC ipython :session :exports both :results raw drawer
    seed = bip39_english.to_seed( entropy_mnemonic )
    seedhex = codecs.encode( seed, 'hex_codec' ).decode( 'ascii' )
    [[ f"{len(seed)*8}-bit seed:" ]] + [
     [ f"{seedhex[b*32:b*32+32]}" ]
     for b in range( len( seedhex ) // 32 )
    ]
    #+END_SRC

    #+RESULTS:
    :results:
    # Out[21]:
    | 0                                |
    |----------------------------------|
    | 512-bit seed:                    |
    | b6a6d8921942dd9806607ebc2750416b |
    | 289adea669198769f2e15ed926c3aa92 |
    | bf88ece232317b4ea463e84b0fcd3b53 |
    | 577812ee449ccc448eb45e6f544e25b6 |
    :end:

    #+LATEX: }

    Then, this 512-bit seed is used to derive HD wallets.  The HD Wallet key derivation process
    consumes whatever seed entropy is provided (512 bits in this case), and uses HMAC SAH512 with a
    prefix of b"Bitcoin seed" to stretch the supplied seed entropy to 64 bytes (512 bits).  Then,
    the HD Wallet path segments are iterated through, permuting the first 32 bytes of this material
    as the key with the second 32 bytes of material as the chain node, until finally the 32-byte
    (256-bit) Ethereum account private key is produced.  We then use this private key to compute the
    rest of the Ethereum account details, such as its public address.

    #+LATEX: {\scriptsize
    #+BEGIN_SRC ipython :session :exports both :results raw drawer
    path = "m/66'/40'/0'/0/0"
    key = eth_account.hdaccount.key_from_seed( seed, path )
    keyhex = '0x' + codecs.encode( key, 'hex_codec' ).decode( 'ascii' )
    eth = eth_account.Account.from_key( keyhex )
    [[ f"{len(key)*8}-bit derived key at path {path!r}:" ]] + [
     [ f"{keyhex}" ]] + [
     [ "... yields ..." ]] + [
     [ f"Ethereum address: {eth.address}" ]
    ]

    #+END_SRC

    #+RESULTS:
    :results:
    # Out[22]:
    | 0                                                                  |
    |--------------------------------------------------------------------|
    | 256-bit derived key at path "m/66'/40'/0'/0/0":                    |
    | 0x9d291e4a972d86ee5a8381a7fc4cd99913a0ec3fd141995e33812c32da40c2fb |
    | ... yields ...                                                     |
    | Ethereum address: 0xeFCf257B01833cF14229Ed9be0a49b3E026409e9       |
    :end:

    #+LATEX: }

*** SLIP-39 Entropy to Mnemonic

    #+LATEX: {\scriptsize
    #+BEGIN_SRC ipython :session :exports both :results raw drawer
    # We can turn off/on randomness during SLIP-39 generation to get deterministic phrases:
    #import shamir_mnemonic
    #shamir_mnemonic.shamir.RANDOM_BYTES = lambda n: b'\00' * n
    #import secrets
    #shamir_mnemonic.shamir.RANDOM_BYTES = secrets.token_bytes
    name,threshold,entropy_slip39,accts = slip39.create(
        "Test", 2, { "Mine": (1,1), "Fam": (2,3) }, entropy, paths=[path] )
    [[ f"{g_name}({g_of}/{len(g_mnems)}) #{g_n+1}:" if l_n == 0 else "" ] + words
     for g_name,(g_of,g_mnems) in entropy_slip39.items()
     for g_n,mnem in enumerate( g_mnems )
     for l_n,(line,words) in enumerate(slip39.organize_mnemonic(
             mnem, rows=7, cols=3, label=f"{g_name}({g_of}/{len(g_mnems)}) #{g_n+1}:" ))
    ]
    #+END_SRC

    #+RESULTS:
    :results:
    # Out[23]:
    | 0             | 1          | 2           | 3           |
    |---------------+------------+-------------+-------------|
    | Mine(1/1) #1: | 1 pancake  | 8 detailed  | 15 award    |
    |               | 2 echo     | 9 item      | 16 forecast |
    |               | 3 acrobat  | 10 package  | 17 realize  |
    |               | 4 easy     | 11 editor   | 18 bucket   |
    |               | 5 debris   | 12 fumes    | 19 crisis   |
    |               | 6 process  | 13 writing  | 20 cover    |
    |               | 7 watch    | 14 lawsuit  |             |
    | Fam(2/3) #1:  | 1 pancake  | 8 warmth    | 15 aspect   |
    |               | 2 echo     | 9 estate    | 16 phantom  |
    |               | 3 beard    | 10 inside   | 17 thorn    |
    |               | 4 echo     | 11 champion | 18 album    |
    |               | 5 answer   | 12 grant    | 19 stilt    |
    |               | 6 drink    | 13 lunar    | 20 glimpse  |
    |               | 7 fragment | 14 pink     |             |
    | Fam(2/3) #2:  | 1 pancake  | 8 pecan     | 15 identify |
    |               | 2 echo     | 9 rich      | 16 jacket   |
    |               | 3 beard    | 10 scared   | 17 sugar    |
    |               | 4 email    | 11 elegant  | 18 mixed    |
    |               | 5 blanket  | 12 fatigue  | 19 frequent |
    |               | 6 ajar     | 13 smirk    | 20 spider   |
    |               | 7 step     | 14 mule     |             |
    | Fam(2/3) #3:  | 1 pancake  | 8 fact      | 15 victim   |
    |               | 2 echo     | 9 client    | 16 rhythm   |
    |               | 3 beard    | 10 analysis | 17 carpet   |
    |               | 4 entrance | 11 tension  | 18 aircraft |
    |               | 5 diploma  | 12 render   | 19 darkness |
    |               | 6 excuse   | 13 both     | 20 length   |
    |               | 7 dismiss  | 14 spend    |             |
    :end:

    #+LATEX: }

    Since there is some randomess in the SLIP-39 mnemonics generation process, we'll get a different
    set of words each time for the fixed "entropy" =0xFFFF..FF= used in this example, but we'll
    *always* derive the same Ethereum account =0x3224..56c5= at the specified HD Wallet derivation
    path.

    #+LATEX: {\scriptsize
    #+BEGIN_SRC ipython :session :exports both :results raw drawer

    [[ "HD Wallet Path:", "Ethereum Address:" ]] + [
     [ path, eth.address ]
     for path,eth in accts.items()
    ]
    #+END_SRC

    #+RESULTS:
    :results:
    # Out[24]:
    | 0                | 1                                          |
    |------------------+--------------------------------------------|
    | HD Wallet Path:  | Ethereum Address:                          |
    | m/66'/40'/0'/0/0 | 0x322408FCF0dAB471570038DEA08536780aAB56c5 |
    :end:

    #+LATEX: }

*** SLIP-39 Mnemonic to Seed

    Lets prove that we can actually recover the *original* entropy from the SLIP-39 recovery
    mnemonics; in this case, we've specified a SLIP-39 group_threshold of 2 groups, so we'll use all
    1 mnemonic from Mine, and 2 from Fam:

    #+LATEX: {\scriptsize
    #+BEGIN_SRC ipython :session :exports both :results raw drawer
    _,mnem_mine = entropy_slip39['Mine']
    _,mnem_fam =  entropy_slip39['Fam']
    recseed = slip39.recover( mnem_mine + mnem_fam[:2] )
    recseedhex = codecs.encode( recseed, 'hex_codec' ).decode( 'ascii' )

    [[ f"{len(recseed)*8}-bit seed:" ]] + [
     [ f"{recseedhex[b*32:b*32+32]}" ]
        for b in range( len( recseedhex ) // 32 )
    ]
    #+END_SRC

    #+RESULTS:
    :results:
    # Out[25]:
    | 0                                |
    |----------------------------------|
    | 128-bit seed:                    |
    | ffffffffffffffffffffffffffffffff |
    :end:

    #+LATEX: }

    And we'll use the same style of code as for the BIP-39 example above, to derive the Ethereum
    address from this 128-bit seed:

    #+LATEX: {\scriptsize
    #+BEGIN_SRC ipython :session :exports both :results raw drawer
    reckey = eth_account.hdaccount.key_from_seed( recseed, path )
    reckeyhex = '0x' + codecs.encode( reckey, 'hex_codec' ).decode( 'ascii' )
    receth = eth_account.Account.from_key( reckeyhex )
    [[ f"{len(reckey)*8}-bit derived key at path {path!r}:" ]] + [
     [ f"{reckeyhex}" ]] + [
     [ "... yields ..." ]] + [
     [ f"Ethereum address: {receth.address}" ]
    ]
    #+END_SRC

    #+RESULTS:
    :results:
    # Out[26]:
    | 0                                                                  |
    |--------------------------------------------------------------------|
    | 256-bit derived key at path "m/66'/40'/0'/0/0":                    |
    | 0x738fc6d8dd28f75027ec04b1c64cead968bbae0d9b15de2dab664e5b59db04f3 |
    | ... yields ...                                                     |
    | Ethereum address: 0x322408FCF0dAB471570038DEA08536780aAB56c5       |
    :end:

    #+LATEX: }

    And we see that we obtain the same Ethereum address =0x3224..56c5= as we originally got from
    =slip39.create= above.

** BIP-39 vs SLIP-39 Key Derivation Summary

   At no time in BIP-39 account derivation is the original 128-bit mnemonic entropy used directly in
   the derivation of the wallet key.  This differs from SLIP-39, which directly uses the 128-bit mnemonic
   entropy recovered from the SLIP-39 Shamir's Secret Sharing System recovery process to generate
   each HD Wallet account's private key.

   Furthermore, there is no point in the BIP-39 entropy to account generation where we *could*
   introduce a known 128-bit seed and produce a known Ethereum wallet from it, other than as the
   very beginning.

   Therefore, SLIP-39 and BIP-39 HD Wallet generation are fundamentally distinct; we cannot produce
   BIP-39 and SLIP-39 mnemonics that result in the same wallet.  To convert your funds from a BIP-39
   wallet to a SLIP-39 wallet will require *moving* the funds.
