Merge pull request #10920 from SomberNight/202609_ln_chan_backup_db_upgrade
What changed, and why it matters
This commit refactors how Electrum stores Lightning channel backups on disk. It converts backup records from a flexible JSON-style format into a stricter binary format, and adds a one-time database upgrade routine. The change is primarily a code-quality and data-format cleanup, not a fix for an active security flaw. However, any database migration that touches sensitive key material carries a small risk of corruption or mis-handling of secrets if the conversion has bugs.
Treat as a normal maintenance/refactor commit. Reviewers should verify that the _convert_version_72 migration correctly preserves all backup fields (especially optional local_payment_pubkey and multisig_funding_privkey), that the uint16 masking does not alter valid values, and that the new str/blob round-trip in lnworker.py cannot introduce type confusion. No urgent security patch is indicated by the diff alone.
Security signals we found
Database migration touches Lightning private key material (privkey, channel_seed, multisig_funding_privkey)
Migration masks legacy int16 sign-extension bugs in funding_index, local_delay, remote_delay, port fields
Serialization format change from JSON StoredObject to custom binary blob
New deserialization path checks isinstance(storage, str) and converts hex blob back to object at load time
No explicit security advisory, CVE, or researcher attribution in commit or supplied references
Evidence from the diff
The patch migrates ChannelBackupStorage subclasses from attr-based StoredObject JSON serialization to frozen dataclasses, and serializes imported channel backups as hex-encoded binary blobs (matching the existing wire format in ImportedChannelBackupStorage.from_bytes). A new wallet DB version 72 and _convert_version_72() re-encode legacy JSON imported_channel_backups into the binary blob format. On-chain backups remain as JSON but are now handled by a dataclass with a stored_at-decorated from_json_dict factory. The code also splits decryption from deserialization by adding decrypt_encrypted_str() and storing the raw encrypted blob hex in the wallet DB instead of a deserialized object.
Changed components
electrum/lnutil.pyelectrum/lnworker.pyelectrum/wallet.pyelectrum/wallet_db.pyLightning channel backup storage and importWallet database upgrade path (seed_version 71 -> 72)Inspect captured patch +314 / −31
### electrum/lnutil.py
@@ -304,12 +304,12 @@ class ChannelConstraints(StoredObject):
assert CHANNEL_BACKUP_VERSION_LATEST in KNOWN_CHANNEL_BACKUP_VERSIONS
-@attr.s
-class ChannelBackupStorage(StoredObject):
- funding_txid = attr.ib(type=str)
- funding_index = attr.ib(type=int, converter=int)
- funding_address = attr.ib(type=str)
- is_initiator = attr.ib(type=bool)
+@dataclasses.dataclass(frozen=True)
+class ChannelBackupStorage:
+ funding_txid: str
+ funding_index: int
+ funding_address: str
+ is_initiator: bool
def funding_outpoint(self):
return Outpoint(self.funding_txid, self.funding_index)
@@ -319,26 +319,33 @@ def channel_id(self):
return chan_id
-@stored_at('/onchain_channel_backups/*')
-@attr.s
+@dataclasses.dataclass(frozen=True)
class OnchainChannelBackupStorage(ChannelBackupStorage):
- node_id_prefix = attr.ib(type=bytes, converter=hex_to_bytes) # remote node pubkey
+ node_id_prefix: bytes # remote node pubkey (prefix)
+ def to_json(self) -> dict:
+ return dataclasses.asdict(self)
-@stored_at('/imported_channel_backups/*')
-@attr.s
+ @staticmethod
+ @stored_at('/onchain_channel_backups/*')
+ def from_json_dict(**kwargs) -> 'OnchainChannelBackupStorage':
+ kwargs['node_id_prefix'] = bytes.fromhex(kwargs['node_id_prefix'])
+ return OnchainChannelBackupStorage(**kwargs)
+
+
+@dataclasses.dataclass(frozen=True)
class ImportedChannelBackupStorage(ChannelBackupStorage):
- node_id = attr.ib(type=bytes, converter=hex_to_bytes) # remote node pubkey
- privkey = attr.ib(type=bytes, converter=hex_to_bytes) # local node privkey
- host = attr.ib(type=str)
- port = attr.ib(type=int, converter=int)
- channel_seed = attr.ib(type=bytes, converter=hex_to_bytes)
- local_delay = attr.ib(type=int, converter=int)
- remote_delay = attr.ib(type=int, converter=int)
- remote_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes)
- remote_revocation_pubkey = attr.ib(type=bytes, converter=hex_to_bytes)
- local_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes) # type: Optional[bytes]
- multisig_funding_privkey = attr.ib(type=bytes, converter=hex_to_bytes) # type: Optional[bytes]
+ node_id: bytes # remote node pubkey
+ privkey: bytes # local node privkey
+ host: str
+ port: int
+ channel_seed: bytes
+ local_delay: int
+ remote_delay: int
+ remote_payment_pubkey: bytes
+ remote_revocation_pubkey: bytes
+ local_payment_pubkey: Optional[bytes]
+ multisig_funding_privkey: Optional[bytes]
def to_bytes(self) -> bytes:
vds = BCDataStream()
@@ -407,11 +414,15 @@ def from_bytes(s: bytes) -> 'ImportedChannelBackupStorage':
)
@staticmethod
- def from_encrypted_str(data: str, *, password: str) -> 'ImportedChannelBackupStorage':
+ def decrypt_encrypted_str(data: str, *, password: str) -> bytes:
if not data.startswith('channel_backup:'):
raise ValueError("missing or invalid magic bytes")
encrypted = data[15:]
- decrypted = pw_decode_with_version_and_mac(encrypted, password)
+ return pw_decode_with_version_and_mac(encrypted, password)
+
+ @staticmethod
+ def from_encrypted_str(data: str, *, password: str) -> 'ImportedChannelBackupStorage':
+ decrypted = ImportedChannelBackupStorage.decrypt_encrypted_str(data, password=password)
return ImportedChannelBackupStorage.from_bytes(decrypted)
### electrum/lnworker.py
@@ -1054,6 +1054,9 @@ def __init__(self, wallet: 'Abstract_Wallet', xprv, *, features: LnFeatures = No
for name in ["onchain_channel_backups", "imported_channel_backups"]:
channel_backups = self.db.get_dict(name)
for channel_id, storage in channel_backups.items():
+ if isinstance(storage, str):
+ storage = ImportedChannelBackupStorage.from_bytes(bytes.fromhex(storage))
+ assert isinstance(storage, (OnchainChannelBackupStorage, ImportedChannelBackupStorage))
self._channel_backups[bfh(channel_id)] = cb = ChannelBackup(storage, lnworker=self)
self.wallet.set_reserved_addresses_for_chan(cb, reserved=True)
@@ -3794,13 +3797,14 @@ async def request_force_close(self, channel_id: bytes, *, connect_str=None) -> N
def import_channel_backup(self, data):
xpub = self.wallet.get_fingerprint()
- cb_storage = ImportedChannelBackupStorage.from_encrypted_str(data, password=xpub)
+ cb_blob = ImportedChannelBackupStorage.decrypt_encrypted_str(data, password=xpub)
+ cb_storage = ImportedChannelBackupStorage.from_bytes(cb_blob)
channel_id = cb_storage.channel_id()
if channel_id.hex() in self.db.get_dict("channels"):
raise Exception('Channel already in wallet')
self.logger.info(f'importing channel backup: {channel_id.hex()}')
d = self.db.get_dict("imported_channel_backups")
- d[channel_id.hex()] = cb_storage
+ d[channel_id.hex()] = cb_blob.hex()
with self.lock:
cb = ChannelBackup(cb_storage, lnworker=self)
self._channel_backups[channel_id] = cb
### electrum/wallet.py
@@ -511,7 +511,7 @@ def save_backup(self, backup_dir):
if self.lnworker:
channel_backups = new_db.get_dict('imported_channel_backups')
for chan_id, chan in self.lnworker.channels.items():
- channel_backups[chan_id.hex()] = self.lnworker.create_channel_backup(chan_id)
+ channel_backups[chan_id.hex()] = self.lnworker.create_channel_backup(chan_id).to_bytes().hex()
new_db.put('channels', None)
new_db.set_modified(True)
new_db.write()
### electrum/wallet_db.py
@@ -35,9 +35,10 @@
from . import bitcoin
from . import constants
-from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, MyEncoder
+from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, MyEncoder, bfh
from .keystore import bip44_derivation
-from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput, BadHeaderMagic
+from .transaction import (Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput, BadHeaderMagic,
+ BCDataStream)
from .logging import Logger
from .lnutil import HTLCOwner, ChannelType, RecvMPPResolution
@@ -71,7 +72,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 = 71 # electrum >= 2.7 will set this to prevent
+FINAL_SEED_VERSION = 72 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format
@@ -260,6 +261,7 @@ def upgrade(self):
self._convert_version_69()
self._convert_version_70()
self._convert_version_71()
+ self._convert_version_72()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
def _convert_wallet_type(self):
@@ -1436,6 +1438,48 @@ def _convert_version_71(self):
self.data['genesis_blockhash'] = constants.net.GENESIS
self.data['seed_version'] = 71
+ def _convert_version_72(self):
+ """Serialize imported channel backups into their internal binary blob format (hex), instead of json
+ StoredObjects."""
+ if not self._is_upgrade_method_needed(71, 71):
+ return
+
+ def _serialize_imported_channel_backup(cb: dict) -> str:
+ # this mirrors the wire format read by lnutil.ImportedChannelBackupStorage.from_bytes.
+ if cb['multisig_funding_privkey'] is not None:
+ version = 2
+ elif cb['local_payment_pubkey'] is not None:
+ version = 1
+ else:
+ version = 0
+ vds = BCDataStream()
+ vds.write_uint16(version)
+ vds.write_boolean(cb['is_initiator'])
+ vds.write_bytes(bfh(cb['privkey']), 32)
+ vds.write_bytes(bfh(cb['channel_seed']), 32)
+ vds.write_bytes(bfh(cb['node_id']), 33)
+ vds.write_bytes(bfh(cb['funding_txid']), 32)
+ # note: Electrum < 4.4.0 parsed the uint16 fields as int16 (see 5a4c39cb94), so
+ # imported backups may hold negative values (e.g. port 42069 stored as -23467): mask them
+ vds.write_uint16(cb['funding_index'] & 0xffff)
+ vds.write_string(cb['funding_address'])
+ vds.write_bytes(bfh(cb['remote_payment_pubkey']), 33)
+ vds.write_bytes(bfh(cb['remote_revocation_pubkey']), 33)
+ vds.write_uint16(cb['local_delay'] & 0xffff)
+ vds.write_uint16(cb['remote_delay'] & 0xffff)
+ vds.write_string(cb['host'])
+ vds.write_uint16(cb['port'] & 0xffff)
+ if version >= 1:
+ vds.write_bytes(bfh(cb['local_payment_pubkey']), 33)
+ if version >= 2:
+ vds.write_bytes(bfh(cb['multisig_funding_privkey']), 32)
+ return bytes(vds.input).hex()
+
+ channel_backups = self.data.get('imported_channel_backups', {})
+ for channel_id, storage in channel_backups.items():
+ channel_backups[channel_id] = _serialize_imported_channel_backup(storage)
+ self.data['seed_version'] = 72
+
def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13):
return
### tests/test_lnutil.py
@@ -10,12 +10,14 @@
derive_privkey, derive_pubkey, make_htlc_tx, extract_ctn_from_tx, get_compressed_pubkey_from_bech32,
ScriptHtlc, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures, ln_compare_features,
IncompatibleLightningFeatures, ChannelType, offered_htlc_trim_threshold_sat, received_htlc_trim_threshold_sat,
- ImportedChannelBackupStorage, list_enabled_ln_feature_bits, PaymentFeeBudget, LnFeatureContexts
+ ImportedChannelBackupStorage, OnchainChannelBackupStorage, list_enabled_ln_feature_bits, PaymentFeeBudget,
+ LnFeatureContexts
)
from electrum.util import bfh, MyEncoder
from electrum.transaction import Transaction, PartialTransaction, Sighash
from electrum.lnworker import LNWallet
from electrum.wallet import Standard_Wallet
+from electrum.wallet_db import WalletDB, FINAL_SEED_VERSION
from electrum.simple_config import SimpleConfig
from . import ElectrumTestCase, as_testnet
@@ -1162,6 +1164,18 @@ async def test_decode_imported_channel_backup_v1(self):
decoded_cb,
)
+ def test_onchain_channel_backup_json_roundtrip(self):
+ cb = OnchainChannelBackupStorage(
+ funding_txid='97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe',
+ funding_index=1,
+ funding_address='tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp',
+ is_initiator=True,
+ node_id_prefix=bfh('02bf82e22f99dcd7ac1de4aad5152ce4'),
+ )
+ data = {'seed_version': FINAL_SEED_VERSION, 'onchain_channel_backups': {cb.channel_id().hex(): cb}}
+ db = WalletDB(json.dumps(data, cls=MyEncoder), storage=None, upgrade=False)
+ self.assertEqual(cb, db.get_dict('onchain_channel_backups')[cb.channel_id().hex()])
+
async def test_payment_fee_budget(self):
config = SimpleConfig({'electrum_path': self.electrum_path})
# test value above cutoff
### tests/test_storage_upgrade.py
@@ -331,6 +331,16 @@ async def test_upgrade_from_client_4_6_0_with_unfulfilled_htlcs(self):
wallet_str = self._get_wallet_str()
await self._upgrade_storage(wallet_str)
+ @as_testnet
+ async def test_upgrade_from_client_4_8_1_9dk_with_ln_chan_backups(self):
+ # Has LN "imported_channel_backups" and "onchain_channel_backups".
+ # This tests imported chan backup conversion (db version 71->72).
+ wallet_str = self._get_wallet_str()
+ db = await self._upgrade_storage(wallet_str)
+ assert db.get("imported_channel_backups").get("ddb06b023f24a587d96a9f113c02d266549d010a57d7b151c1f5332a9bbaafd5") \
+ == "0200017e634853dc47f0bc2f2e0d1054b302fcb414371ddbd889f29ba8aa4e8b62c7725d472c7b642b14176f275d6dca60c8d1ec5cfbf935169f1fe873e6bd0ad155da038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9d5afba9b2a33f5c151b1d7570a019d5466d2023c119f6ad987a5243f026bb0dd00003e74623171357a64726430703772366d68353961726e636179763030326e727530347030706636653264756b6479326833707672726e67747336687473616a02f2fa10e1317153b9cca5c0af211bcdd48aac4cf67a6f4d1cb7de71857261a1190303a53b5175b7ad2de558fc1f140d129fc5dd0949f1fbfac28ce2c33b236fbc6ef00390000e3230332e3133322e39342e313936072602a1ceaaae7b1da9d2e679977615988c62903e93a2e5d972aff6f0441face4be10c87b61e091f3f786ca44dc25283557214686d5ddb138eeb9df484145b991356b"
+
+
##########
plugins: 'electrum.plugin.Plugins'
### tests/test_storage_upgrade/client_4_8_1_9dk_with_ln_chan_backups
@@ -0,0 +1,200 @@
+{
+ "active_forwardings": {},
+ "addr_history": {},
+ "addresses": {
+ "change": [
+ "tb1q07ulrxeuu45uqen0clqe85v5en6rf77cxgxsj5",
+ "tb1qjm4rr97lxawx5csc3nu3rxhwnpxqe3s4uhwagu",
+ "tb1quhk94rhlsflc4wgxl9qzd6p6wszt30uxt4a0yj",
+ "tb1qdy4xwmgklqmyrfj336g4f54582zxtm2yhlge8l",
+ "tb1q0quewquwhlfgahhsdg0q3r5lmyzufrtp3fzme4",
+ "tb1qvsvr06h2phnwmzvwxrlkuzfu2ekhjpfpvguyct",
+ "tb1q25arh97ze37n6nk74n3js8ls8z7sva3f0d8pnl",
+ "tb1q008n3k9xjpcuyx4mlczn9jm2at90ts55yrtynq",
+ "tb1qcwytrrw3wugydlktsh6yvshlk7jwld38akp8l3",
+ "tb1qak6t2hcl3se6epvhlffaprvfjuf37xunnxq7c9",
+ "tb1qgdp7aa38x3p2kpn2s5486mkvvx2sktnmxkf47e",
+ "tb1qjpgepu2p6gyff9a92n2mwst4j2wjktra956lcg",
+ "tb1qcmq7v2zg0jjy5g47k90fqd0h7a4mcyp7f3ly6r",
+ "tb1q8m8pzk9gpjamgrw3y6y8xtfmw754nedldje5q5",
+ "tb1qgqfvg54gaads92a6dhwcgvvfjvnxdtq373guj5",
+ "tb1qahrz50yej9v7574q9are3urwyqsdcdddmjl9a6",
+ "tb1q02g5nde0heaed0y24rztkedh9nvswknw50h7fx",
+ "tb1qzyaz308030saay93zqma0at032vfqa9y0gfge3",
+ "tb1q3s4hkssd34tyxdlhafthv4muckjtgwuhltu37t",
+ "tb1qjv7a78ea0jp9d793d5ra7mtzkjzezwldz5zvr6",
+ "tb1qndaru6pfal030ev296uxwuulxrezaj0j70ceje",
+ "tb1qsd2c4xwg47hnngn2uqg66y5rxz2hp074u9rq6r",
+ "tb1qtqqddqrlg4xj3dzvjnea8wh5zy2fdf3jxl7qhs",
+ "tb1q9u3ufsm9ksql8utguap40ch8zpnw83fgzf4z97",
+ "tb1qlclgzsp2tktdl66xuk3je7ztztstxvjatly8wy",
+ "tb1q7mhmnxal53vtc5flh69nph44vah5j56eyesjx9",
+ "tb1qttqhfn950jq8dwfz7zfzc3erg3js2hchy28q9f",
+ "tb1q7dk8lyygf3qf8tyvln9qg75uj6r2ldnd4qa5lr",
+ "tb1q5l5z042rw4zl8sahc45rysas3jutwuuxxs4p0g",
+ "tb1qdjvf3d6m5znfnfvfdfgw9a0x8xwv3lfurt7d30",
+ "tb1qtpmptaf7dzqthwdn4m2vmkcwz9lc02wvff3nna",
+ "tb1qk8j0m6djjadcmrxwpt5w4y2hzqnrugtes9yjrg",
+ "tb1qycdpva4psa7l6zzgnm084gx79cgxuad2tnu9cq",
+ "tb1qhau8u680acw3wrmmue8vqc6udtgzn77f4rn2lx",
+ "tb1qgm7a35m6dwg6699rqm8ctdhhr8rp4dy7mgh8tz",
+ "tb1qmtqmld0urkac3zkze0uzskruvcmc8x50enwvh3",
+ "tb1quhatr4gqxmltzna9uzvszngrjw6gg8amxgn4qm",
+ "tb1qxdqrfts08fz7rpvrqkq9fxxc5y3qwgahf7ksk5",
+ "tb1qj3tnfdgrty4q245axeg79ddx3rdt0s3fxs8xdg",
+ "tb1qyz2qskwql5z46lq9uqvre8d3etah4g2gfly2t6",
+ "tb1qwe6w86xu9y4rm8ghf732xs3zeug6m6vulky46d",
+ "tb1qwgjhu99qvhhsevv73x7tw8m9t9zyp27ddc4rm7",
+ "tb1q4gvt93xjyway66wv8f7klf5swgyd4q32hz4s49",
+ "tb1qwy3hxfexvf78dmarlx0enu2ujdypkz7tndq5f4",
+ "tb1qpaugc2dkjv78wq4r3hg4l79yg097zf9cpuqqrv",
+ "tb1qd5qtp32eyup89grq58zcf0umz8hq2haqnt6lhu",
+ "tb1qvgsrly3g3u4h8f76nl42jy3d8udmgv9qscem7k",
+ "tb1qz7vlhhljhlklergq89wryn87x5kxeh8t4cgldv",
+ "tb1qxqzzt4mkgw4wpxv88c3mzg2anksd89q9rwaj27",
+ "tb1qf9wtnavdd2ukcq0mfn8hmnehtw9hpx48ggnnrl",
+ "tb1q4783w0whd4m555f6nzv6my25mscshafgqf4zgs",
+ "tb1q492qgm94n6ekc2hfpwfd3w6pmah84zgylrgfwg",
+ "tb1q99ndnd0jgfgrdgaxeshf6ykxk35td6guwhttf3",
+ "tb1q95d7fqsed6c06xlg6dtdcm9p9pdq6q6782mex6",
+ "tb1q3uvwvkhcgpxrx3ads0nz2a734k94axny7dwey9",
+ "tb1qp7acdultka0588ndxr99xzgwd3ekm2uhjsnqat",
+ "tb1q0gc3fgxunm3gr0fxc0zw5azsfaal25azgza9pa",
+ "tb1qedqzpglqrvyeet08ghf7r67lt6dlup7trck5y4",
+ "tb1qaxtjxnm7nlxf9y842hmp7cmfa3z2cjku02sqz7",
+ "tb1quk0kunsjdzwr978hwrl7edzyayl6xfmv5z8wlj",
+ "tb1qz78zs8p9q03uuvqqqkz4pg6cme8whz4kyzhj2x",
+ "tb1q7wlpupnkx888h3kh0xmpcf29r49ef5pgws2h47",
+ "tb1q3jvreack3l0skwkm7t02rzlke0v8h6wrugsqq6",
+ "tb1qj3np7cx69cr32d5h3acatenr6j82t59yjqys6a"
+ ],
+ "receiving": [
+ "tb1qq2tmmcngng78nllq2pvrkchcdukemtj5s6l0zu",
+ "tb1qm7ckcjsed98zhvhv3dr56a22w3fehlkxyh4wgd",
+ "tb1qwu2d7rjn9yxalg7cjw6phqzk77lf3fmsta8rhu",
+ "tb1qn7d2x7272lznt5hhk9s07q3cqnrqljnwa55w6c",
+ "tb1qusm48zmlzwr32csxdw4ar7atw260h22c8zq7jk",
+ "tb1qgcgk7j9kpt2mygmhmnu4zep79cd289t6aely7z",
+ "tb1qfu50fqxhfkl69n6urjzuhc5ndr96tuaxrh3an8",
+ "tb1qd7tjvgttaxzkszzh5ty4yq97r8wscgteejustc",
+ "tb1qchyc02y9mv4xths4je9puc4yzuxt8rfm26ef07",
+ "tb1q6k5h4cz6ra8nzhg90xm9wldvadgh0fpttfthcg",
+ "tb1qplsf242vay6vavy4eguef855fx3klmp9505g88",
+ "tb1qrdzfu6mlgrxpupd4syxrv77ncku89a0y0vd7f3",
+ "tb1qt339ksrha0n5a6lwpql778erkm272hxgamdc0u",
+ "tb1qtf9mwfv8ux0j90cwtx9nvz9l46jav40sak7ncg",
+ "tb1qf03zdjdnzxwztxs9d3g9ynsvvs5rmjhvtmln35",
+ "tb1qq0gdz0vz02ypa3cawlljstrx8cxydhvalcv8wc",
+ "tb1qkp86pkt75ds257snenp3q7vs29pf4g6cmhy2hw",
+ "tb1q3t0xcpmzreece8xdxq8k5aaxrt3r623tqldp8n",
+ "tb1qr7mjlxgc6at67tx0s8ypa5efx8clc47xh6yjqg",
+ "tb1qkneqe450eqxtpr3r5z8aw4234sjmpknm0gxsae",
+ "tb1qs087qkcawefutkkv8pg037t6txldk5szfntamj",
+ "tb1q069xqa4lej2tljmd8fcvvfedav54nmspvjnfs2",
+ "tb1q0j2gt4ap2s08cz5vzm5jg87fdeps7x8v4djgrm",
+ "tb1qs3j3j05rjefnjqf0mlpztszg8acz868wnxgz6j",
+ "tb1qh878je0rfut79fkudf4mkl8m4cn8uzfsluersy",
+ "tb1qvwwxv48k9vch5ddmf83g4fhd0tnx3mt8jp6rka",
+ "tb1q8evsj0vkzfak2y5qnqx4yf9lty462l7yfhegyd",
+ "tb1q99hkhlswfnj8r5wy2xu9a9m0vy8mvffwzhrx6n",
+ "tb1qh0csljush2tad6t0s4qgx4r5t0rzcw9729l7kx",
+ "tb1qad28sgvvrxjnxdnfjxcuepgzzhzlapgxcwuj0k",
+ "tb1qym6srwn87eu2sa5prkgd2lqva0nh0tr2xkeftp",
+ "tb1qucj6lx6eatgm5396fe539x53cp8zr0yzgclk6q",
+ "tb1qee76spnnt5h4hfapfhtt8trk0mv49s5khzjaf6",
+ "tb1q8k48x77hy7dzyqwatpnh3qglgwqfltr5pws3nt",
+ "tb1qgr6mc55t7e0mfxkfwg4ucs7xlxhn7zyvq7tzkz",
+ "tb1qpkhd5zcl7hhfekd23c0cyxhnmnxe6jtvl6vy40",
+ "tb1qd9eu64qya0a4d5w553qw39xe04p7k5wema96d6",
+ "tb1qwl7h67kufm5ehdcwr637dfmtg6uhhr0dn4pyxt",
+ "tb1qvp777ckrjrqtxpuptwj4v5vxn9eppywvmzhzs3",
+ "tb1qwr53xpu64cwngmhl82a0h604anhak05hsxnt5l"
+ ]
+ },
+ "channels": {},
+ "db_metadata": {
+ "creation_timestamp": 1788279542,
+ "first_electrum_version_used": "4.8.1"
+ },
+ "dont_expire_htlcs": {},
+ "dont_settle_htlcs": {},
+ "fiat_value": {},
+ "forwarding_failures": {},
+ "frozen_coins": {},
+ "genesis_blockhash": "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943",
+ "imported_channel_backups": {
+ "ddb06b023f24a587d96a9f113c02d266549d010a57d7b151c1f5332a9bbaafd5": {
+ "channel_seed": "5d472c7b642b14176f275d6dca60c8d1ec5cfbf935169f1fe873e6bd0ad155da",
+ "funding_address": "tb1q5zdrd0p7r6mh59arncayv002nru04p0pf6e2dukdy2h3pvrrngts6htsaj",
+ "funding_index": 0,
+ "funding_txid": "d5afba9b2a33f5c151b1d7570a019d5466d2023c119f6ad987a5243f026bb0dd",
+ "host": "203.132.94.196",
+ "is_initiator": true,
+ "local_delay": 1008,
+ "local_payment_pubkey": "02a1ceaaae7b1da9d2e679977615988c62903e93a2e5d972aff6f0441face4be10",
+ "multisig_funding_privkey": "c87b61e091f3f786ca44dc25283557214686d5ddb138eeb9df484145b991356b",
+ "node_id": "038863cf8ab91046230f561cd5b386cbff8309fa02e3f0c3ed161a3aeb64a643b9",
+ "port": 9735,
+ "privkey": "7e634853dc47f0bc2f2e0d1054b302fcb414371ddbd889f29ba8aa4e8b62c772",
+ "remote_delay": 144,
+ "remote_payment_pubkey": "02f2fa10e1317153b9cca5c0af211bcdd48aac4cf67a6f4d1cb7de71857261a119",
+ "remote_revocation_pubkey": "0303a53b5175b7ad2de558fc1f140d129fc5dd0949f1fbfac28ce2c33b236fbc6e"
+ }
+ },
+ "invoices": {},
+ "keystore": {
+ "derivation": "m/0h",
+ "pw_hash_version": 1,
+ "root_fingerprint": "535e473f",
+ "seed": "9dk",
+ "seed_type": "segwit",
+ "type": "bip32",
+ "xprv": "vprv9FrABTX8HFeSYL9aaMnLRcEkHBbJnBu9foDJaTvcF8SLvHx6uKqL8rtt7kTd66V4QPLfWPaCJMVZa3h9zuzLr7YFZd1uoEevqqyxp66oSbN",
+ "xpub": "vpub5UqWay427dCjkpE3gPKLnkBUqDRoBed1328uNrLDoTyKo6HFSs9agfDMy1VXbVtcuBVRiAZQsPPsPdu1Ge8m8qvNZPyzJ4ecPsf6U1ieW4x"
+ },
+ "labels": {},
+ "lightning_payments": {},
+ "lightning_preimages": {},
+ "lightning_xprv": "vprv9HAix419EKycrbTEJxT1zKCLURqA9LWRHeohrddxQ35QuvfH8fqMurdF4mseJ5oytUCJH5VvhEXSmCTs5SaoT7jEr2hcy7e8uCFLJtuDSme",
+ "notes_text": "",
+ "num_parents": {
+ "145c342860df8e36fb9b8edccc97afc0fec484091720addc1c75841b97d2c58b": 4,
+ "54b95c9427edcfb65337f52373f39fe6ea1c86b3ce35206a6db44be997c3c41e": 64,
+ "d5afba9b2a33f5c151b1d7570a019d5466d2023c119f6ad987a5243f026bb0dd": 66
+ },
+ "onchain_channel_backups": {
+ "8bc5d2971b84751cdcad20170984c4fec0af97ccdc8e9bfb368edf6028345c15": {
+ "funding_address": "tb1qz44lreq94yv7sr6lep2es9jlwny0gljrnf2gncn4xyjakv00sxdsrvwc40",
+ "funding_index": 1,
+ "funding_txid": "145c342860df8e36fb9b8edccc97afc0fec484091720addc1c75841b97d2c58b",
+ "is_initiator": true,
+ "node_id_prefix": "02bf82e22f99dcd7ac1de4aad5152ce4"
+ }
+ },
+ "payment_requests": {},
+ "plugin_data": {},
+ "prevouts_by_scripthash": {},
+ "qt-console-history": [
+ "wallet.clear_history()"
+ ],
+ "received_mpp_htlcs": {},
+ "seed_version": 71,
+ "spent_outpoints": {},
+ "stored_height": 5127877,
+ "submarine_swaps": {},
+ "transactions": {},
+ "tx_batches": {},
+ "tx_fees": {},
+ "txi": {},
+ "txo": {},
+ "use_encryption": false,
+ "verified_tx3": {},
+ "wallet_nonce": 142,
+ "wallet_type": "standard",
+ "winpos-qt": [
+ 907,
+ 217,
+ 840,
+ 400
+ ]
+}Why this scored 26/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.