electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit c3c64a37c26f965c89479923f3fc5f3ae2bf24c2
parent 8872e43f27d92f2ed090b76bd589c7159485e20c
Author: SomberNight <somber.night@protonmail.com>
Date:   Thu, 10 Dec 2020 17:06:28 +0100

keystore: ignore fingerprint for pubkeys in psbt, try to match all keys

Diffstat:
Melectrum/keystore.py | 28++++++++++++++++++----------
Melectrum/tests/test_wallet_vertical.py | 21+++++++++++++++++++++
2 files changed, 39 insertions(+), 10 deletions(-)

diff --git a/electrum/keystore.py b/electrum/keystore.py @@ -371,8 +371,9 @@ class MasterPublicKeyMixin(ABC): *, only_der_suffix=True, ) -> Union[Sequence[int], str, None]: + EXPECTED_DER_SUFFIX_LEN = 2 def test_der_suffix_against_pubkey(der_suffix: Sequence[int], pubkey: bytes) -> bool: - if len(der_suffix) != 2: + if len(der_suffix) != EXPECTED_DER_SUFFIX_LEN: return False try: if pubkey != self.derive_pubkey(*der_suffix): @@ -387,11 +388,11 @@ class MasterPublicKeyMixin(ABC): der_suffix = None full_path = None # 1. try fp against our root - my_root_fingerprint_hex = self.get_root_fingerprint() - my_der_prefix_str = self.get_derivation_prefix() - ks_der_prefix = convert_bip32_path_to_list_of_uint32(my_der_prefix_str) if my_der_prefix_str else None - if (my_root_fingerprint_hex is not None and ks_der_prefix is not None and - fp_found.hex() == my_root_fingerprint_hex): + ks_root_fingerprint_hex = self.get_root_fingerprint() + ks_der_prefix_str = self.get_derivation_prefix() + ks_der_prefix = convert_bip32_path_to_list_of_uint32(ks_der_prefix_str) if ks_der_prefix_str else None + if (ks_root_fingerprint_hex is not None and ks_der_prefix is not None and + fp_found.hex() == ks_root_fingerprint_hex): if path_found[:len(ks_der_prefix)] == ks_der_prefix: der_suffix = path_found[len(ks_der_prefix):] if not test_der_suffix_against_pubkey(der_suffix, pubkey): @@ -402,10 +403,17 @@ class MasterPublicKeyMixin(ABC): der_suffix = path_found if not test_der_suffix_against_pubkey(der_suffix, pubkey): der_suffix = None - # NOTE: problem: if we don't know our root fp, but tx contains root fp and full path, - # we will miss the pubkey (false negative match). Though it might still work - # within gap limit due to tx.add_info_from_wallet overwriting the fields. - # Example: keystore has intermediate xprv without root fp; tx contains root fp and full path. + # 3. hack/bruteforce: ignore fp and check pubkey anyway + # This is only to resolve the following scenario/problem: + # problem: if we don't know our root fp, but tx contains root fp and full path, + # we will miss the pubkey (false negative match). Though it might still work + # within gap limit due to tx.add_info_from_wallet overwriting the fields. + # Example: keystore has intermediate xprv without root fp; tx contains root fp and full path. + if der_suffix is None: + der_suffix = path_found[-EXPECTED_DER_SUFFIX_LEN:] + if not test_der_suffix_against_pubkey(der_suffix, pubkey): + der_suffix = None + # if all attempts/methods failed, we give up now: if der_suffix is None: return None if ks_der_prefix is not None: diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py @@ -2046,6 +2046,27 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid()) @mock.patch.object(wallet.Abstract_Wallet, 'save_db') + def test_signing_where_offline_ks_does_not_have_keyorigin_but_psbt_contains_it(self, mock_save_db): + # keystore has intermediate xprv without root fp; tx contains root fp and full path. + # tx has input with key beyond gap limit + wallet_offline = WalletIntegrityHelper.create_standard_wallet( + # bip39 seed: "brave scare company drastic consider confirm grow differ alter wide olympic utility" + # der: m/84'/1'/0' + keystore.from_xprv('vprv9KXDgRXYp3WCozCS3bMehASe2cJhY28DihCZ3KuyiTTjngopkfRC9QkH1SUREyCvnV7TSD6EgEHTTYa5yod7ZveBhVReEU1uDgfVASFqLNw'), + gap_limit=4, + config=self.config + ) + + tx = tx_from_any('70736274ff01005202000000017b748828553b1127b86674e71ad0cd4a2e5e8baeab8792a3c3263f7ea0ba86500000000000fdffffff01ad16010000000000160014d74b54300bc0d4b6e8f506fe540b47ce0da38b4a08f21c00000100bf0200000000010163a419b779be17167c54ff3acb1205e5347fbd72963f89fb1d66b5cf09f329c90000000000fdffffff011b17010000000000160014ed420532f0c33477b9b3fbb57431b4a1adce99c90247304402204e4ad4992fa8798e3b595d17c59961b905ca71c32dc3ba910ae14f139259ffbe02206ee2281f21499e46aa77f4bec2edce3674fea529d9dd340439365c2232bad35701210334080358ffdac08f83d6800a8e477e3512ad5c39ede553089db8c4bbe16f59aad7f11c00220602d137f257a96cbc58c7e60f2085cd65a311e242459e23d1efbed77dd8f372513818cc2bdaaa540000800100008000000080000000001e000000002202030671d324eeba0f85499a8749f783a4883103d23f5dedbe048391ff18c3da067818cc2bdaaa540000800100008000000080000000000100000000') + self.assertEqual('065b6e0a5731107641828337f5e000c9ddd94a12d074708643b0bca517374c6a', tx.txid()) + + # sign tx + tx = wallet_offline.sign_transaction(tx, password=None) + self.assertTrue(tx.is_complete()) + self.assertEqual('020000000001017b748828553b1127b86674e71ad0cd4a2e5e8baeab8792a3c3263f7ea0ba86500000000000fdffffff01ad16010000000000160014d74b54300bc0d4b6e8f506fe540b47ce0da38b4a0247304402203098741bf4d4f956e96f2706a517a1c0a63f67a242a50d155fbc56ad0bbac8b102207e535391c03bdab641f3205762311c1e6648b3459681e53d68fa44e63604a7f6012102d137f257a96cbc58c7e60f2085cd65a311e242459e23d1efbed77dd8f372513808f21c00', + str(tx)) + + @mock.patch.object(wallet.Abstract_Wallet, 'save_db') def test_sending_offline_wif_online_addr_p2pkh(self, mock_save_db): # compressed pubkey wallet_offline = WalletIntegrityHelper.create_imported_wallet(privkeys=True, config=self.config) wallet_offline.import_private_key('p2pkh:cQDxbmQfwRV3vP1mdnVHq37nJekHLsuD3wdSQseBRA2ct4MFk5Pq', password=None)