Browse Source

Merge pull request #8871 from SomberNight/202402_slip19_trezor

support SLIP-19 ownership proofs, for trezor-based Standard_Wallets
master
ThomasV 2 years ago committed by GitHub
parent
commit
e2db5ca2ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      electrum/gui/qt/my_treeview.py
  2. 70
      electrum/gui/qt/transaction_dialog.py
  3. 6
      electrum/keystore.py
  4. 10
      electrum/plugins/trezor/clientbase.py
  5. 41
      electrum/plugins/trezor/trezor.py
  6. 8
      electrum/simple_config.py
  7. 10
      electrum/transaction.py
  8. 13
      electrum/wallet.py

6
electrum/gui/qt/my_treeview.py

@ -71,9 +71,9 @@ if TYPE_CHECKING:
from .main_window import ElectrumWindow from .main_window import ElectrumWindow
class MyMenu(QMenu): class QMenuWithConfig(QMenu):
def __init__(self, config): def __init__(self, config: 'SimpleConfig'):
QMenu.__init__(self) QMenu.__init__(self)
self.setToolTipsVisible(True) self.setToolTipsVisible(True)
self.config = config self.config = config
@ -113,7 +113,7 @@ class MyMenu(QMenu):
def create_toolbar_with_menu(config: 'SimpleConfig', title): def create_toolbar_with_menu(config: 'SimpleConfig', title):
menu = MyMenu(config) menu = QMenuWithConfig(config)
toolbar_button = QToolButton() toolbar_button = QToolButton()
toolbar_button.setIcon(read_QIcon("preferences.png")) toolbar_button.setIcon(read_QIcon("preferences.png"))
toolbar_button.setMenu(menu) toolbar_button.setMenu(menu)

70
electrum/gui/qt/transaction_dialog.py

@ -63,9 +63,9 @@ from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path,
TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX, TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX,
TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX, TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX,
BlockingWaitingDialog, getSaveFileName, ColorSchemeItem, BlockingWaitingDialog, getSaveFileName, ColorSchemeItem,
get_iconname_qrcode, VLine) get_iconname_qrcode, VLine, WaitingDialog)
from .rate_limiter import rate_limited from .rate_limiter import rate_limited
from .my_treeview import create_toolbar_with_menu from .my_treeview import create_toolbar_with_menu, QMenuWithConfig
if TYPE_CHECKING: if TYPE_CHECKING:
from .main_window import ElectrumWindow from .main_window import ElectrumWindow
@ -456,7 +456,7 @@ class TxDialog(QDialog, MessageBoxMixin):
self.desc = self.wallet.get_label_for_txid(txid) or None self.desc = self.wallet.get_label_for_txid(txid) or None
self.setMinimumWidth(640) self.setMinimumWidth(640)
self.psbt_only_widgets = [] # type: List[QWidget] self.psbt_only_widgets = [] # type: List[Union[QWidget, QAction]]
vbox = QVBoxLayout() vbox = QVBoxLayout()
self.setLayout(vbox) self.setLayout(vbox)
@ -502,15 +502,24 @@ class TxDialog(QDialog, MessageBoxMixin):
b.clicked.connect(self.close) b.clicked.connect(self.close)
b.setDefault(True) b.setDefault(True)
self.export_actions_menu = export_actions_menu = QMenu() self.export_actions_menu = export_actions_menu = QMenuWithConfig(config=self.config)
self.add_export_actions_to_menu(export_actions_menu) self.add_export_actions_to_menu(export_actions_menu)
export_actions_menu.addSeparator() export_actions_menu.addSeparator()
export_submenu = export_actions_menu.addMenu(_("For CoinJoin; strip privates")) export_option = export_actions_menu.addConfig(
self.add_export_actions_to_menu(export_submenu, gettx=self._gettx_for_coinjoin) self.config.cv.GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA)
self.psbt_only_widgets.append(export_submenu) self.psbt_only_widgets.append(export_option)
export_submenu = export_actions_menu.addMenu(_("For hardware device; include xpubs")) export_option = export_actions_menu.addConfig(
self.add_export_actions_to_menu(export_submenu, gettx=self._gettx_for_hardware_device) self.config.cv.GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS)
self.psbt_only_widgets.append(export_submenu) self.psbt_only_widgets.append(export_option)
if self.wallet.has_support_for_slip_19_ownership_proofs():
export_option = export_actions_menu.addAction(
_('Include SLIP-19 ownership proofs'),
self._add_slip_19_ownership_proofs_to_tx)
export_option.setToolTip(_("Some cosigners (e.g. Trezor) might require this for coinjoins."))
self._export_option_slip19 = export_option
export_option.setCheckable(True)
export_option.setChecked(False)
self.psbt_only_widgets.append(export_option)
self.export_actions_button = QToolButton() self.export_actions_button = QToolButton()
self.export_actions_button.setText(_("Share")) self.export_actions_button.setText(_("Share"))
@ -604,9 +613,17 @@ class TxDialog(QDialog, MessageBoxMixin):
# Override escape-key to close normally (and invoke closeEvent) # Override escape-key to close normally (and invoke closeEvent)
self.close() self.close()
def add_export_actions_to_menu(self, menu: QMenu, *, gettx: Callable[[], Transaction] = None) -> None: def add_export_actions_to_menu(self, menu: QMenu) -> None:
if gettx is None: def gettx() -> Transaction:
gettx = lambda: None if not isinstance(self.tx, PartialTransaction):
return self.tx
tx = copy.deepcopy(self.tx)
if self.config.GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS:
Network.run_from_another_thread(
tx.prepare_for_export_for_hardware_device(self.wallet))
if self.config.GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA:
tx.prepare_for_export_for_coinjoin()
return tx
action = QAction(_("Copy to clipboard"), self) action = QAction(_("Copy to clipboard"), self)
action.triggered.connect(lambda: self.copy_to_clipboard(tx=gettx())) action.triggered.connect(lambda: self.copy_to_clipboard(tx=gettx()))
@ -620,20 +637,19 @@ class TxDialog(QDialog, MessageBoxMixin):
action.triggered.connect(lambda: self.export_to_file(tx=gettx())) action.triggered.connect(lambda: self.export_to_file(tx=gettx()))
menu.addAction(action) menu.addAction(action)
def _gettx_for_coinjoin(self) -> PartialTransaction: def _add_slip_19_ownership_proofs_to_tx(self):
if not isinstance(self.tx, PartialTransaction): assert isinstance(self.tx, PartialTransaction)
raise Exception("Can only export partial transactions for coinjoins.") def on_success(result):
tx = copy.deepcopy(self.tx) self._export_option_slip19.setEnabled(False)
tx.prepare_for_export_for_coinjoin() self.main_window.pop_top_level_window(self)
return tx def on_failure(exc_info):
self._export_option_slip19.setChecked(False)
def _gettx_for_hardware_device(self) -> PartialTransaction: self.main_window.on_error(exc_info)
if not isinstance(self.tx, PartialTransaction): self.main_window.pop_top_level_window(self)
raise Exception("Can only export partial transactions for hardware device.") task = partial(self.wallet.add_slip_19_ownership_proofs_to_tx, self.tx)
tx = copy.deepcopy(self.tx) msg = _('Adding SLIP-19 ownership proofs to transaction...')
Network.run_from_another_thread( self.main_window.push_top_level_window(self)
tx.prepare_for_export_for_hardware_device(self.wallet)) WaitingDialog(self, msg, task, on_success, on_failure)
return tx
def copy_to_clipboard(self, *, tx: Transaction = None): def copy_to_clipboard(self, *, tx: Transaction = None):
if tx is None: if tx is None:

6
electrum/keystore.py

@ -202,6 +202,12 @@ class KeyStore(Logger, ABC):
def can_have_deterministic_lightning_xprv(self) -> bool: def can_have_deterministic_lightning_xprv(self) -> bool:
return False return False
def has_support_for_slip_19_ownership_proofs(self) -> bool:
return False
def add_slip_19_ownership_proofs_to_tx(self, tx: 'PartialTransaction', *, password) -> None:
raise NotImplementedError()
class Software_KeyStore(KeyStore): class Software_KeyStore(KeyStore):

10
electrum/plugins/trezor/clientbase.py

@ -260,6 +260,16 @@ class TrezorClientBase(HardwareClientBase, Logger):
with self.run_flow(): with self.run_flow():
return trezorlib.btc.sign_tx(self.client, *args, **kwargs) return trezorlib.btc.sign_tx(self.client, *args, **kwargs)
@runs_in_hwd_thread
def get_ownership_id(self, *args, **kwargs):
with self.run_flow():
return trezorlib.btc.get_ownership_id(self.client, *args, **kwargs)
@runs_in_hwd_thread
def get_ownership_proof(self, *args, **kwargs):
with self.run_flow():
return trezorlib.btc.get_ownership_proof(self.client, *args, **kwargs)
@runs_in_hwd_thread @runs_in_hwd_thread
def reset_device(self, *args, **kwargs): def reset_device(self, *args, **kwargs):
with self.run_flow(): with self.run_flow():

41
electrum/plugins/trezor/trezor.py

@ -34,6 +34,8 @@ try:
TxInputType, TxOutputType, TxOutputBinType, TransactionType, AmountUnit) TxInputType, TxOutputType, TxOutputBinType, TransactionType, AmountUnit)
from trezorlib.client import PASSPHRASE_ON_DEVICE from trezorlib.client import PASSPHRASE_ON_DEVICE
import trezorlib.log
#trezorlib.log.enable_debug_output()
TREZORLIB = True TREZORLIB = True
except Exception as e: except Exception as e:
@ -95,6 +97,39 @@ class TrezorKeyStore(Hardware_KeyStore):
self.plugin.sign_transaction(self, tx, prev_tx) self.plugin.sign_transaction(self, tx, prev_tx)
def has_support_for_slip_19_ownership_proofs(self) -> bool:
return True
def add_slip_19_ownership_proofs_to_tx(self, tx: 'PartialTransaction', password) -> None:
assert isinstance(tx, PartialTransaction)
client = self.get_client()
assert isinstance(client, TrezorClientBase), client
for txin in tx.inputs():
if txin.is_coinbase_input():
continue
if txin.is_complete() or not txin.is_mine:
continue
assert txin.scriptpubkey
desc = txin.script_descriptor
assert desc
trezor_multisig = None
if multi := desc.get_simple_multisig():
# trezor_multisig = self._make_multisig(multi)
raise Exception("multisig not supported for slip-19 ownership proof")
trezor_script_type = self.plugin.get_trezor_input_script_type(desc.to_legacy_electrum_script_type())
my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin)
if full_path:
trezor_address_n = full_path
else:
continue
proof, _proof_sig = client.get_ownership_proof(
coin_name=self.plugin.get_coin_name(),
n=trezor_address_n,
multisig=trezor_multisig,
script_type=trezor_script_type,
)
txin.slip_19_ownership_proof = proof
class TrezorInitSettings(NamedTuple): class TrezorInitSettings(NamedTuple):
word_count: int word_count: int
@ -352,11 +387,13 @@ class TrezorPlugin(HW_PluginBase):
assert isinstance(tx, PartialTransaction) assert isinstance(tx, PartialTransaction)
assert isinstance(txin, PartialTxInput) assert isinstance(txin, PartialTxInput)
assert keystore assert keystore
if txin.is_complete() or not txin.is_mine: if txin.is_complete() or not txin.is_mine: # we don't sign
txinputtype.script_type = InputScriptType.EXTERNAL txinputtype.script_type = InputScriptType.EXTERNAL
assert txin.scriptpubkey assert txin.scriptpubkey
txinputtype.script_pubkey = txin.scriptpubkey txinputtype.script_pubkey = txin.scriptpubkey
else: if not txin.is_mine and txin.slip_19_ownership_proof:
txinputtype.ownership_proof = txin.slip_19_ownership_proof
else: # we sign
desc = txin.script_descriptor desc = txin.script_descriptor
assert desc assert desc
if multi := desc.get_simple_multisig(): if multi := desc.get_simple_multisig():

8
electrum/simple_config.py

@ -1083,6 +1083,14 @@ This will result in longer routes; it might increase your fees and decrease the
'Download parent transactions from the network.\n' 'Download parent transactions from the network.\n'
'Allows filling in missing fee and input details.'), 'Allows filling in missing fee and input details.'),
) )
GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA = ConfigVar(
'gui_qt_tx_dialog_export_strip_sensitive_metadata', default=False, type_=bool,
short_desc=lambda: _('For CoinJoin; strip privates'),
)
GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS = ConfigVar(
'gui_qt_tx_dialog_export_include_global_xpubs', default=False, type_=bool,
short_desc=lambda: _('For hardware device; include xpubs'),
)
GUI_QT_RECEIVE_TABS_INDEX = ConfigVar('receive_tabs_index', default=0, type_=int) GUI_QT_RECEIVE_TABS_INDEX = ConfigVar('receive_tabs_index', default=0, type_=int)
GUI_QT_RECEIVE_TAB_QR_VISIBLE = ConfigVar('receive_qr_visible', default=False, type_=bool) GUI_QT_RECEIVE_TAB_QR_VISIBLE = ConfigVar('receive_qr_visible', default=False, type_=bool)
GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar( GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar(

10
electrum/transaction.py

@ -1289,6 +1289,7 @@ class PSBTInputType(IntEnum):
BIP32_DERIVATION = 6 BIP32_DERIVATION = 6
FINAL_SCRIPTSIG = 7 FINAL_SCRIPTSIG = 7
FINAL_SCRIPTWITNESS = 8 FINAL_SCRIPTWITNESS = 8
SLIP19_OWNERSHIP_PROOF = 0x19
class PSBTOutputType(IntEnum): class PSBTOutputType(IntEnum):
@ -1386,6 +1387,7 @@ class PartialTxInput(TxInput, PSBTSection):
self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path) self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path)
self.redeem_script = None # type: Optional[bytes] self.redeem_script = None # type: Optional[bytes]
self.witness_script = None # type: Optional[bytes] self.witness_script = None # type: Optional[bytes]
self.slip_19_ownership_proof = None # type: Optional[bytes]
self._unknown = {} # type: Dict[bytes, bytes] self._unknown = {} # type: Dict[bytes, bytes]
self._script_descriptor = None # type: Optional[Descriptor] self._script_descriptor = None # type: Optional[Descriptor]
@ -1439,6 +1441,7 @@ class PartialTxInput(TxInput, PSBTSection):
'part_sigs': {pubkey.hex(): sig.hex() for pubkey, sig in self.part_sigs.items()}, 'part_sigs': {pubkey.hex(): sig.hex() for pubkey, sig in self.part_sigs.items()},
'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path)) 'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path))
for pubkey, (xfp, path) in self.bip32_paths.items()}, for pubkey, (xfp, path) in self.bip32_paths.items()},
'slip_19_ownership_proof': self.slip_19_ownership_proof.hex() if self.slip_19_ownership_proof else None,
'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()}, 'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()},
}) })
return d return d
@ -1553,6 +1556,11 @@ class PartialTxInput(TxInput, PSBTSection):
raise SerializationError(f"duplicate key: {repr(kt)}") raise SerializationError(f"duplicate key: {repr(kt)}")
self.witness = val self.witness = val
if key: raise SerializationError(f"key for {repr(kt)} must be empty") if key: raise SerializationError(f"key for {repr(kt)} must be empty")
elif kt == PSBTInputType.SLIP19_OWNERSHIP_PROOF:
if self.slip_19_ownership_proof is not None:
raise SerializationError(f"duplicate key: {repr(kt)}")
self.slip_19_ownership_proof = val
if key: raise SerializationError(f"key for {repr(kt)} must be empty")
else: else:
full_key = self.get_fullkey_from_keytype_and_key(kt, key) full_key = self.get_fullkey_from_keytype_and_key(kt, key)
if full_key in self._unknown: if full_key in self._unknown:
@ -1579,6 +1587,8 @@ class PartialTxInput(TxInput, PSBTSection):
wr(PSBTInputType.FINAL_SCRIPTSIG, self.script_sig) wr(PSBTInputType.FINAL_SCRIPTSIG, self.script_sig)
if self.witness is not None: if self.witness is not None:
wr(PSBTInputType.FINAL_SCRIPTWITNESS, self.witness) wr(PSBTInputType.FINAL_SCRIPTWITNESS, self.witness)
if self.slip_19_ownership_proof:
wr(PSBTInputType.SLIP19_OWNERSHIP_PROOF, self.slip_19_ownership_proof)
for full_key, val in sorted(self._unknown.items()): for full_key, val in sorted(self._unknown.items()):
key_type, key = self.get_keytype_and_key_from_fullkey(full_key) key_type, key = self.get_keytype_and_key_from_fullkey(full_key)
wr(key_type, val, key=key) wr(key_type, val, key=key)

13
electrum/wallet.py

@ -2447,6 +2447,12 @@ class Abstract_Wallet(ABC, Logger, EventListener):
self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix) self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height
def has_support_for_slip_19_ownership_proofs(self) -> bool:
return False
def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:
raise NotImplementedError()
def get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]: def get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]:
if not self.is_mine(address): if not self.is_mine(address):
return None return None
@ -3765,6 +3771,13 @@ class Standard_Wallet(Simple_Wallet, Deterministic_Wallet):
pubkey = pubkeys[0] pubkey = pubkeys[0]
return bitcoin.pubkey_to_address(self.txin_type, pubkey) return bitcoin.pubkey_to_address(self.txin_type, pubkey)
def has_support_for_slip_19_ownership_proofs(self) -> bool:
return self.keystore.has_support_for_slip_19_ownership_proofs()
def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None:
tx.add_info_from_wallet(self)
self.keystore.add_slip_19_ownership_proofs_to_tx(tx=tx, password=None)
class Multisig_Wallet(Deterministic_Wallet): class Multisig_Wallet(Deterministic_Wallet):
# generic m of n # generic m of n

Loading…
Cancel
Save