Merge pull request #10592 from SomberNight/202604_testnet_mainnet_mixup2

wallet_db: put 'genesis_blockhash' in DB, detect mainnet/testnet mixup. (db upgrade)
This commit is contained in:
ghost43
2026-04-24 12:59:31 +00:00
committed by GitHub
5 changed files with 76 additions and 16 deletions
+13 -2
View File
@@ -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):
@@ -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.
@@ -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:
+3 -3
View File
@@ -33,7 +33,7 @@ from aiorpcx import run_in_thread, RPCError
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
@@ -84,12 +84,12 @@ class SynchronizerBase(NetworkJobOnDefaultServer):
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)
+2 -10
View File
@@ -46,6 +46,7 @@ import electrum_ecc as ecc
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
@@ -456,7 +457,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
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')
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}")
@@ -695,15 +696,6 @@ class Abstract_Wallet(ABC, Logger, EventListener):
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)
+32 -1
View File
@@ -34,6 +34,7 @@ from functools import partial
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
@@ -69,7 +70,7 @@ class WalletUnfinished(WalletFileException):
# 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
@@ -245,6 +246,7 @@ class WalletDBUpgrader(Logger):
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):
@@ -1400,6 +1402,27 @@ class WalletDBUpgrader(Logger):
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
@@ -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()),
@@ -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():
+26
View File
@@ -2,6 +2,7 @@ import asyncio
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
@@ -11,6 +12,7 @@ from electrum.lnworker import LNWallet, LNPeerManager
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
@@ -347,3 +349,27 @@ class TestLoadWallet(DaemonTestCase):
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)