Skip to content

Commit

Permalink
[electrum] Implement SLIP-0039 wallet recovery.
Browse files Browse the repository at this point in the history
Summary:
This implements recovery of a wallet from a Shamir's secret-sharing scheme.

Current limitations:
 - we can only recover a wallet from an existing set of mnemonic shares, we don't generate new ones
 - the mnemonic shares are not saved to storage (wallet file), so users can't see their "seed" via the "Wallet > Show" seed menu (the action is disabled in the GUI)
 - we don't autodetect the seed type, the user needs to click the option button and select "slip39" explicitely

This is a backport of [[spesmilo/electrum#6917 | electrum#6917]] and [[spesmilo/electrum#9059 | electrum#9059]]

Depends on D17576

Test Plan:
`python test_runner.py`

Import a wallet from a SLIP39 share, test that it works (receive a send a transaction), close it and reopen it and test again (make sure the wallet file has all the info needed to open the wallet). Test the derivation path scanner.

Reviewers: #bitcoin_abc, Fabien

Reviewed By: #bitcoin_abc, Fabien

Differential Revision: https://reviews.bitcoinabc.org/D17577
  • Loading branch information
andrewkozlik authored and PiRK committed Jan 23, 2025
1 parent 9fe5282 commit fb94895
Show file tree
Hide file tree
Showing 11 changed files with 2,459 additions and 56 deletions.
1 change: 1 addition & 0 deletions electrum/contrib/build-wine/deterministic.spec
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ datas = [
(home+'electrumabc/servers_testnet.json', 'electrumabc'),
(home+'electrumabc/servers_regtest.json', 'electrumabc'),
(home+'electrumabc/wordlist/english.txt', 'electrumabc/wordlist'),
(home+'electrumabc/wordlist/slip39.txt', 'electrumabc/wordlist'),
(home+'electrumabc/locale', 'electrumabc/locale'),
(home+'electrumabc_gui/qt/data/ecsupplemental_win.ttf', 'electrumabc_gui/qt/data'),
(home+'electrumabc_plugins', 'electrumabc_plugins'),
Expand Down
1 change: 1 addition & 0 deletions electrum/contrib/osx/osx.spec
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ datas = [
(home+'electrumabc/servers_testnet.json', PYPKG),
(home+'electrumabc/servers_regtest.json', PYPKG),
(home+'electrumabc/wordlist/english.txt', PYPKG + '/wordlist'),
(home+'electrumabc/wordlist/slip39.txt', PYPKG + '/wordlist'),
(home+'electrumabc/locale', PYPKG + '/locale'),
(home+'electrumabc_plugins', PYPKG + '_plugins'),
]
Expand Down
33 changes: 24 additions & 9 deletions electrum/electrumabc/base_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

from electrumabc_plugins.hw_wallet import HWPluginBase

from . import bitcoin, keystore, mnemo, util
from . import bitcoin, keystore, mnemo, slip39, util
from .address import Address
from .bip32 import is_bip32_derivation, xpub_type
from .constants import CURRENCY, PROJECT_NAME, REPOSITORY_URL
Expand Down Expand Up @@ -558,20 +558,27 @@ def passphrase_dialog(self, run_next):

def restore_from_seed(self):
self.opt_bip39 = True
self.opt_slip39 = True
self.opt_ext = True
# TODO FIX #bitcoin.is_seed if self.wallet_type == 'standard' else bitcoin.is_new_seed
test = mnemo.is_seed
self.restore_seed_dialog(run_next=self.on_restore_seed, test=test)

def on_restore_seed(self, seed, is_bip39, is_ext):
# NB: seed_type_name here may also auto-detect 'bip39'
self.seed_type = "bip39" if is_bip39 else mnemo.seed_type_name(seed)
def on_restore_seed(self, seed: str | slip39.EncryptedSeed, seed_type, is_ext):
self.seed_type = seed_type
if self.seed_type == "bip39":

def f(passphrase):
self.on_restore_bip39(seed, passphrase)

self.passphrase_dialog(run_next=f) if is_ext else f("")
elif self.seed_type == "slip39":

def f(passphrase):
self.on_restore_slip39(seed, passphrase)

self.passphrase_dialog(run_next=f) if is_ext else f("")

elif self.seed_type in ["standard", "electrum"]:

def f(passphrase):
Expand All @@ -583,10 +590,18 @@ def f(passphrase):
else:
raise RuntimeError("Unknown seed type", self.seed_type)

def on_restore_bip39(self, seed, passphrase):
def on_restore_bip39(self, seed: str, passphrase):
bip32_seed = mnemo.bip39_mnemonic_to_seed(seed, passphrase or "")
self.derivation_dialog(
lambda x: self.run("on_bip44", seed, passphrase, str(x)),
lambda x: self.run("on_bip44", seed, passphrase, "bip39", str(x)),
keystore.bip44_derivation_xec(0),
bip32_seed=bip32_seed,
)

def on_restore_slip39(self, seed: slip39.EncryptedSeed, passphrase: str):
bip32_seed = seed.decrypt(passphrase)
self.derivation_dialog(
lambda x: self.run("on_bip44", seed, passphrase, "slip39", str(x)),
keystore.bip44_derivation_xec(0),
bip32_seed=bip32_seed,
)
Expand All @@ -597,10 +612,9 @@ def create_keystore(self, seed, passphrase):
k = keystore.from_seed(seed, passphrase)
self.on_keystore(k)

def on_bip44(self, seed, passphrase, derivation):
# BIP39
def on_bip44(self, seed, passphrase, seed_type, derivation):
k = keystore.from_seed(
seed, passphrase, derivation=derivation, seed_type="bip39"
seed, passphrase, seed_type=seed_type, derivation=derivation
)
self.on_keystore(k)

Expand Down Expand Up @@ -756,6 +770,7 @@ def create_seed(self, seed_type):
# This should never happen.
raise ValueError("Cannot make seed for unknown seed type " + str(seed_type))
self.opt_bip39 = False
self.opt_slip39 = False
self.show_seed_dialog(
run_next=lambda x: self.request_passphrase(seed, x), seed_text=seed
)
Expand Down
14 changes: 12 additions & 2 deletions electrum/electrumabc/keystore.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

from mnemonic import Mnemonic

from . import bitcoin, mnemo, networks
from . import bitcoin, mnemo, networks, slip39
from .address import Address, PublicKey
from .bip32 import (
CKD_pub,
Expand Down Expand Up @@ -840,7 +840,9 @@ def from_bip32_seed_and_derivation(bip32_seed: bytes, derivation: str) -> BIP32K
return keystore


def from_seed(seed, passphrase, *, seed_type="", derivation=None) -> KeyStore:
def from_seed(
seed: str | slip39.EncryptedSeed, passphrase, *, seed_type="", derivation=None
) -> KeyStore:
if not seed_type:
seed_type = mnemo.seed_type_name(seed) # auto-detect
if seed_type == "old":
Expand All @@ -858,6 +860,14 @@ def from_seed(seed, passphrase, *, seed_type="", derivation=None) -> KeyStore:
keystore = from_bip32_seed_and_derivation(bip32_seed, derivation)
keystore.add_seed(seed, seed_type=seed_type)
keystore.passphrase = passphrase
elif seed_type == "slip39":
derivation = derivation or bip44_derivation_xec(0)
bip32_seed = seed.decrypt(passphrase)
keystore = from_bip32_seed_and_derivation(bip32_seed, derivation)
keystore.seed_type = "slip39"
# We don't save the "seed" (the shares) and passphrase to disk for now.
# Users won't be able to display the "seed" (the shares used to restore the
# wallet).
else:
raise InvalidSeed()
return keystore
Expand Down
Loading

0 comments on commit fb94895

Please sign in to comment.