Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions electrum/bitcoin.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ def script_to_address(script: bytes, *, net=None) -> Optional[str]:
def address_to_script(addr: str, *, net=None) -> bytes:
if net is None: net = constants.net
if not is_address(addr, net=net):
raise BitcoinException(f"invalid bitcoin address: {addr}")
raise BitcoinException(f"invalid bitcoin address: {neuter_bitcoin_address(addr)}")
witver, witprog = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, addr)
if witprog is not None:
if not (0 <= witver <= 16):
Expand All @@ -455,6 +455,17 @@ def address_to_script(addr: str, *, net=None) -> bytes:
return script


def neuter_bitcoin_address(addr: str) -> str:
"""Truncate a bitcoin address, for display in errors that might get sent to the crash reporter,
to reduce harm to the user's privacy.
"""
assert isinstance(addr, str), type(addr)
if len(addr) <= 7:
return addr
neutered_addr = addr[:5] + '..' + addr[-2:]
return f"{neutered_addr!r} (len={len(addr)})"


class OnchainOutputType(Enum):
"""Opaque types of scriptPubKeys.
In case of p2sh, p2wsh and similar, no knowledge of redeem script, etc.
Expand All @@ -470,7 +481,7 @@ def address_to_payload(addr: str, *, net=None) -> Tuple[OnchainOutputType, bytes
"""Return (type, pubkey hash / witness program) for an address."""
if net is None: net = constants.net
if not is_address(addr, net=net):
raise BitcoinException(f"invalid bitcoin address: {addr}")
raise BitcoinException(f"invalid bitcoin address: {neuter_bitcoin_address(addr)}")
witver, witprog = segwit_addr.decode_segwit_address(net.SEGWIT_HRP, addr)
if witprog is not None:
if witver == 0:
Expand Down
6 changes: 3 additions & 3 deletions electrum/synchronizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from . import util
from .transaction import Transaction, PartialTransaction
from .util import make_aiohttp_session, NetworkJobOnDefaultServer, random_shuffled_copy, OldTaskGroup
from .bitcoin import address_to_scripthash, is_address
from .bitcoin import address_to_scripthash, is_address, neuter_bitcoin_address
from .logging import Logger
from .interface import GracefulDisconnect, NetworkTimeout

Expand Down Expand Up @@ -84,12 +84,12 @@ async def _run_tasks(self, *, taskgroup):
self.session.unsubscribe(self.status_queue)

def add(self, addr: str) -> None:
if not is_address(addr): raise ValueError(f"invalid bitcoin address {addr}")
if not is_address(addr): raise ValueError(f"invalid bitcoin address {neuter_bitcoin_address(addr)}")
self._adding_addrs.add(addr) # this lets is_up_to_date already know about addr

async def _add_address(self, addr: str):
try:
if not is_address(addr): raise ValueError(f"invalid bitcoin address {addr}")
if not is_address(addr): raise ValueError(f"invalid bitcoin address {neuter_bitcoin_address(addr)}")
if addr in self.requested_addrs: return
self.requested_addrs.add(addr)
await self.taskgroup.spawn(self._subscribe_to_address, addr)
Expand Down
12 changes: 2 additions & 10 deletions electrum/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from aiorpcx import ignore_after, run_in_thread

from . import util, keystore, transaction, bitcoin, coinchooser, bip32, descriptor
from . import constants
from .i18n import _
from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath, convert_bip32_strpath_to_intpath
from .logging import get_logger, Logger
Expand Down Expand Up @@ -456,7 +457,7 @@ def __init__(self, db: WalletDB, *, config: SimpleConfig):
self._up_to_date = False
self.up_to_date_changed_event = asyncio.Event()

self.test_addresses_sanity()
assert self.db.get('genesis_blockhash') == constants.net.GENESIS, self.db.get('genesis_blockhash')
Copy link
Copy Markdown
Member

@f321x f321x Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed this AssertionError gets caught in ElectrumGui.start_new_window() and shown as warning dialog instead of being forwarded to the crash reporter.
We should probably modify start_new_window() for the regular wallet loading case similar to the change done here for wizard exceptions: 457a092

Edit: opened a PR for this: #10605

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I did not think it possible to trigger this assert -- in theory the same check in WalletDB.upgrade_wallet_db should raise before we get here.
Is that not the case?

Copy link
Copy Markdown
Member

@f321x f321x Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah i was just manually modifying the db for testing purposes. I just think that in general it would make sense to send unexpected exceptions coming from load_wallet to the crash reporter (though this is not directly related to this PR)

if self.storage and self.has_storage_encryption():
if (se := self.storage.get_encryption_version()) not in (ae := self.get_available_storage_encryption_versions()):
raise WalletFileException(f"unexpected storage encryption type. found: {se!r}. allowed: {ae!r}")
Expand Down Expand Up @@ -695,15 +696,6 @@ def get_master_public_keys(self):
def basename(self) -> str:
return self.storage.basename() if self.storage else 'no_name'

def test_addresses_sanity(self) -> None:
addrs = self.get_receiving_addresses()
if len(addrs) > 0:
addr = str(addrs[0])
if not bitcoin.is_address(addr):
neutered_addr = addr[:5] + '..' + addr[-2:]
raise WalletFileException(f'The addresses in this wallet are not bitcoin addresses.\n'
f'e.g. {neutered_addr} (length: {len(addr)})')

def check_returned_address_for_corruption(func):
def wrapper(self, *args, **kwargs):
addr = func(self, *args, **kwargs)
Expand Down
33 changes: 32 additions & 1 deletion electrum/wallet_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import attr

from . import bitcoin
from . import constants
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, MyEncoder
from .keystore import bip44_derivation
from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput, BadHeaderMagic
Expand Down Expand Up @@ -69,7 +70,7 @@ def __init__(self, wallet_db: 'WalletDB'):
# seed_version is now used for the version of the wallet file
OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
FINAL_SEED_VERSION = 70 # electrum >= 2.7 will set this to prevent
FINAL_SEED_VERSION = 71 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format


Expand Down Expand Up @@ -245,6 +246,7 @@ def upgrade(self):
self._convert_version_68()
self._convert_version_69()
self._convert_version_70()
self._convert_version_71()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure

def _convert_wallet_type(self):
Expand Down Expand Up @@ -1400,6 +1402,27 @@ def _convert_version_70(self):
connection['budget_spends'] = new_budget_spends
self.data['seed_version'] = 70

def _convert_version_71(self):
"""Save 'genesis_blockhash' in DB."""
if not self._is_upgrade_method_needed(70, 70):
return
# first, check we are trying to open this DB on the correct chain (mainnet vs testnet)
addresses = self.data.get("addresses", {})
if self.data['wallet_type'] == 'imported':
recv_addrs = list(addresses.keys())
else:
recv_addrs = addresses.get("receiving", [])
if len(recv_addrs) > 0:
first_address = recv_addrs[0]
if not bitcoin.is_address(first_address):
neutered_addr = first_address[:5] + '..' + first_address[-2:]
raise WalletFileException(
f"The addresses in this wallet are not bitcoin addresses. "
f"e.g. {neutered_addr} (len={len(first_address)})")
# if so, save genesis hash
self.data['genesis_blockhash'] = constants.net.GENESIS
self.data['seed_version'] = 71

def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13):
return
Expand Down Expand Up @@ -1503,6 +1526,7 @@ def upgrade_wallet_db(data: dict, do_upgrade: bool) -> Tuple[dict, bool]:
if len(data) == 0:
# create new DB
data['seed_version'] = FINAL_SEED_VERSION
data["genesis_blockhash"] = constants.net.GENESIS
# store this for debugging purposes
v = DBMetadata(
creation_timestamp=int(time.time()),
Expand All @@ -1511,6 +1535,13 @@ def upgrade_wallet_db(data: dict, do_upgrade: bool) -> Tuple[dict, bool]:
assert data.get("db_metadata", None) is None
data["db_metadata"] = v.to_json()
was_upgraded = True
# Test mainnet/testnet mixup. Do this before DB upgrades, as those might assume
# network magic bytes (e.g. if they parse an address or an xpub).
if data.get("genesis_blockhash", None) not in (constants.net.GENESIS, None):
raise WalletFileException(
_("This wallet file was created for a different network/chain.\n"
"Current chain: {}").format(constants.net.NET_NAME)
)

dbu = WalletDBUpgrader(data)
if dbu.requires_split():
Expand Down
26 changes: 26 additions & 0 deletions tests/test_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections import defaultdict
import os
from typing import Optional, Iterable
from unittest import mock

from electrum.commands import Commands
from electrum.daemon import Daemon
Expand All @@ -11,6 +12,7 @@
from electrum.lnwatcher import LNWatcher
from electrum import util
from electrum.utils.memory_leak import count_objects_in_memory
from electrum import constants

from . import ElectrumTestCase, as_testnet, restore_wallet_from_text__for_unittest

Expand Down Expand Up @@ -347,3 +349,27 @@ async def test_password_checks_for_sto_enc(self):
wallet1 = self.daemon.load_wallet(path1, password="garbage")
with self.assertRaises(util.InvalidPassword):
wallet1 = self.daemon.load_wallet(path1, password="garbage", force_check_password=True)

async def test_mainnet_testnet_mixup(self):
"""version bytes in addresses, xpubs, etc. differ between mainnet and testnet.
If the user tries to open a wallet for a different chain, try to show a reasonable error message.
"""
# we are on mainnet, and will try to open testnet wallets:
assert constants.net.TESTNET is False

# case 1: fresh wallet created on wrong network
with mock.patch("electrum.constants.net", constants.BitcoinTestnet):
path = self._restore_wallet_from_text("9dk", password=None)
with self.assertRaises(util.WalletFileException):
wallet = self.daemon.load_wallet(path, password=None, upgrade=True)

# case 2: existing older wallet (db v57) that gets populated with 'genesis_blockhash' in convert_version_71
path = self.get_wallet_file_path("client_4_5_2_9dk_with_ln")
with self.assertRaises(util.WalletFileException):
wallet = self.daemon.load_wallet(path, password=None, upgrade=True)

# case 3: existing older wallet (db v18) that gets populated with 'genesis_blockhash' in convert_version_71
# // this test does not work: convert_version_20 raises InvalidMasterKeyVersionBytes
# path = self.get_wallet_file_path("client_3_3_8_xpub_with_realistic_history")
# with self.assertRaises(util.WalletFileException):
# wallet = self.daemon.load_wallet(path, password=None, upgrade=True)