wallet: move checks for {sign,verify,encrypt,decrypt}_message from UIs to wallet.py - #10790
Conversation
79d53a9 to
4264a98
Compare
4264a98 to
566937f
Compare
|
note: this is mostly boring clean-up and refactoring. It is in low-risk code, and code that I am familiar with. Even though the diff is large, unless someone proactively reviews it, I will self-merge to reduce review burden. |
I let the clanker have a look, mostly found some nits, sharing here fyi: See output (click to expand)1. Behaviour changes to the public CLI/RPC (undocumented)Same probe run against base and head:
➡️ All six deserve a 2. Defects2.1
|
| File | Now-unused |
|---|---|
commands.py |
binascii (L32), is_hex_str, to_bytes (L53) |
gui/qt/main_window.py |
electrum_ecc as ecc (L48), bfh (L57) |
CI won't catch these — the flake8 selection is F5,F6,F7,F8, so F401 is off — but all five are newly dead because of this diff.
3. Design
3.1 isinstance(self, …) in the base class replaces a working override — wallet.py:3286, 3290
The PR deletes Imported_Wallet.decrypt_message and re-implements it as if isinstance(self, Imported_Wallet) inside Abstract_Wallet, plus if isinstance(self, Multisig_Wallet) for the unsupported case. Abstract_Wallet now names two of its own subclasses — the one place the refactor moves away from cleaner structure.
Suggested: keep Imported_Wallet.decrypt_message as an override (or add _pubkey_to_addr_index() for subclasses), and express the multisig case as an overridable can_decrypt_message() / NotImplementedError on Multisig_Wallet.
3.2 verify_message / encrypt_message don't belong on Abstract_Wallet
Both are @classmethod but never touch cls (@staticmethod would at least be accurate), and neither needs a wallet — as the PR description says. The consequence: commands.encrypt, a @command('') with no wallet, must call Abstract_Wallet.encrypt_message(…), and qedaemon.py now imports electrum.wallet solely to reach a pure function.
Suggested: module-level functions in bitcoin.py / crypto.py; thin Abstract_Wallet wrappers can stay if call-site symmetry matters.
3.3 Type validation duplicated at two layers
commands.py adds 18 lines of isinstance → UserFacingException for four commands; wallet.py then re-asserts the same invariants. There's a defensible reading (RPC boundary = user error, wallet internals = programming error), but the same fact is stated twice in different dialects. Having the wallet methods raise UserFacingException on type errors and dropping the commands.py preamble is strictly less code, same behaviour.
3.4 The consolidation stops short of .strip()
Whitespace handling is still per-UI and still inconsistent:
| Caller | address | message | signature |
|---|---|---|---|
Qt (main_window.py:2165) |
strip | strip | no strip |
| QML | strip | strip | strip |
commands.py |
none | none | none |
A Qt user pasting a signature with a trailing newline gets "Wrong signature" (validate=True rejects it → False); the same paste works in QML. Given #10787 just landed fixing this asymmetry in the other direction, pulling .strip() into verify_message/sign_message looks like the natural completion.
3.5 Smaller consistency items
sign_messageusesbitcoin.is_address(3241);verify_messageuses bareis_address(3269). Adjacent new methods, both imported, two spellings.message: strreassigned tobytesinverify_message(3277) andencrypt_message(3307). Harmless at runtime, but the PR is explicitly about type hints — a separatemessage_byteslocal keeps them honest.- Translated and untranslated strings mixed on the same
UserFacingExceptionsurface:_("Invalid Bitcoin address.")next to the raw f-string at 3289. - Continuation lines at 3253-3255 are indented one column past their opening quote at 3252.
4. QML
4.1 Both new signals are unreachable through the only dialog that listens
SignVerifyMessageDialog.qml:203 gates Verify on enabled: _addressValid && signature.text, where _addressValid = bitcoin.isAddress(addressField.text) — and QEBitcoin.isAddress is the same is_address that verify_message tests before raising. The QML check runs on unstripped text while Python strips first, so the Python check is strictly more permissive: verifyMessageError can never fire from here.
Same on the sign side — visible: canSignMessage already excludes multisig and watching-only, enabled: _addressMine covers the third check, leaving only the txin-type case, which no supported wallet type reaches.
That's a new signal, a new import and ~20 lines of QML for paths the UI already prevents. Defensible as defense-in-depth, but worth deciding deliberately. If they ever do fire, the user gets both the red Invalid! field state and a modal error dialog.
4.2 Duplicated handlers
The two Connections blocks (L215-236) have byte-identical bodies. One local function showError(error) would do.
5. Qt
Moving do_verify / do_encrypt onto self.thread (99ab58d) buys uniform error routing through ElectrumWindow.on_error, which does handle UserFacingException cleanly via show_error — that part is correct. The cost: two sub-millisecond pure-CPU operations now queue on the shared wallet TaskThread, behind e.g. a hardware-wallet signing prompt. Low risk, but no upside for verify specifically.
6. Worth calling out as good
- Hoisting
msg_signout of theElectrumWindowclass body is a real i18n fix (probably unintentional): as a class attribute, the_()calls ran at import time, freezing translations before the language was selected. Now evaluated per-call. Same msgids, so existing.poentries survive the move (push_locale.pyglobselectrum/**/*.py). except ValueErrorinstead ofbinascii.Erroraroundb64decodeis a genuine bug fix with a correctly-explanatory comment — base raised an uncaughtValueErroron a non-ASCII signature.- Narrowing
except BaseExceptiontoexcept ecc.InvalidECPointExceptionis safe: inelectrum_ecc0.0.7 everyECPubkey(bytes)parse failure funnels through_x_and_y_from_pubkey_bytes, which raises exactly that. - Hardware wallets are safe from the new
is_watching_only()gate —Hardware_KeyStore.is_watching_only()hard-returnsFalse. Plugin-levelsign_message/decrypt_messageare keystore methods and untouched.
7. Testing
No tests added for any consolidated method. Existing coverage only exercises imported wallets, so untested:
- the entire new deterministic-wallet
decrypt_messagebranch (pubkeys_to_address→get_address_index→ "Pubkey unrelated") - multisig refusal
- watching-only refusals (sign and decrypt)
- the unicode-signature fix — the one change here that fixes a crash, and the most valuable regression test in the set
To make them homogeneous. Also note: window.on_error handles UserFacingException, which the wallet methods can now raise.
566937f to
92e938f
Compare
|
Thanks. I took a few of the llm suggestions in https://github.com/spesmilo/electrum/compare/566937f87888ec601ccf477fc4684fb2fd397dc0..92e938f4bf5340c2ba032bf19784b053fa4e3d90.
That's a bug on master I did not know about. Is now fixed. |
Previously we had
wallet.sign_messageandwallet.decrypt_message, and all preliminary checks had to be done by the caller. This meant Qt/qml/CLI all duplicated some subset of all desired checks.Now we have
wallet.sign_message,wallet.verify_message,wallet.decrypt_message,wallet.encrypt_messageall in wallet.py, and all checks are self-contained inside. (The two new methods are classmethods, as they don't require a wallet, and that's why they did not exist before)note: this is somewhat related to #10787 but I wanted to clean this code up before we decide to strip/not-strip whitespaces as it really should be un-duped.