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/ 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() diff --git a/electrum/lnutil.py b/electrum/lnutil.py index b325fb7ab..4ffb77101 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 @@ -264,44 +264,52 @@ class ImportedChannelBackupStorage(ChannelBackupStorage): def to_bytes(self) -> 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): + 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( - 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_uint16(), + funding_address=vds.read_string(), + remote_payment_pubkey=vds.read_bytes(33), + remote_revocation_pubkey=vds.read_bytes(33), + local_delay=vds.read_uint16(), + remote_delay=vds.read_uint16(), + host=vds.read_string(), + port=vds.read_uint16(), + ) + @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, + )