diff --git a/electrum/keystore.py b/electrum/keystore.py index e0419fb4f..9ccf2ab30 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -36,7 +36,9 @@ from .bitcoin import deserialize_privkey, serialize_privkey, BaseDecodeError from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, TxInput from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME, is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation, - convert_bip32_intpath_to_strpath, is_xkey_consistent_with_key_origin_info) + convert_bip32_intpath_to_strpath, is_xkey_consistent_with_key_origin_info, + KeyOriginInfo) +from .descriptor import PubkeyProvider from .ecc import string_to_number from .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST, SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160, @@ -179,6 +181,10 @@ class KeyStore(Logger, ABC): """ pass + @abstractmethod + def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]: + pass + def find_my_pubkey_in_txinout( self, txinout: Union['PartialTxInput', 'PartialTxOutput'], *, only_der_suffix: bool = False @@ -302,6 +308,15 @@ class Imported_KeyStore(Software_KeyStore): return pubkey.hex() return None + def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]: + if sequence in self.keypairs: + return PubkeyProvider( + origin=None, + pubkey=sequence, + deriv_path=None, + ) + return None + def update_password(self, old_password, new_password): self.check_password(old_password) if new_password == '': @@ -403,6 +418,9 @@ class MasterPublicKeyMixin(ABC): """ pass + def get_key_origin_info(self) -> Optional[KeyOriginInfo]: + return None + @abstractmethod def derive_pubkey(self, for_change: int, n: int) -> bytes: """Returns pubkey at given path. @@ -532,6 +550,22 @@ class Xpub(MasterPublicKeyMixin): ) return bip32node.to_xpub() + def get_key_origin_info(self) -> Optional[KeyOriginInfo]: + fp_bytes, der_full = self.get_fp_and_derivation_to_be_used_in_partial_tx( + der_suffix=[], only_der_suffix=False) + origin = KeyOriginInfo(fingerprint=fp_bytes, path=der_full) + return origin + + def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]: + strpath = convert_bip32_intpath_to_strpath(sequence) + strpath = strpath[1:] # cut leading "m" + bip32node = self.get_bip32_node_for_xpub() + return PubkeyProvider( + origin=self.get_key_origin_info(), + pubkey=bip32node._replace(xtype="standard").to_xkey(), + deriv_path=strpath, + ) + def add_key_origin_from_root_node(self, *, derivation_prefix: str, root_node: BIP32Node): assert self.xpub # try to derive ourselves from what we were given @@ -802,6 +836,13 @@ class Old_KeyStore(MasterPublicKeyMixin, Deterministic_KeyStore): der_full = der_prefix_ints + list(der_suffix) return fingerprint_bytes, der_full + def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]: + return PubkeyProvider( + origin=None, + pubkey=self.derive_pubkey(*sequence).hex(), + deriv_path=None, + ) + def update_password(self, old_password, new_password): self.check_password(old_password) if new_password == '': diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py index 9c4aa8e53..dac6b64d7 100644 --- a/electrum/tests/test_wallet_vertical.py +++ b/electrum/tests/test_wallet_vertical.py @@ -821,7 +821,11 @@ class TestWalletSending(ElectrumTestCase): # wallet2 -> wallet1 outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)] - tx = wallet2.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False) + tx = wallet2.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False, sign=False) + self.assertEqual( + "pkh(045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25ed)", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) + wallet2.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) @@ -893,6 +897,9 @@ class TestWalletSending(ElectrumTestCase): outputs = [PartialTxOutput.from_address_and_value(wallet2a.get_receiving_address(), 165000)] tx = wallet1a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False, sign=False) self.assertEqual((0, 2), tx.signature_count()) + self.assertEqual( + "wsh(sortedmulti(2,[b2e35a7d/1h]tpubD9aPYLPPYw8MxU3cD57LwpV5v7GomHxdv62MSbPcRkp47zwXx69ACUFsKrj8xzuzRrij9FWVhfvkvNqtqsr8ZtefkDsGZ9GLuHzoS6bXyk1/0/0,[53b77ddb/1h]tpubD8spLJysN7v7V1KHvkZ7AwjnXShKafopi7Vu3Ahs2S46FxBPTode8DgGxDo55k4pJvETGScZFwnM5f2Y31EUjteJdhxR73sjr9ieydgah2U/0/0,[43067d63/1h]tpubD8khd1g1tzFeKeaU59QV811hyvhwn9KDfy5sqFJ5m2wJLw6rUt4AZviqutRPXTUAK4SpU2we3y2WBP916Ma8Em4qFGcbYkFvXVfpGYV3oZR/0/0))", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) wallet1a.sign_transaction(tx, password=None) self.assertEqual((1, 2), tx.signature_count()) txid = tx.txid() @@ -925,6 +932,9 @@ class TestWalletSending(ElectrumTestCase): outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)] tx = wallet2a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False) self.assertEqual((1, 2), tx.signature_count()) + self.assertEqual( + "sh(wsh(sortedmulti(2,[d1dbcc21]tpubDDsv4RpsGViZeEVwivuj3aaKhFQSv1kYsz64mwRoHkqBfw8qBSYEmc8TtyVGotJb44V3pviGzefP9m9hidRg9dPPaDWL2yoRpMW3hdje3Rk/0/0,[17cea914]tpubDCZU2kACPGACYDvAXvZUXQ7cE7msFfCtpah5QCuaz8iarKMLTgR4c2u8RGKdFhbb3YJxzmktDd1rCtF58ksyVgFw28pchY55uwkDiXjY9hU/0/0)))", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) txid = tx.txid() partial_tx = tx.serialize_as_bytes().hex() self.assertEqual("70736274ff01007e010000000149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e0100000000feffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a086010000000000220020f7b6b30c3073ae2680a7e90c589bbfec5303331be68bbab843eed5d51ba012390000000000010120888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870100fd7c0101000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000220202119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb14730440220091ea67af7c1131f51f62fe9596dff0a60c8b45bfc5be675389e193912e8a71802201bf813bbf83933a35ecc46e2d5b0442bd8758fa82e0f8ed16392c10d51f7f7660101042200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163c010547522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae220602119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb10cd1dbcc210000000000000000220602fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab81260c17cea9140000000000000000000100220020717ab7037b81797cb3e192a8a1b4d88083444bbfcd26934cadf3bcf890f14e05010147522102987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde21034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f9952ae220202987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde0c17cea91401000000000000002202034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f990cd1dbcc2101000000000000000000", @@ -2613,6 +2623,9 @@ class TestWalletSending(ElectrumTestCase): tx.version = 2 tx.locktime = 2378363 self.assertEqual("04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035", tx.txid()) + self.assertEqual( + "wsh(sortedmulti(2,[9559fbd1/9999h]tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK/0/0,[015148ee]tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg/0/0))", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) self.assertEqual({}, tx.to_json()['xpubs']) self.assertEqual( {'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999h/0/0"), @@ -2640,6 +2653,9 @@ class TestWalletSending(ElectrumTestCase): tx.version = 2 tx.locktime = 2378363 self.assertEqual("04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035", tx.txid()) + self.assertEqual( + "wsh(sortedmulti(2,[9559fbd1/9999h]tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK/0/0,[30cf1be5/48h/1h/0h/2h]tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg/0/0))", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) self.assertEqual({}, tx.to_json()['xpubs']) self.assertEqual( {'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999h/0/0"), @@ -3036,6 +3052,9 @@ class TestWalletOfflineSigning(ElectrumTestCase): # sign tx tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertEqual( + "sh(wpkh(03845818239fe468a9e7c7ae1a3d3653a8333f89ff316a771a3acf6854b4d8c6db))", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual('7642816d051aa3b333b6564bb6e44fe3a5885bfe7db9860dfbc9973a5c9a6562', tx.txid()) @@ -3114,6 +3133,9 @@ class TestWalletOfflineSigning(ElectrumTestCase): # sign tx tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertEqual( + "pkh([233d2ae4]tpubDDMN69wQjDZxaJz9afZQGa48hZS7X5oSegF2hg67yddNvqfpuTN9DqvDEp7YyVf7AzXnqBqHdLhzTAStHvsoMDDb8WoJQzNrcHgDJHVYgQF/0/1)", + tx.inputs()[0].script_descriptor.to_string_no_checksum()) self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.txid()) diff --git a/electrum/wallet.py b/electrum/wallet.py index 6d0270fef..c1e0198e2 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -686,14 +686,14 @@ class Abstract_Wallet(ABC, Logger, EventListener): pass def get_redeem_script(self, address: str) -> Optional[str]: - desc = self._get_script_descriptor_for_address(address) + desc = self.get_script_descriptor_for_address(address) if desc is None: return None redeem_script = desc.expand().redeem_script if redeem_script: return redeem_script.hex() def get_witness_script(self, address: str) -> Optional[str]: - desc = self._get_script_descriptor_for_address(address) + desc = self.get_script_descriptor_for_address(address) if desc is None: return None witness_script = desc.expand().witness_script if witness_script: @@ -2196,32 +2196,38 @@ class Abstract_Wallet(ABC, Logger, EventListener): if self.lnworker: self.lnworker.swap_manager.add_txin_info(txin) return - txin.script_descriptor = self._get_script_descriptor_for_address(address) + txin.script_descriptor = self.get_script_descriptor_for_address(address) 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 - 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): return None script_type = self.get_txin_type(address) if script_type in ('address', 'unknown'): return None - if script_type in ('p2pk', 'p2pkh', 'p2wpkh-p2sh', 'p2wpkh'): - pubkey = self.get_public_keys(address)[0] - return descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=script_type) + addr_index = self.get_address_index(address) + if addr_index is None: + return None + pubkeys = [ks.get_pubkey_provider(addr_index) for ks in self.get_keystores()] + if not pubkeys: + return None + if script_type == 'p2pk': + return descriptor.PKDescriptor(pubkey=pubkeys[0]) + elif script_type == 'p2pkh': + return descriptor.PKHDescriptor(pubkey=pubkeys[0]) + elif script_type == 'p2wpkh': + return descriptor.WPKHDescriptor(pubkey=pubkeys[0]) + elif script_type == 'p2wpkh-p2sh': + wpkh = descriptor.WPKHDescriptor(pubkey=pubkeys[0]) + return descriptor.SHDescriptor(subdescriptor=wpkh) elif script_type == 'p2sh': - pubkeys = self.get_public_keys(address) - pubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys] multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True) return descriptor.SHDescriptor(subdescriptor=multi) elif script_type == 'p2wsh': - pubkeys = self.get_public_keys(address) - pubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys] multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True) return descriptor.WSHDescriptor(subdescriptor=multi) elif script_type == 'p2wsh-p2sh': - pubkeys = self.get_public_keys(address) - pubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys] multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True) wsh = descriptor.WSHDescriptor(subdescriptor=multi) return descriptor.SHDescriptor(subdescriptor=wsh) @@ -2279,7 +2285,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address) if not is_mine: return - txout.script_descriptor = self._get_script_descriptor_for_address(address) + txout.script_descriptor = self.get_script_descriptor_for_address(address) txout.is_mine = True txout.is_change = self.is_change(address) self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix) @@ -3390,7 +3396,7 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet): return pubkeys[0] def load_keystore(self): - self.keystore = load_keystore(self.db, 'keystore') + self.keystore = load_keystore(self.db, 'keystore') # type: KeyStoreWithMPK try: xtype = bip32.xpub_type(self.keystore.xpub) except: