From 13d9677e534cc82aeace3d428a6bc78eadb0e1d4 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Mon, 29 Apr 2024 16:32:19 +0000 Subject: [PATCH] transaction: tx.sign API change: rm hex usage --- electrum/commands.py | 8 ++++---- electrum/gui/qt/main_window.py | 22 ++++++++++++++++++---- electrum/gui/qt/send_tab.py | 4 ++-- electrum/gui/qt/transaction_dialog.py | 6 +++--- electrum/keystore.py | 14 ++++++++------ electrum/lnchannel.py | 2 +- electrum/lnsweep.py | 6 +++--- electrum/lnutil.py | 2 +- electrum/transaction.py | 15 +++++++-------- electrum/wallet.py | 18 ++++++++++-------- tests/test_lnutil.py | 4 ++-- 11 files changed, 59 insertions(+), 42 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index dd7f1a586..890f347c7 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -422,9 +422,9 @@ class Commands: sec = txin_dict.get('privkey') if sec: txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec) - pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) - keypairs[pubkey] = privkey, compressed - desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type) + pubkey = ecc.ECPrivkey(privkey).get_public_key_bytes(compressed=compressed) + keypairs[pubkey] = privkey + desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type) txin.script_descriptor = desc inputs.append(txin) @@ -465,7 +465,7 @@ class Commands: if address in txins_dict.keys(): for txin in txins_dict[address]: txin.script_descriptor = desc - tx.sign({pubkey.hex(): (priv2, compressed)}) + tx.sign({pubkey: priv2}) return tx.serialize() diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index c755d896e..2cb35999c 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -34,7 +34,7 @@ import base64 from functools import partial import queue import asyncio -from typing import Optional, TYPE_CHECKING, Sequence, List, Union, Dict, Set +from typing import Optional, TYPE_CHECKING, Sequence, List, Union, Dict, Set, Mapping import concurrent.futures from PyQt5.QtGui import QPixmap, QKeySequence, QIcon, QCursor, QFont, QFontMetrics @@ -1106,7 +1106,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self, tx: Transaction, *, - external_keypairs=None, + external_keypairs: Mapping[bytes, bytes] = None, payment_identifier: PaymentIdentifier = None, ): show_transaction(tx, parent=self, external_keypairs=external_keypairs, payment_identifier=payment_identifier) @@ -1269,10 +1269,24 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.send_tab.broadcast_transaction(tx, payment_identifier=payment_identifier) @protected - def sign_tx(self, tx, *, callback, external_keypairs, password): + def sign_tx( + self, + tx: PartialTransaction, + *, + callback, + external_keypairs: Optional[Mapping[bytes, bytes]], + password, + ): self.sign_tx_with_password(tx, callback=callback, password=password, external_keypairs=external_keypairs) - def sign_tx_with_password(self, tx: PartialTransaction, *, callback, password, external_keypairs=None): + def sign_tx_with_password( + self, + tx: PartialTransaction, + *, + callback, + password, + external_keypairs: Mapping[bytes, bytes] = None, + ): '''Sign the transaction in a separate thread. When done, calls the callback with a success code of True or False. ''' diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 8566b6a79..e14ae70e2 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -3,7 +3,7 @@ # file LICENCE or http://www.opensource.org/licenses/mit-license.php from decimal import Decimal -from typing import Optional, TYPE_CHECKING, Sequence, List, Callable, Union +from typing import Optional, TYPE_CHECKING, Sequence, List, Callable, Union, Mapping from PyQt5.QtCore import pyqtSignal, QPoint, QSize, Qt from PyQt5.QtWidgets import (QLabel, QVBoxLayout, QGridLayout, QHBoxLayout, QWidget, QToolTip, QPushButton, QApplication) @@ -295,7 +295,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): outputs: List[PartialTxOutput], *, nonlocal_only=False, - external_keypairs=None, + external_keypairs: Mapping[bytes, bytes] = None, get_coins: Callable[..., Sequence[PartialTxInput]] = None, invoice: Optional[Invoice] = None ) -> None: diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 8f1835751..eb3b80694 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -30,7 +30,7 @@ import copy import datetime import traceback import time -from typing import TYPE_CHECKING, Callable, Optional, List, Union, Tuple +from typing import TYPE_CHECKING, Callable, Optional, List, Union, Tuple, Mapping from functools import partial from decimal import Decimal @@ -410,7 +410,7 @@ def show_transaction( *, parent: 'ElectrumWindow', prompt_if_unsaved: bool = False, - external_keypairs=None, + external_keypairs: Mapping[bytes, bytes] = None, payment_identifier: 'PaymentIdentifier' = None, ): try: @@ -438,7 +438,7 @@ class TxDialog(QDialog, MessageBoxMixin): *, parent: 'ElectrumWindow', prompt_if_unsaved: bool, - external_keypairs=None, + external_keypairs: Mapping[bytes, bytes] = None, payment_identifier: 'PaymentIdentifier' = None, ): '''Transactions in the wallet will show their description. diff --git a/electrum/keystore.py b/electrum/keystore.py index f71ba1926..4c5c5b804 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -107,13 +107,13 @@ class KeyStore(Logger, ABC): """Returns whether the keystore can be encrypted with a password.""" pass - def _get_tx_derivations(self, tx: 'PartialTransaction') -> Dict[str, Union[Sequence[int], str]]: + def _get_tx_derivations(self, tx: 'PartialTransaction') -> Dict[bytes, Union[Sequence[int], str]]: keypairs = {} for txin in tx.inputs(): keypairs.update(self._get_txin_derivations(txin)) return keypairs - def _get_txin_derivations(self, txin: 'PartialTxInput') -> Dict[str, Union[Sequence[int], str]]: + def _get_txin_derivations(self, txin: 'PartialTxInput') -> Dict[bytes, Union[Sequence[int], str]]: if txin.is_complete(): return {} keypairs = {} @@ -124,7 +124,7 @@ class KeyStore(Logger, ABC): derivation = self.get_pubkey_derivation(pubkey, txin) if not derivation: continue - keypairs[pubkey.hex()] = derivation + keypairs[pubkey] = derivation return keypairs def can_sign(self, tx: 'Transaction', *, ignore_watching_only=False) -> bool: @@ -237,9 +237,11 @@ class Software_KeyStore(KeyStore): # Raise if password is not correct. self.check_password(password) # Add private keys - keypairs = self._get_tx_derivations(tx) - for k, v in keypairs.items(): - keypairs[k] = self.get_private_key(v, password) + keypairs = {} + pubkey_to_deriv_map = self._get_tx_derivations(tx) + for pubkey, deriv in pubkey_to_deriv_map.items(): + privkey, is_compressed = self.get_private_key(deriv, password) + keypairs[pubkey] = privkey # Sign if keypairs: tx.sign(keypairs) diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py index ef171eacd..18c5156ca 100644 --- a/electrum/lnchannel.py +++ b/electrum/lnchannel.py @@ -1614,7 +1614,7 @@ class Channel(AbstractChannel): def force_close_tx(self) -> PartialTransaction: tx = self.get_latest_commitment(LOCAL) assert self.signature_fits(tx) - tx.sign({self.config[LOCAL].multisig_key.pubkey.hex(): (self.config[LOCAL].multisig_key.privkey, True)}) + tx.sign({self.config[LOCAL].multisig_key.pubkey: self.config[LOCAL].multisig_key.privkey}) remote_sig = self.config[LOCAL].current_commitment_signature remote_sig = ecc.ecdsa_der_sig_from_ecdsa_sig64(remote_sig) + Sighash.to_sigbytes(Sighash.ALL) tx.add_signature_to_txin(txin_idx=0, diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py index 539d39c28..d80707004 100644 --- a/electrum/lnsweep.py +++ b/electrum/lnsweep.py @@ -511,12 +511,12 @@ def create_sweeptx_their_ctx_to_remote( sweep_address: str, ctx: Transaction, output_idx: int, our_payment_privkey: ecc.ECPrivkey, config: SimpleConfig) -> Optional[PartialTransaction]: - our_payment_pubkey = our_payment_privkey.get_public_key_hex(compressed=True) + our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True) val = ctx.outputs()[output_idx].value prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx) txin = PartialTxInput(prevout=prevout) txin._trusted_value_sats = val - desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=our_payment_pubkey, script_type='p2wpkh') + desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=our_payment_pubkey.hex(), script_type='p2wpkh') txin.script_descriptor = desc sweep_inputs = [txin] tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh @@ -526,7 +526,7 @@ def create_sweeptx_their_ctx_to_remote( sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)] sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs) sweep_tx.set_rbf(True) - sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)}) + sweep_tx.sign({our_payment_pubkey: our_payment_privkey.get_secret_bytes()}) if not sweep_tx.is_complete(): raise Exception('channel close sweep tx is not complete') return sweep_tx diff --git a/electrum/lnutil.py b/electrum/lnutil.py index 44aad8fb8..102548c58 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -1084,7 +1084,7 @@ def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes) -> st return bitcoin.pubkey_to_address('p2wpkh', remote_payment_pubkey.hex()) def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config): - tx.sign({local_config.multisig_key.pubkey.hex(): (local_config.multisig_key.privkey, True)}) + tx.sign({local_config.multisig_key.pubkey: local_config.multisig_key.privkey}) sig = tx.inputs()[0].part_sigs[local_config.multisig_key.pubkey] sig_64 = ecdsa_sig64_from_der_sig(sig[:-1]) return sig_64 diff --git a/electrum/transaction.py b/electrum/transaction.py index c407c95b2..5ae075f6b 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -33,7 +33,7 @@ import sys import io import base64 from typing import (Sequence, Union, NamedTuple, Tuple, Optional, Iterable, - Callable, List, Dict, Set, TYPE_CHECKING) + Callable, List, Dict, Set, TYPE_CHECKING, Mapping) from collections import defaultdict from enum import IntEnum import itertools @@ -2130,22 +2130,21 @@ class PartialTransaction(Transaction): preimage = nVersion + txins + txouts + nLocktime + nHashType return preimage - def sign(self, keypairs) -> None: - # keypairs: pubkey_hex -> (secret_bytes, is_compressed) + def sign(self, keypairs: Mapping[bytes, bytes]) -> None: + # keypairs: pubkey_bytes -> secret_bytes bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields() for i, txin in enumerate(self.inputs()): - pubkeys = [pk.hex() for pk in txin.pubkeys] - for pubkey in pubkeys: + for pubkey in txin.pubkeys: if txin.is_complete(): break if pubkey not in keypairs: continue _logger.info(f"adding signature for {pubkey}. spending utxo {txin.prevout.to_str()}") - sec, compressed = keypairs[pubkey] + sec = keypairs[pubkey] sig = self.sign_txin(i, sec, bip143_shared_txdigest_fields=bip143_shared_txdigest_fields) - self.add_signature_to_txin(txin_idx=i, signing_pubkey=bfh(pubkey), sig=sig) + self.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey, sig=sig) - _logger.debug(f"is_complete {self.is_complete()}") + _logger.debug(f"tx.sign() finished. is_complete={self.is_complete()}") self.invalidate_ser_cache() def sign_txin( diff --git a/electrum/wallet.py b/electrum/wallet.py index 088d6d091..101d431d5 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -40,7 +40,7 @@ from functools import partial from collections import defaultdict from numbers import Number from decimal import Decimal -from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence, Dict, Any, Set, Iterable +from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence, Dict, Any, Set, Iterable, Mapping from abc import ABC, abstractmethod import itertools import threading @@ -136,20 +136,22 @@ async def _append_utxos_to_inputs( await group.spawn(append_single_utxo(item)) -async def sweep_preparations(privkeys, network: 'Network', imax=100): +async def sweep_preparations( + privkeys: Iterable[str], network: 'Network', imax=100, +) -> Tuple[Sequence[PartialTxInput], Mapping[bytes, bytes]]: - async def find_utxos_for_privkey(txin_type, privkey, compressed): - pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) - desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type) + async def find_utxos_for_privkey(txin_type: str, privkey: bytes, compressed: bool): + pubkey = ecc.ECPrivkey(privkey).get_public_key_bytes(compressed=compressed) + desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type) await _append_utxos_to_inputs( inputs=inputs, network=network, script_descriptor=desc, imax=imax) - keypairs[pubkey] = privkey, compressed + keypairs[pubkey] = privkey inputs = [] # type: List[PartialTxInput] - keypairs = {} + keypairs = {} # type: Dict[bytes, bytes] async with OldTaskGroup() as group: for sec in privkeys: txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec) @@ -169,7 +171,7 @@ async def sweep_preparations(privkeys, network: 'Network', imax=100): async def sweep( - privkeys, + privkeys: Iterable[str], *, network: 'Network', config: 'SimpleConfig', diff --git a/tests/test_lnutil.py b/tests/test_lnutil.py index b6f5b0101..3689253d7 100644 --- a/tests/test_lnutil.py +++ b/tests/test_lnutil.py @@ -725,7 +725,7 @@ class TestLNUtil(ElectrumTestCase): ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220' self.assertEqual(str(our_commit_tx), ref_commit_tx_str) - def sign_and_insert_remote_sig(self, tx: PartialTransaction, remote_pubkey, remote_signature, pubkey, privkey): + def sign_and_insert_remote_sig(self, tx: PartialTransaction, remote_pubkey: bytes, remote_signature: bytes, pubkey: bytes, privkey: bytes): assert type(remote_pubkey) is bytes assert len(remote_pubkey) == 33 assert type(remote_signature) is bytes @@ -733,7 +733,7 @@ class TestLNUtil(ElectrumTestCase): assert type(privkey) is bytes assert len(pubkey) == 33 assert len(privkey) == 33 - tx.sign({pubkey.hex(): (privkey[:-1], True)}) + tx.sign({pubkey: privkey[:-1]}) sighash = Sighash.to_sigbytes(Sighash.ALL) tx.add_signature_to_txin(txin_idx=0, signing_pubkey=remote_pubkey, sig=remote_signature + sighash)