tests: wizard: add test "adding unrelated seed to xpub-only ks raises"
What changed, and why it matters
This commit adds a safety check and a test for Electrum wallet setup. It ensures that if a user tries to add a seed phrase that does not match an existing watch-only wallet's public key (xpub), the wallet raises a clear error instead of silently replacing the keystore. This prevents accidental or malicious mismatches between a seed and the wallet's expected public key.
No immediate action required. This is a hardening/test addition. Review related wizard code paths to ensure all keystore replacement flows enforce xpub/seed consistency, and consider whether a more specific exception type should be used instead of generic Exception.
Security signals we found
Defense-in-depth: replaces debug-only assert with runtime exception
Prevents keystore replacement with mismatched seed/xpub
Adds regression test for mismatching seed vs xpub
Potential user-facing safety improvement for wallet recovery flows
Evidence from the diff
The patch changes Standard_Wallet._update_keystore() in electrum/wallet.py from an assert to an explicit Exception when the new keystore’s master public key (xpub) does not match the existing keystore’s xpub. It also adds a unit test in tests/test_wizard.py verifying that adding an unrelated Electrum seed to an xpub-only (watching-only) keystore raises ‘mismatching xpubs’. The previous assert would only fire in debug mode; the new behavior is active in production builds.
Changed components
electrum/wallet.py: Standard_Wallet._update_keystore()tests/test_wizard.py: KeystoreWizardTestCaseInspect captured patch +21 / −2
diff --git a/electrum/wallet.py b/electrum/wallet.py
index 9a03dc6..b931e4a 100644
--- a/electrum/wallet.py
+++ b/electrum/wallet.py
@@ -4106,7 +4106,8 @@ class Standard_Wallet(Simple_Wallet, Deterministic_Wallet):
self.keystore.add_slip_19_ownership_proofs_to_tx(tx=tx, password=None)
def _update_keystore(self, keystore):
- assert self.keystore.get_master_public_key() == keystore.get_master_public_key()
+ if self.keystore.get_master_public_key() != keystore.get_master_public_key():
+ raise Exception("mismatching xpubs")
self.keystore = keystore
self.save_keystore()
diff --git a/tests/test_wizard.py b/tests/test_wizard.py
index 8eb12fa..dbe54f7 100644
--- a/tests/test_wizard.py
+++ b/tests/test_wizard.py
@@ -127,7 +127,6 @@ class ServerConnectWizardTestCase(WizardTestCase):
class KeystoreWizardTestCase(WizardTestCase):
# TODO add test cases for:
# - multisig
- # - mismatching xpub vs seed errors
class TKeystoreWizard(KeystoreWizard):
def is_single_password(self):
@@ -243,6 +242,25 @@ class KeystoreWizardTestCase(WizardTestCase):
wallet.disable_keystore(wallet.get_keystore())
self._sanity_checks_after_disabling_keystore(ks=wallet.get_keystore(), xpub=myxpub, key_origin_info=my_keyorigininfo)
+ async def test_haveseed_electrum__mismatching_seed(self):
+ """adding an unrelated seed to an xpub-only keystore should raise"""
+ w, v = self._wizard_for()
+ d = v.wizard_data
+ d.update({
+ 'seed': 'abandon bike', 'seed_type': 'segwit', 'seed_extend': False, 'seed_variant': 'electrum',
+ })
+ self.assertTrue(w.is_last_view(v.view, d))
+ w.resolve_next(v.view, d)
+ ks, ishww = w._result
+ self.assertFalse(ishww)
+
+ wallet = self._create_xpub_keystore_wallet(xpub='zpub6nAZodjgiMNf9zzX1pTqd6ZVX61ax8azhUDnWRumKVUr1VYATVoqAuqv3qKsb8WJXjxei4wei2p4vnMG9RnpKnen2kmgdhvZUmug2NnHNsr')
+ self.assertTrue(wallet.get_keystore().is_watching_only())
+ self.assertTrue(wallet.can_enable_disable_keystore(ks))
+ with self.assertRaises(Exception) as ctx:
+ wallet.enable_keystore(ks, ishww, None)
+ self.assertTrue("mismatching xpubs" in ctx.exception.args[0])
+
async def test_haveseed_electrum_oldseed(self):
w, v = self._wizard_for()
d = v.wizard_data
Why this scored 42/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.