Merge pull request #11017 from f321x/fix_10998
What changed, and why it matters
This commit fixes a bug where Electrum would save private keys for unsupported Bitcoin address types (such as multi-signature 'p2wsh' keys) to the wallet file when a user tried to import them. The fix makes the wallet reject and not store these unsupported keys. Before the fix, an imported unsupported key could silently persist in the wallet even though Electrum could not actually use it, which could mislead users and potentially cause loss of funds or confusion.
Users who imported private keys in older versions should verify their wallet does not contain unexpected keypairs for unsupported script types. Upgrade to the patched version. Developers should review whether any existing wallets may have persisted p2wsh/p2wsh-p2sh keys from prior imports.
Security signals we found
Input validation moved to prevent persistence of unsupported key material
New test explicitly verifies unsupported private keys are not persisted to wallet storage
Exception handling changed to surface unsupported key types as user-facing errors
Fixes referenced issue #10998
Evidence from the diff
The patch moves validation earlier in the private key import flow. Previously, import_privkey in keystore.py deserialized and stored any private key, and the unsupported-type check happened afterward in import_private_keys. This allowed unsupported script types (e.g., p2wsh, p2wsh-p2sh) to be persisted in keystore.keypairs. The fix raises NotImplementedError inside import_privkey for any txin_type outside PUBKEYHASH_SCRIPT_TYPES (‘p2pkh’, ‘p2wpkh’, ‘p2wpkh-p2sh’), so unsupported keys are rejected before storage. The wizard and tests are updated accordingly. A constant PUBKEYHASH_SCRIPT_TYPES is introduced in bitcoin.py and used in verify_usermessage_with_address and wallet.py.
Changed components
electrum/keystore.pyelectrum/wizard.pyelectrum/bitcoin.pyelectrum/wallet.pyInspect captured patch +40 / −13
### electrum/bitcoin.py
@@ -630,6 +630,7 @@ def DecodeBase58Check(psz: Union[bytes, str]) -> bytes:
'p2wsh-p2sh': 7
}
WIF_SCRIPT_TYPES_INV = inv_dict(WIF_SCRIPT_TYPES)
+PUBKEYHASH_SCRIPT_TYPES = ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh')
def is_segwit_script_type(txin_type: str) -> bool:
@@ -901,7 +902,7 @@ def verify_usermessage_with_address(address: str, sig65: bytes, message: bytes,
return False
# check public key using the address
pubkey_hex = public_key.get_public_key_hex(compressed)
- txin_types = (txin_type_guess,) if txin_type_guess else ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh')
+ txin_types = (txin_type_guess,) if txin_type_guess else PUBKEYHASH_SCRIPT_TYPES
for txin_type in txin_types:
addr = pubkey_to_address(txin_type, pubkey_hex, net=net)
if address == addr:
### electrum/keystore.py
@@ -290,6 +290,8 @@ def check_password(self, password):
def import_privkey(self, sec: str, password) -> Tuple[str, str]:
txin_type, privkey, compressed = deserialize_privkey(sec)
+ if txin_type not in bitcoin.PUBKEYHASH_SCRIPT_TYPES:
+ raise NotImplementedError(txin_type)
pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
# re-serialize the key so the internal storage format is consistent
serialized_privkey = serialize_privkey(
@@ -307,13 +309,12 @@ def import_private_keys(self, keys: Sequence[str], password: Optional[str]):
for key in keys:
try:
txin_type, pubkey = self.import_privkey(key, password)
+ except NotImplementedError as e:
+ bad_keys.append((key, 'not implemented type' + f': {e}'))
except Exception as e:
bad_keys.append((key, 'invalid private key' + f': {e}'))
- continue
- if txin_type not in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
- bad_keys.append((key, 'not implemented type' + f': {txin_type}'))
- continue
- good_inputs.append((txin_type, pubkey))
+ else:
+ good_inputs.append((txin_type, pubkey))
return good_inputs, bad_keys
def delete_imported_key(self, key: str) -> None:
### electrum/wallet.py
@@ -3288,7 +3288,7 @@ def sign_message(self, *, address: str, message: str, password, strip_inputs: bo
raise UserFacingException(_("Address not in wallet."))
txin_type = self.get_txin_type(address)
assert txin_type != "address" # logic error, as this implies watching-only
- if txin_type not in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']:
+ if txin_type not in bitcoin.PUBKEYHASH_SCRIPT_TYPES:
raise UserFacingException(
_("Cannot sign messages with this type of address:") +
" " + txin_type + "\n\n"
### electrum/wizard.py
@@ -16,7 +16,7 @@
from electrum.util import UserFacingException
from electrum.wallet_db import WalletDB
from electrum.bip32 import normalize_bip32_derivation, xpub_type
-from electrum import descriptor, keystore, mnemonic, bitcoin
+from electrum import keystore, mnemonic, bitcoin
from electrum.mnemonic import is_any_2fa_seed_type, can_seed_have_passphrase
from electrum.util import multisig_type
@@ -731,13 +731,13 @@ def create_storage(self, path: str, data: dict):
keys = keystore.get_private_keys(data['private_key_list'])
for pk in keys:
assert bitcoin.is_private_key(pk)
- txin_type, pubkey = k.import_privkey(pk, None)
try:
- addr = bitcoin.pubkey_to_address(txin_type, pubkey)
- except descriptor.NotLegacySinglesigScriptType as e:
+ txin_type, pubkey = k.import_privkey(pk, None)
+ except NotImplementedError as e:
raise UserFacingException(
- _("Importing individual private keys of type '{}' is not supported.").format(txin_type),
+ _("Importing individual private keys of type '{}' is not supported.").format(e),
) from e
+ addr = bitcoin.pubkey_to_address(txin_type, pubkey)
addresses[addr] = {'type': txin_type, 'pubkey': pubkey}
elif 'address_list' in data:
for addr in data['address_list'].split():
### tests/test_wallet.py
@@ -128,13 +128,23 @@ async def test_storage_imported_add_privkeys_persistence_test(self):
wallet.import_private_keys(['p2wpkh:KzuqaaLp9zYjVuj8vQtCwFdiZFreW3NJNBachgVS8S9XMgj5y78b'], password=None)
self.assertEqual(3, len(wallet.get_receiving_addresses()))
self.assertEqual(3, len(wallet.keystore.keypairs))
+
+ # keys with unsupported script types get rejected, and must not be persisted (see #10998)
+ keypairs_before = dict(wallet.keystore.keypairs)
+ good_addr, bad_keys = wallet.import_private_keys([
+ 'p2wsh:L1cgMEnShp73r9iCukoPE3MogLeueNYRD9JVsfT1zVHyPBR3KqBY',
+ 'p2wsh-p2sh:KzuqaaLp9zYjVuj8vQtCwFdiZFreW3NJNBachgVS8S9XMgj5y78b',
+ ], password=None)
+ self.assertEqual([], good_addr)
+ self.assertEqual(2, len(bad_keys))
+ self.assertEqual(keypairs_before, wallet.keystore.keypairs)
await wallet.stop()
# open the wallet anew again, and verify if the privkey was stored
del wallet
wallet = Daemon._load_wallet(self.wallet_path, password=None, config=self.config)
self.assertEqual(3, len(wallet.get_receiving_addresses()))
- self.assertEqual(3, len(wallet.keystore.keypairs))
+ self.assertEqual(keypairs_before, wallet.keystore.keypairs)
self.assertTrue('03bf450797034dc95693096e575e3b3db14e5f074679b349b727f90fc7804ce7ab' in wallet.keystore.keypairs)
self.assertTrue('030dac677b9484e23db6f9255eddf433f4f12c02f9b35e0100f2f103ffbccf540f' in wallet.keystore.keypairs)
self.assertTrue('02f11d5f222a728fd08226cb5a1e85a74d58fc257bd3764bf1234346f91defed72' in wallet.keystore.keypairs)
### tests/test_wizard.py
@@ -1229,3 +1229,18 @@ async def test_create_imported_wallet_from_wif_keys(self):
{"bc1qq2tmmcngng78nllq2pvrkchcdukemtj56uyue0", "1LNvv5h6QHoYv1nJcqrp13T2TBkD2sUGn1", "1FJEEB8ihPMbzs2SkLmr37dHyRFzakqUmo"},
)
self.assertFalse(wallet.can_enable_disable_keystore(wallet.keystore))
+
+ async def test_create_imported_wallet_from_wif_keys__unsupported_type(self):
+ w = self._wizard_for(wallet_type='imported')
+ v = w._current
+ d = v.wizard_data
+ self.assertEqual('imported', v.view)
+
+ d.update({
+ 'private_key_list':
+ 'p2wpkh:L1cgMEnShp73r9iCukoPE3MogLeueNYRD9JVsfT1zVHyPBR3KqBY\n'
+ 'p2wsh:KyQ2voUQj71P6E9KyDFqQoYMMm3yKKAPMKbfqZccib6xWxbWHCex\n'})
+ v = w.resolve_next(v.view, d)
+ with self.assertRaises(util.UserFacingException) as ctx:
+ self._set_password_and_check_address(v=v, w=w, recv_addr=None)
+ self.assertEqual("Importing individual private keys of type 'p2wsh' is not supported.", str(ctx.exception))Why this scored 59/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.