From 68bba4705247bb8e77ecbdc14b35306a510eb3bb Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 19 Mar 2023 18:34:43 +0000 Subject: [PATCH 1/5] commands: small fix and clean-up for "serialize" cmd Docstring was outdated, and `txout.get('value', txout['value_sats'])` was a logic bug. fixes https://github.com/spesmilo/electrum/issues/8265 --- electrum/commands.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 91c42a2c8..d7e1af79e 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -371,9 +371,17 @@ class Commands: @command('') async def serialize(self, jsontx): - """Create a transaction from json inputs. - Inputs must have a redeemPubkey. - Outputs must be a list of {'address':address, 'value':satoshi_amount}. + """Create a signed raw transaction from a json tx template. + + Example value for "jsontx" arg: { + "inputs": [ + {"prevout_hash": "9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539", "prevout_n": 1, + "value_sats": 1000000, "privkey": "p2wpkh:cVDXzzQg6RoCTfiKpe8MBvmm5d5cJc6JLuFApsFDKwWa6F5TVHpD"} + ], + "outputs": [ + {"address": "tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd", "value_sats": 990000} + ] + } """ keypairs = {} inputs = [] # type: List[PartialTxInput] @@ -386,7 +394,10 @@ class Commands: else: raise Exception("missing prevout for txin") txin = PartialTxInput(prevout=prevout) - txin._trusted_value_sats = int(txin_dict.get('value', txin_dict['value_sats'])) + try: + txin._trusted_value_sats = int(txin_dict.get('value') or txin_dict['value_sats']) + except KeyError: + raise Exception("missing 'value_sats' field for txin") nsequence = txin_dict.get('nsequence', None) if nsequence is not None: txin.nsequence = nsequence @@ -399,8 +410,19 @@ class Commands: txin.script_descriptor = desc inputs.append(txin) - outputs = [PartialTxOutput.from_address_and_value(txout['address'], int(txout.get('value', txout['value_sats']))) - for txout in jsontx.get('outputs')] + outputs = [] # type: List[PartialTxOutput] + for txout_dict in jsontx.get('outputs'): + try: + txout_addr = txout_dict['address'] + except KeyError: + raise Exception("missing 'address' field for txout") + try: + txout_val = int(txout_dict.get('value') or txout_dict['value_sats']) + except KeyError: + raise Exception("missing 'value_sats' field for txout") + txout = PartialTxOutput.from_address_and_value(txout_addr, txout_val) + outputs.append(txout) + tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime) tx.sign(keypairs) return tx.serialize() From a30cda4ebd2b867c7cc14beb1ea618036e95f91a Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 19 Mar 2023 19:15:44 +0000 Subject: [PATCH 2/5] lnutil: test ImportedChannelBackupStorage.from_bytes regression test - we should not inadvertently break deserialising existing backups --- electrum/lnutil.py | 11 +++++++++-- electrum/lnworker.py | 5 +---- electrum/tests/test_lnutil.py | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index b325fb7ab..b5a657be6 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -16,7 +16,7 @@ from .util import list_enabled_bits from .util import ShortID as ShortChannelID from .util import format_short_id as format_short_channel_id -from .crypto import sha256 +from .crypto import sha256, pw_decode_with_version_and_mac from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOutpoint, PartialTxOutput, opcodes, TxOutput) from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number @@ -281,7 +281,7 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): return bytes(vds.input) @staticmethod - def from_bytes(s): + def from_bytes(s: bytes) -> "ImportedChannelBackupStorage": vds = BCDataStream() vds.write(s) version = vds.read_int16() @@ -302,6 +302,13 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): host = vds.read_string(), port = vds.read_int16()) + @staticmethod + def from_encrypted_str(data: str, *, password: str) -> "ImportedChannelBackupStorage": + 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 ImportedChannelBackupStorage.from_bytes(decrypted) class ScriptHtlc(NamedTuple): diff --git a/electrum/lnworker.py b/electrum/lnworker.py index fe86970e8..57652bdc5 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -2452,11 +2452,8 @@ class LNWallet(LNWorker): raise Exception(f'Unknown channel {channel_id.hex()}') def import_channel_backup(self, data): - assert data.startswith('channel_backup:') - encrypted = data[15:] xpub = self.wallet.get_fingerprint() - decrypted = pw_decode_with_version_and_mac(encrypted, xpub) - cb_storage = ImportedChannelBackupStorage.from_bytes(decrypted) + cb_storage = ImportedChannelBackupStorage.from_encrypted_str(data, password=xpub) channel_id = cb_storage.channel_id() if channel_id.hex() in self.db.get_dict("channels"): raise Exception('Channel already in wallet') diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py index e3dff0d60..5133c1ad3 100644 --- a/electrum/tests/test_lnutil.py +++ b/electrum/tests/test_lnutil.py @@ -9,12 +9,15 @@ from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_see derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret, get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError, ScriptHtlc, extract_nodeid, calc_fees_for_commitment_tx, UpdateAddHtlc, LnFeatures, - ln_compare_features, IncompatibleLightningFeatures, ChannelType) + ln_compare_features, IncompatibleLightningFeatures, ChannelType, + ImportedChannelBackupStorage) from electrum.util import bfh, MyEncoder from electrum.transaction import Transaction, PartialTransaction, Sighash from electrum.lnworker import LNWallet +from electrum.wallet import restore_wallet_from_text, Standard_Wallet +from electrum.simple_config import SimpleConfig -from . import ElectrumTestCase +from . import ElectrumTestCase, as_testnet funding_tx_id = '8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be' @@ -903,3 +906,29 @@ class TestLNUtil(ElectrumTestCase): # ignore unknown channel types channel_type = ChannelType(0b10000000001000000000010).discard_unknown_and_check() self.assertEqual(ChannelType(0b10000000001000000000000), channel_type) + + @as_testnet + async def test_decode_imported_channel_backup(self): + encrypted_cb = "channel_backup:Adn87xcGIs9H2kfp4VpsOaNKWCHX08wBoqq37l1cLYKGlJamTeoaLEwpJA81l1BXF3GP/mRxqkY+whZG9l51G8izIY/kmMSvnh0DOiZEdwaaT/1/MwEHfsEomruFqs+iW24SFJPHbMM7f80bDtIxcLfZkKmgcKBAOlcqtq+dL3U3yH74S8BDDe2L4snaxxpCjF0JjDMBx1UR/28D+QlIi+lbvv1JMaCGXf+AF1+3jLQf8+lVI+rvFdyArws6Ocsvjf+ANQeSGUwW6Nb2xICQcMRgr1DO7bO4pgGu408eYRr2v3ayJBVtnKwSwd49gF5SDSjTDAO4CCM0uj9H5RxyzH7fqotkd9J80MBr84RiBXAeXKz+Ap8608/FVqgQ9BOcn6LhuAQdE5zXpmbQyw5jUGkPvHuseR+rzthzncy01odUceqTNg==" + config = SimpleConfig({'electrum_path': self.electrum_path}) + d = restore_wallet_from_text("9dk", path=None, gap_limit=2, config=config) + wallet1 = d['wallet'] # type: Standard_Wallet + decoded_cb = ImportedChannelBackupStorage.from_encrypted_str(encrypted_cb, password=wallet1.get_fingerprint()) + self.assertEqual( + ImportedChannelBackupStorage( + funding_txid='97767fdefef3152319363b772914d71e5eb70e793b835c13dce20037d3ac13fe', + funding_index=1, + funding_address='tb1qfsxllwl2edccpar9jas9wsxd4vhcewlxqwmn0w27kurkme3jvkdqn4msdp', + is_initiator=True, + node_id=bfh('02bf82e22f99dcd7ac1de4aad5152ce48f0694c46ec582567f379e0adbf81e2d0f'), + privkey=bfh('7e634853dc47f0bc2f2e0d1054b302fcb414371ddbd889f29ba8aa4e8b62c772'), + host='lightning.electrum.org', + port=9739, + channel_seed=bfh('ce9bad44ff8521d9f57fd202ad7cdedceb934f0056f42d0f3aa7a576b505332a'), + local_delay=1008, + remote_delay=720, + remote_payment_pubkey=bfh('02a1bbc818e2e88847016a93c223eb4adef7bb8becb3709c75c556b6beb3afe7bd'), + remote_revocation_pubkey=bfh('022f28b7d8d1f05768ada3df1b0966083b8058e1e7197c57393e302ec118d7f0ae'), + ), + decoded_cb, + ) From 4fb35c000200eee89d9d175dc91779fbc1522075 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 19 Mar 2023 19:22:41 +0000 Subject: [PATCH 3/5] lnutil: clean-up ImportedChannelBackupStorage.from_bytes --- electrum/lnutil.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/electrum/lnutil.py b/electrum/lnutil.py index b5a657be6..8cea30275 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -288,19 +288,20 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): if version != CHANNEL_BACKUP_VERSION: raise Exception(f"unknown version for channel backup: {version}") return ImportedChannelBackupStorage( - is_initiator = vds.read_boolean(), - privkey = vds.read_bytes(32).hex(), - channel_seed = vds.read_bytes(32).hex(), - node_id = vds.read_bytes(33).hex(), - funding_txid = vds.read_bytes(32).hex(), - funding_index = vds.read_int16(), - funding_address = vds.read_string(), - remote_payment_pubkey = vds.read_bytes(33).hex(), - remote_revocation_pubkey = vds.read_bytes(33).hex(), - local_delay = vds.read_int16(), - remote_delay = vds.read_int16(), - host = vds.read_string(), - port = vds.read_int16()) + is_initiator=vds.read_boolean(), + privkey=vds.read_bytes(32), + channel_seed=vds.read_bytes(32), + node_id=vds.read_bytes(33), + funding_txid=vds.read_bytes(32).hex(), + funding_index=vds.read_int16(), + funding_address=vds.read_string(), + remote_payment_pubkey=vds.read_bytes(33), + remote_revocation_pubkey=vds.read_bytes(33), + local_delay=vds.read_int16(), + remote_delay=vds.read_int16(), + host=vds.read_string(), + port=vds.read_int16(), + ) @staticmethod def from_encrypted_str(data: str, *, password: str) -> "ImportedChannelBackupStorage": From 5a4c39cb94375221fad0025f044d1a3eacc1dfe6 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Sun, 19 Mar 2023 19:32:09 +0000 Subject: [PATCH 4/5] lnutil.ImportedChannelBackupStorage: change ser format: int16->uint16 In the binary serialised format, replace all instances of int16 with uint16. In particular, this allows port>32767. Fixes https://github.com/spesmilo/electrum/issues/8264 I think this is backwards compatible, as in, any existing channel backup already out there, should be properly parsed with the new code. (new code however can serialise cbs that old code deserialises incorrectly) ``` >>> struct.pack('>> struct.pack(' bytes: vds = BCDataStream() - vds.write_int16(CHANNEL_BACKUP_VERSION) + vds.write_uint16(CHANNEL_BACKUP_VERSION) vds.write_boolean(self.is_initiator) vds.write_bytes(self.privkey, 32) vds.write_bytes(self.channel_seed, 32) vds.write_bytes(self.node_id, 33) vds.write_bytes(bfh(self.funding_txid), 32) - vds.write_int16(self.funding_index) + vds.write_uint16(self.funding_index) vds.write_string(self.funding_address) vds.write_bytes(self.remote_payment_pubkey, 33) vds.write_bytes(self.remote_revocation_pubkey, 33) - vds.write_int16(self.local_delay) - vds.write_int16(self.remote_delay) + vds.write_uint16(self.local_delay) + vds.write_uint16(self.remote_delay) vds.write_string(self.host) - vds.write_int16(self.port) + vds.write_uint16(self.port) return bytes(vds.input) @staticmethod def from_bytes(s: bytes) -> "ImportedChannelBackupStorage": vds = BCDataStream() vds.write(s) - version = vds.read_int16() + version = vds.read_uint16() if version != CHANNEL_BACKUP_VERSION: raise Exception(f"unknown version for channel backup: {version}") return ImportedChannelBackupStorage( @@ -293,14 +293,14 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): channel_seed=vds.read_bytes(32), node_id=vds.read_bytes(33), funding_txid=vds.read_bytes(32).hex(), - funding_index=vds.read_int16(), + funding_index=vds.read_uint16(), funding_address=vds.read_string(), remote_payment_pubkey=vds.read_bytes(33), remote_revocation_pubkey=vds.read_bytes(33), - local_delay=vds.read_int16(), - remote_delay=vds.read_int16(), + local_delay=vds.read_uint16(), + remote_delay=vds.read_uint16(), host=vds.read_string(), - port=vds.read_int16(), + port=vds.read_uint16(), ) @staticmethod From 08ae0a73b2d3fb5163a115372d92d44704f7ee35 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 20 Mar 2023 01:33:04 +0000 Subject: [PATCH 5/5] build: add separate .dockerignore files The .dockerignore symlink in the project root dir is only picked up by the android build. The android build has the project root as its build context for "docker build" -- the other builds have their own subdirectories as build context, e.g. contrib/build-linux/appimage. --- contrib/build-linux/appimage/.dockerignore | 3 +++ contrib/build-linux/sdist/.dockerignore | 1 + contrib/build-wine/.dockerignore | 6 ++++++ 3 files changed, 10 insertions(+) create mode 100644 contrib/build-linux/appimage/.dockerignore create mode 100644 contrib/build-linux/sdist/.dockerignore create mode 100644 contrib/build-wine/.dockerignore diff --git a/contrib/build-linux/appimage/.dockerignore b/contrib/build-linux/appimage/.dockerignore new file mode 100644 index 000000000..d75fb8304 --- /dev/null +++ b/contrib/build-linux/appimage/.dockerignore @@ -0,0 +1,3 @@ +build/ +.cache/ +fresh_clone/ diff --git a/contrib/build-linux/sdist/.dockerignore b/contrib/build-linux/sdist/.dockerignore new file mode 100644 index 000000000..d364c6400 --- /dev/null +++ b/contrib/build-linux/sdist/.dockerignore @@ -0,0 +1 @@ +fresh_clone/ diff --git a/contrib/build-wine/.dockerignore b/contrib/build-wine/.dockerignore new file mode 100644 index 000000000..f1aa3647c --- /dev/null +++ b/contrib/build-wine/.dockerignore @@ -0,0 +1,6 @@ +tmp/ +build/ +.cache/ +dist/ +signed/ +fresh_clone/