././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741351045.8523705 ledger_bitcoin-0.4.0/0000775000175000017500000000000014762564206014360 5ustar00singalasingala././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/LICENSE0000664000175000017500000002613514506542032015361 0ustar00singalasingala Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741351045.8523705 ledger_bitcoin-0.4.0/PKG-INFO0000644000175000017500000001350714762564206015461 0ustar00singalasingalaMetadata-Version: 2.2 Name: ledger_bitcoin Version: 0.4.0 Summary: Client for Ledger Nano Bitcoin application Home-page: https://github.com/LedgerHQ/app-bitcoin-new Author: Ledger Author-email: hello@ledger.fr Project-URL: Bug Tracker, https://github.com/LedgerHQ/app-bitcoin-new/issues Classifier: Programming Language :: Python :: 3 Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Requires-Python: >=3.7 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: typing-extensions>=3.7 Requires-Dist: ledgercomm>=1.1.0 Requires-Dist: packaging>=21.3 Provides-Extra: hid Requires-Dist: hidapi>=0.9.0.post3; extra == "hid" # Ledger Bitcoin application client ## Overview Client library for Ledger Bitcoin application. Main repository and documentation: https://github.com/LedgerHQ/app-bitcoin-new ## Install If you just want to communicate through TCP socket (for example with the Speculos emulator), there is no dependency: ```bash $ pip install ledger_bitcoin ``` otherwise, [hidapi](https://github.com/trezor/cython-hidapi) must be installed as an extra dependency: ```bash $ pip install ledger_bitcoin[hid] ``` ## Getting started The main method exported by the library is `createClient`, which queries the hardware wallet for the version of the running app, and then returns the appropriate implementation of the `Client` class. See the documentation of the class and the example below for the supported methods. When running on a legacy version of the app (below version `2.0.0`), only the features that were available on the app are supported. Any unsopported method (e.g.: multisig registration or addresses, taproot addresses) will raise a `NotImplementedError`. ### Running with speculos It is possible to run the app and the library with the [speculos](https://github.com/LedgerHQ/speculos) emulator. ⚠️ Currently, speculos does not correctly emulate the version of the app, always returning a dummy value; in order to use the library, it is necessary to set the `SPECULOS_APPNAME` environment variable before starting speculos, for example with: ``` $ export SPECULOS_APPNAME="Bitcoin Test:2.1.0" ``` Similarly, to test the library behavior on a legacy version of the app, one can set the version to `1.6.5` (the final version of the 1.X series). The expected application name is `Bitcoin` for mainnet, `Bitcoin Test` for testnet. ### Example The following example showcases all the main methods of the `Client`'s interface. If you are not using the context manager syntax when creating the client, remember to call the `stop()` method to release the communication channel. Testing the `sign_psbt` method requires producing a valid PSBT (with any external tool that supports either PSBTv0 or PSBTv2), and provide the corresponding wallet policy; it is skipped by default in the following example. ```python from typing import Optional from ledger_bitcoin import createClient, Chain, MultisigWallet, MultisigWallet, WalletPolicy, AddressType, TransportClient from ledger_bitcoin.psbt import PSBT def main(): # speculos on default host/port # with createClient(TransportClient(), chain=Chain.TEST) as client: # Ledger Nano connected via USB with createClient(chain=Chain.TEST) as client: # ==> Get the master key fingerprint fpr = client.get_master_fingerprint().hex() print(f"Master key fingerprint: {fpr}") # ==> Get and display on screen the first taproot address first_taproot_account_pubkey = client.get_extended_pubkey("m/86'/1'/0'") first_taproot_account_policy = WalletPolicy( "", "tr(@0/**)", [ f"[{fpr}/86'/1'/0']{first_taproot_account_pubkey}" ], ) first_taproot_account_address = client.get_wallet_address( first_taproot_account_policy, None, change=0, address_index=0, display=True # show address on the wallet's screen ) print(f"First taproot account receive address: {first_taproot_account_address}") # ==> Register a multisig wallet named "Cold storage" our_pubkey = client.get_extended_pubkey("m/48'/1'/0'/2'") other_key_info = "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF" multisig_policy = MultisigWallet( name="Cold storage", address_type=AddressType.WIT, threshold=2, keys_info=[ other_key_info, # some other bitcoiner f"[{fpr}/48'/1'/0'/2']{our_pubkey}", # that's us ], ) policy_id, policy_hmac = client.register_wallet(multisig_policy) print(f"Policy hmac: {policy_hmac.hex()}. Store it safely (together with the policy).") assert policy_id == multisig_policy.id # should never fail # ==> Derive and show an address for "Cold storage" multisig_address = client.get_wallet_address(multisig_policy, policy_hmac, change=0, address_index=0, display=True) print(f"Multisig wallet address: {multisig_address}") # ==> Sign a psbt # TODO: set a wallet policy and a valid psbt file in order to test psbt signing psbt_filename: Optional[str] = None signing_policy: Optional[WalletPolicy] = None signing_policy_hmac: Optional[bytes] = None if not psbt_filename or not signing_policy: print("Nothing to sign :(") return raw_psbt_base64 = open(psbt_filename, "r").read() psbt = PSBT() psbt.deserialize(raw_psbt_base64) result = client.sign_psbt(psbt, signing_policy, signing_policy_hmac) print("Returned signatures:") print(result) if __name__ == "__main__": main() ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/README.md0000664000175000017500000001220614623570367015641 0ustar00singalasingala# Ledger Bitcoin application client ## Overview Client library for Ledger Bitcoin application. Main repository and documentation: https://github.com/LedgerHQ/app-bitcoin-new ## Install If you just want to communicate through TCP socket (for example with the Speculos emulator), there is no dependency: ```bash $ pip install ledger_bitcoin ``` otherwise, [hidapi](https://github.com/trezor/cython-hidapi) must be installed as an extra dependency: ```bash $ pip install ledger_bitcoin[hid] ``` ## Getting started The main method exported by the library is `createClient`, which queries the hardware wallet for the version of the running app, and then returns the appropriate implementation of the `Client` class. See the documentation of the class and the example below for the supported methods. When running on a legacy version of the app (below version `2.0.0`), only the features that were available on the app are supported. Any unsopported method (e.g.: multisig registration or addresses, taproot addresses) will raise a `NotImplementedError`. ### Running with speculos It is possible to run the app and the library with the [speculos](https://github.com/LedgerHQ/speculos) emulator. ⚠️ Currently, speculos does not correctly emulate the version of the app, always returning a dummy value; in order to use the library, it is necessary to set the `SPECULOS_APPNAME` environment variable before starting speculos, for example with: ``` $ export SPECULOS_APPNAME="Bitcoin Test:2.1.0" ``` Similarly, to test the library behavior on a legacy version of the app, one can set the version to `1.6.5` (the final version of the 1.X series). The expected application name is `Bitcoin` for mainnet, `Bitcoin Test` for testnet. ### Example The following example showcases all the main methods of the `Client`'s interface. If you are not using the context manager syntax when creating the client, remember to call the `stop()` method to release the communication channel. Testing the `sign_psbt` method requires producing a valid PSBT (with any external tool that supports either PSBTv0 or PSBTv2), and provide the corresponding wallet policy; it is skipped by default in the following example. ```python from typing import Optional from ledger_bitcoin import createClient, Chain, MultisigWallet, MultisigWallet, WalletPolicy, AddressType, TransportClient from ledger_bitcoin.psbt import PSBT def main(): # speculos on default host/port # with createClient(TransportClient(), chain=Chain.TEST) as client: # Ledger Nano connected via USB with createClient(chain=Chain.TEST) as client: # ==> Get the master key fingerprint fpr = client.get_master_fingerprint().hex() print(f"Master key fingerprint: {fpr}") # ==> Get and display on screen the first taproot address first_taproot_account_pubkey = client.get_extended_pubkey("m/86'/1'/0'") first_taproot_account_policy = WalletPolicy( "", "tr(@0/**)", [ f"[{fpr}/86'/1'/0']{first_taproot_account_pubkey}" ], ) first_taproot_account_address = client.get_wallet_address( first_taproot_account_policy, None, change=0, address_index=0, display=True # show address on the wallet's screen ) print(f"First taproot account receive address: {first_taproot_account_address}") # ==> Register a multisig wallet named "Cold storage" our_pubkey = client.get_extended_pubkey("m/48'/1'/0'/2'") other_key_info = "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF" multisig_policy = MultisigWallet( name="Cold storage", address_type=AddressType.WIT, threshold=2, keys_info=[ other_key_info, # some other bitcoiner f"[{fpr}/48'/1'/0'/2']{our_pubkey}", # that's us ], ) policy_id, policy_hmac = client.register_wallet(multisig_policy) print(f"Policy hmac: {policy_hmac.hex()}. Store it safely (together with the policy).") assert policy_id == multisig_policy.id # should never fail # ==> Derive and show an address for "Cold storage" multisig_address = client.get_wallet_address(multisig_policy, policy_hmac, change=0, address_index=0, display=True) print(f"Multisig wallet address: {multisig_address}") # ==> Sign a psbt # TODO: set a wallet policy and a valid psbt file in order to test psbt signing psbt_filename: Optional[str] = None signing_policy: Optional[WalletPolicy] = None signing_policy_hmac: Optional[bytes] = None if not psbt_filename or not signing_policy: print("Nothing to sign :(") return raw_psbt_base64 = open(psbt_filename, "r").read() psbt = PSBT() psbt.deserialize(raw_psbt_base64) result = client.sign_psbt(psbt, signing_policy, signing_policy_hmac) print("Returned signatures:") print(result) if __name__ == "__main__": main() ```././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741351045.8483703 ledger_bitcoin-0.4.0/ledger_bitcoin/0000775000175000017500000000000014762564206017331 5ustar00singalasingala././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741350787.0 ledger_bitcoin-0.4.0/ledger_bitcoin/__init__.py0000664000175000017500000000111614762563603021441 0ustar00singalasingala """Ledger Nano Bitcoin app client""" from .client_base import Client, TransportClient, PartialSignature, MusigPubNonce, MusigPartialSignature, SignPsbtYieldedObject from .client import createClient from .common import Chain from .wallet import AddressType, WalletPolicy, MultisigWallet, WalletType __version__ = '0.4.0' __all__ = [ "Client", "TransportClient", "PartialSignature", "MusigPubNonce", "MusigPartialSignature", "SignPsbtYieldedObject", "createClient", "Chain", "AddressType", "WalletPolicy", "MultisigWallet", "WalletType" ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/_base58.py0000664000175000017500000000772314506542032021127 0ustar00singalasingala""" Base 58 conversion utilities **************************** """ # # base58.py # # Revision here from https://github.com/bitcoin-core/HWI/blob/3fe369d0379212fae1c72729a179d133b0adc872/hwilib/_base58.py # # Original source: git://github.com/joric/brutus.git # which was forked from git://github.com/samrushing/caesure.git # # Distributed under the MIT/X11 software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # from binascii import hexlify, unhexlify from typing import List from .common import hash256 from .errors import BadArgumentError b58_digits: str = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' def encode(b: bytes) -> str: """ Encode bytes to a base58-encoded string :param b: Bytes to encode :return: Base58 encoded string of ``b`` """ # Convert big-endian bytes to integer n: int = int('0x0' + hexlify(b).decode('utf8'), 16) # Divide that integer into base58 temp: List[str] = [] while n > 0: n, r = divmod(n, 58) temp.append(b58_digits[r]) res: str = ''.join(temp[::-1]) # Encode leading zeros as base58 zeros czero: int = 0 pad: int = 0 for c in b: if c == czero: pad += 1 else: break return b58_digits[0] * pad + res def decode(s: str) -> bytes: """ Decode a base58-encoding string, returning bytes :param s: Base48 string to decode :return: Bytes encoded by ``s`` """ if not s: return b'' # Convert the string to an integer n: int = 0 for c in s: n *= 58 if c not in b58_digits: raise BadArgumentError('Character %r is not a valid base58 character' % c) digit = b58_digits.index(c) n += digit # Convert the integer to bytes h: str = '%x' % n if len(h) % 2: h = '0' + h res = unhexlify(h.encode('utf8')) # Add padding back. pad = 0 for c in s[:-1]: if c == b58_digits[0]: pad += 1 else: break return b'\x00' * pad + res def get_xpub_fingerprint(s: str) -> bytes: """ Get the parent fingerprint from an extended public key :param s: The extended pubkey :return: The parent fingerprint bytes """ data = decode(s) fingerprint = data[5:9] return fingerprint def get_xpub_fingerprint_hex(xpub: str) -> str: """ Get the parent fingerprint as a hex string from an extended public key :param s: The extended pubkey :return: The parent fingerprint as a hex string """ data = decode(xpub) fingerprint = data[5:9] return hexlify(fingerprint).decode() def to_address(b: bytes, version: bytes) -> str: """ Base58 Check Encode the data with the version number. Used to encode legacy style addresses. :param b: The data to encode :param version: The version number to encode with :return: The Base58 Check Encoded string """ data = version + b checksum = hash256(data)[0:4] data += checksum return encode(data) def xpub_to_pub_hex(xpub: str) -> str: """ Get the public key as a string from the extended public key. :param xpub: The extended pubkey :return: The pubkey hex string """ data = decode(xpub) pubkey = data[-37:-4] return hexlify(pubkey).decode() def xpub_to_xonly_pub_hex(xpub: str) -> str: """ Get the public key as a string from the extended public key. :param xpub: The extended pubkey :return: The pubkey hex string """ data = decode(xpub) pubkey = data[-36:-4] return hexlify(pubkey).decode() def xpub_main_2_test(xpub: str) -> str: """ Convert an extended pubkey from mainnet version to testnet version. :param xpub: The extended pubkey :return: The extended pubkey re-encoded using testnet version bytes """ data = decode(xpub) test_data = b'\x04\x35\x87\xCF' + data[4:-4] checksum = hash256(test_data)[0:4] return encode(test_data + checksum) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/_script.py0000664000175000017500000001025114506542032021332 0ustar00singalasingala""" Original version: https://raw.githubusercontent.com/bitcoin-core/HWI/3fe369d0379212fae1c72729a179d133b0adc872/hwilib/_script.py Distributed under the MIT License. Bitcoin Script utilities ************************ """ from typing import ( Optional, Sequence, Tuple, ) def is_opreturn(script: bytes) -> bool: """ Determine whether a script is an OP_RETURN output script. :param script: The script :returns: Whether the script is an OP_RETURN output script """ return script[0] == 0x6a def is_p2sh(script: bytes) -> bool: """ Determine whether a script is a P2SH output script. :param script: The script :returns: Whether the script is a P2SH output script """ return len(script) == 23 and script[0] == 0xa9 and script[1] == 0x14 and script[22] == 0x87 def is_p2pkh(script: bytes) -> bool: """ Determine whether a script is a P2PKH output script. :param script: The script :returns: Whether the script is a P2PKH output script """ return len(script) == 25 and script[0] == 0x76 and script[1] == 0xa9 and script[2] == 0x14 and script[23] == 0x88 and script[24] == 0xac def is_p2pk(script: bytes) -> bool: """ Determine whether a script is a P2PK output script. :param script: The script :returns: Whether the script is a P2PK output script """ return (len(script) == 35 or len(script) == 67) and (script[0] == 0x21 or script[0] == 0x41) and script[-1] == 0xac def is_p2tr(script: bytes) -> bool: """ Determine whether a script is a P2TR output script. :param script: The script :returns: Whether the script is a P2TR output script """ return len(script) == 34 and script[0] == 0x51 and script[1] == 0x20 def is_witness(script: bytes) -> Tuple[bool, int, bytes]: """ Determine whether a script is a segwit output script. If so, also returns the witness version and witness program. :param script: The script :returns: A tuple of a bool indicating whether the script is a segwit output script, an int representing the witness version, and the bytes of the witness program. """ if len(script) < 4 or len(script) > 42: return (False, 0, b"") if script[0] != 0 and (script[0] < 81 or script[0] > 96): return (False, 0, b"") if script[1] + 2 == len(script): return (True, script[0] - 0x50 if script[0] else 0, script[2:]) return (False, 0, b"") def is_p2wpkh(script: bytes) -> bool: """ Determine whether a script is a P2WPKH output script. :param script: The script :returns: Whether the script is a P2WPKH output script """ is_wit, wit_ver, wit_prog = is_witness(script) if not is_wit: return False elif wit_ver != 0: return False return len(wit_prog) == 20 def is_p2wsh(script: bytes) -> bool: """ Determine whether a script is a P2WSH output script. :param script: The script :returns: Whether the script is a P2WSH output script """ is_wit, wit_ver, wit_prog = is_witness(script) if not is_wit: return False elif wit_ver != 0: return False return len(wit_prog) == 32 # Only handles up to 15 of 15. Returns None if this script is not a # multisig script. Returns (m, pubkeys) otherwise. def parse_multisig(script: bytes) -> Optional[Tuple[int, Sequence[bytes]]]: """ Determine whether a script is a multisig script. If so, determine the parameters of that multisig. :param script: The script :returns: ``None`` if the script is not multisig. If multisig, returns a tuple of the number of signers required, and a sequence of public key bytes. """ # Get m m = script[0] - 80 if m < 1 or m > 15: return None # Get pubkeys pubkeys = [] offset = 1 while True: pubkey_len = script[offset] if pubkey_len != 33: break offset += 1 pubkeys.append(script[offset:offset + 33]) offset += 33 # Check things at the end n = script[offset] - 80 if n != len(pubkeys): return None offset += 1 op_cms = script[offset] if op_cms != 174: return None return (m, pubkeys) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/_serialize.py0000664000175000017500000001500514506542032022017 0ustar00singalasingala# Original version: https://raw.githubusercontent.com/bitcoin-core/HWI/3fe369d0379212fae1c72729a179d133b0adc872/hwilib/_serialize.py # #!/usr/bin/env python3 # Copyright (c) 2010 ArtForz -- public domain half-a-node # Copyright (c) 2012 Jeff Garzik # Copyright (c) 2010-2016 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Bitcoin Object Python Serializations ************************************ Modified from the test/test_framework/mininode.py file from the Bitcoin repository """ import struct from typing import ( List, Sequence, TypeVar, Callable, ) from typing_extensions import Protocol class Readable(Protocol): def read(self, n: int = -1) -> bytes: ... class Deserializable(Protocol): def deserialize(self, f: Readable) -> None: ... class Serializable(Protocol): def serialize(self) -> bytes: ... # Serialization/deserialization tools def ser_compact_size(size: int) -> bytes: """ Serialize an integer using Bitcoin's compact size unsigned integer serialization. :param size: The int to serialize :returns: The int serialized as a compact size unsigned integer """ r = b"" if size < 253: r = struct.pack("B", size) elif size < 0x10000: r = struct.pack(" int: """ Deserialize a compact size unsigned integer from the beginning of the byte stream. :param f: The byte stream :returns: The integer that was serialized """ nit: int = struct.unpack(" bytes: """ Deserialize a variable length byte string serialized with Bitcoin's variable length string serialization from a byte stream. :param f: The byte stream :returns: The byte string that was serialized """ nit = deser_compact_size(f) return f.read(nit) def ser_string(s: bytes) -> bytes: """ Serialize a byte string with Bitcoin's variable length string serialization. :param s: The byte string to be serialized :returns: The serialized byte string """ return ser_compact_size(len(s)) + s def deser_uint256(f: Readable) -> int: """ Deserialize a 256 bit integer serialized with Bitcoin's 256 bit integer serialization from a byte stream. :param f: The byte stream. :returns: The integer that was serialized """ r = 0 for i in range(8): t = struct.unpack(" bytes: """ Serialize a 256 bit integer with Bitcoin's 256 bit integer serialization. :param u: The integer to serialize :returns: The serialized 256 bit integer """ rs = b"" for _ in range(8): rs += struct.pack(">= 32 return rs def uint256_from_str(s: bytes) -> int: """ Deserialize a 256 bit integer serialized with Bitcoin's 256 bit integer serialization from a byte string. :param s: The byte string :returns: The integer that was serialized """ r = 0 t = struct.unpack(" List[D]: """ Deserialize a vector of objects with Bitcoin's object vector serialization from a byte stream. :param f: The byte stream :param c: The class of object to deserialize for each object in the vector :returns: A list of objects that were serialized """ nit = deser_compact_size(f) r = [] for _ in range(nit): t = c() t.deserialize(f) r.append(t) return r def ser_vector(v: Sequence[Serializable]) -> bytes: """ Serialize a vector of objects with Bitcoin's object vector serialzation. :param v: The list of objects to serialize :returns: The serialized objects """ r = ser_compact_size(len(v)) for i in v: r += i.serialize() return r def deser_string_vector(f: Readable) -> List[bytes]: """ Deserialize a vector of byte strings from a byte stream. :param f: The byte stream :returns: The list of byte strings that were serialized """ nit = deser_compact_size(f) r = [] for _ in range(nit): t = deser_string(f) r.append(t) return r def ser_string_vector(v: List[bytes]) -> bytes: """ Serialize a list of byte strings as a vector of byte strings. :param v: The list of byte strings to serialize :returns: The serialized list of byte strings """ r = ser_compact_size(len(v)) for sv in v: r += ser_string(sv) return r def ser_sig_der(r: bytes, s: bytes) -> bytes: """ Serialize the ``r`` and ``s`` values of an ECDSA signature using DER. :param r: The ``r`` value bytes :param s: The ``s`` value bytes :returns: The DER encoded signature """ sig = b"\x30" # Make r and s as short as possible ri = 0 for b in r: if b == 0: ri += 1 else: break r = r[ri:] si = 0 for b in s: if b == 0: si += 1 else: break s = s[si:] # Make positive of neg first = r[0] if first & (1 << 7) != 0: r = b"\x00" + r first = s[0] if first & (1 << 7) != 0: s = b"\x00" + s # Write total length total_len = len(r) + len(s) + 4 sig += struct.pack("B", total_len) # write r sig += b"\x02" sig += struct.pack("B", len(r)) sig += r # write s sig += b"\x02" sig += struct.pack("B", len(s)) sig += s sig += b"\x01" return sig def ser_sig_compact(r: bytes, s: bytes, recid: bytes) -> bytes: """ Serialize the ``r`` and ``s`` values of an ECDSA signature using the compact signature serialization scheme. :param r: The ``r`` value bytes :param s: The ``s`` value bytes :returns: The compact signature """ rec = struct.unpack("B", recid)[0] prefix = struct.pack("B", 27 + 4 + rec) sig = b"" sig += prefix sig += r + s return sig ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733838905.0 ledger_bitcoin-0.4.0/ledger_bitcoin/bip0327.py0000664000175000017500000001337414726044071020772 0ustar00singalasingala# extracted from the BIP327 reference implementation: https://github.com/bitcoin/bips/blob/b3701faef2bdb98a0d7ace4eedbeefa2da4c89ed/bip-0327/reference.py # Only contains the key aggregation part of the library # The code in this source file is distributed under the BSD-3-Clause. # autopep8: off from typing import List, Optional, Tuple, NewType, NamedTuple import hashlib # # The following helper functions were copied from the BIP-340 reference implementation: # https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py # p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 # Points are tuples of X and Y coordinates and the point at infinity is # represented by the None keyword. G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) Point = Tuple[int, int] # This implementation can be sped up by storing the midstate after hashing # tag_hash instead of rehashing it all the time. def tagged_hash(tag: str, msg: bytes) -> bytes: tag_hash = hashlib.sha256(tag.encode()).digest() return hashlib.sha256(tag_hash + tag_hash + msg).digest() def is_infinite(P: Optional[Point]) -> bool: return P is None def x(P: Point) -> int: assert not is_infinite(P) return P[0] def y(P: Point) -> int: assert not is_infinite(P) return P[1] def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]: if P1 is None: return P2 if P2 is None: return P1 if (x(P1) == x(P2)) and (y(P1) != y(P2)): return None if P1 == P2: lam = (3 * x(P1) * x(P1) * pow(2 * y(P1), p - 2, p)) % p else: lam = ((y(P2) - y(P1)) * pow(x(P2) - x(P1), p - 2, p)) % p x3 = (lam * lam - x(P1) - x(P2)) % p return (x3, (lam * (x(P1) - x3) - y(P1)) % p) def point_mul(P: Optional[Point], n: int) -> Optional[Point]: R = None for i in range(256): if (n >> i) & 1: R = point_add(R, P) P = point_add(P, P) return R def bytes_from_int(x: int) -> bytes: return x.to_bytes(32, byteorder="big") def lift_x(b: bytes) -> Optional[Point]: x = int_from_bytes(b) if x >= p: return None y_sq = (pow(x, 3, p) + 7) % p y = pow(y_sq, (p + 1) // 4, p) if pow(y, 2, p) != y_sq: return None return (x, y if y & 1 == 0 else p-y) def int_from_bytes(b: bytes) -> int: return int.from_bytes(b, byteorder="big") def has_even_y(P: Point) -> bool: assert not is_infinite(P) return y(P) % 2 == 0 # # End of helper functions copied from BIP-340 reference implementation. # PlainPk = NewType('PlainPk', bytes) XonlyPk = NewType('XonlyPk', bytes) # There are two types of exceptions that can be raised by this implementation: # - ValueError for indicating that an input doesn't conform to some function # precondition (e.g. an input array is the wrong length, a serialized # representation doesn't have the correct format). # - InvalidContributionError for indicating that a signer (or the # aggregator) is misbehaving in the protocol. # # Assertions are used to (1) satisfy the type-checking system, and (2) check for # inconvenient events that can't happen except with negligible probability (e.g. # output of a hash function is 0) and can't be manually triggered by any # signer. # This exception is raised if a party (signer or nonce aggregator) sends invalid # values. Actual implementations should not crash when receiving invalid # contributions. Instead, they should hold the offending party accountable. class InvalidContributionError(Exception): def __init__(self, signer, contrib): self.signer = signer # contrib is one of "pubkey", "pubnonce", "aggnonce", or "psig". self.contrib = contrib infinity = None def xbytes(P: Point) -> bytes: return bytes_from_int(x(P)) def cbytes(P: Point) -> bytes: a = b'\x02' if has_even_y(P) else b'\x03' return a + xbytes(P) def point_negate(P: Optional[Point]) -> Optional[Point]: if P is None: return P return (x(P), p - y(P)) def cpoint(x: bytes) -> Point: if len(x) != 33: raise ValueError('x is not a valid compressed point.') P = lift_x(x[1:33]) if P is None: raise ValueError('x is not a valid compressed point.') if x[0] == 2: return P elif x[0] == 3: P = point_negate(P) assert P is not None return P else: raise ValueError('x is not a valid compressed point.') KeyAggContext = NamedTuple('KeyAggContext', [('Q', Point), ('gacc', int), ('tacc', int)]) def key_agg(pubkeys: List[PlainPk]) -> KeyAggContext: pk2 = get_second_key(pubkeys) u = len(pubkeys) Q = infinity for i in range(u): try: P_i = cpoint(pubkeys[i]) except ValueError: raise InvalidContributionError(i, "pubkey") a_i = key_agg_coeff_internal(pubkeys, pubkeys[i], pk2) Q = point_add(Q, point_mul(P_i, a_i)) # Q is not the point at infinity except with negligible probability. assert(Q is not None) gacc = 1 tacc = 0 return KeyAggContext(Q, gacc, tacc) def hash_keys(pubkeys: List[PlainPk]) -> bytes: return tagged_hash('KeyAgg list', b''.join(pubkeys)) def get_second_key(pubkeys: List[PlainPk]) -> PlainPk: u = len(pubkeys) for j in range(1, u): if pubkeys[j] != pubkeys[0]: return pubkeys[j] return PlainPk(b'\x00'*33) def key_agg_coeff_internal(pubkeys: List[PlainPk], pk_: PlainPk, pk2: PlainPk) -> int: L = hash_keys(pubkeys) if pk_ == pk2: return 1 return int_from_bytes(tagged_hash('KeyAgg coefficient', L + pk_)) % n ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741351045.8493705 ledger_bitcoin-0.4.0/ledger_bitcoin/btchip/0000775000175000017500000000000014762564206020602 5ustar00singalasingala././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/btchip/__init__.py0000664000175000017500000000152714506542032022705 0ustar00singalasingala""" ******************************************************************************* * BTChip Bitcoin Hardware Wallet Python API * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************** """ __version__ = "0.1.31" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/btchip/bitcoinTransaction.py0000664000175000017500000001141214506542032024775 0ustar00singalasingala""" ******************************************************************************* * BTChip Bitcoin Hardware Wallet Python API * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************** """ from .bitcoinVarint import * from binascii import hexlify class bitcoinInput: def __init__(self, bufferOffset=None): self.prevOut = "" self.script = "" self.sequence = "" if bufferOffset is not None: buf = bufferOffset['buffer'] offset = bufferOffset['offset'] self.prevOut = buf[offset:offset + 36] offset += 36 scriptSize = readVarint(buf, offset) offset += scriptSize['size'] self.script = buf[offset:offset + scriptSize['value']] offset += scriptSize['value'] self.sequence = buf[offset:offset + 4] offset += 4 bufferOffset['offset'] = offset def serialize(self): result = [] result.extend(self.prevOut) writeVarint(len(self.script), result) result.extend(self.script) result.extend(self.sequence) return result def __str__(self): buf = "Prevout : " + hexlify(self.prevOut) + "\r\n" buf += "Script : " + hexlify(self.script) + "\r\n" buf += "Sequence : " + hexlify(self.sequence) + "\r\n" return buf class bitcoinOutput: def __init__(self, bufferOffset=None): self.amount = "" self.script = "" if bufferOffset is not None: buf = bufferOffset['buffer'] offset = bufferOffset['offset'] self.amount = buf[offset:offset + 8] offset += 8 scriptSize = readVarint(buf, offset) offset += scriptSize['size'] self.script = buf[offset:offset + scriptSize['value']] offset += scriptSize['value'] bufferOffset['offset'] = offset def serialize(self): result = [] result.extend(self.amount) writeVarint(len(self.script), result) result.extend(self.script) return result def __str__(self): buf = "Amount : " + hexlify(self.amount) + "\r\n" buf += "Script : " + hexlify(self.script) + "\r\n" return buf class bitcoinTransaction: def __init__(self, data=None): self.version = "" self.inputs = [] self.outputs = [] self.lockTime = "" self.witness = False self.witnessScript = "" if data is not None: offset = 0 self.version = data[offset:offset + 4] offset += 4 if (data[offset] == 0) and (data[offset + 1] != 0): offset += 2 self.witness = True inputSize = readVarint(data, offset) offset += inputSize['size'] numInputs = inputSize['value'] for i in range(numInputs): tmp = { 'buffer': data, 'offset' : offset} self.inputs.append(bitcoinInput(tmp)) offset = tmp['offset'] outputSize = readVarint(data, offset) offset += outputSize['size'] numOutputs = outputSize['value'] for i in range(numOutputs): tmp = { 'buffer': data, 'offset' : offset} self.outputs.append(bitcoinOutput(tmp)) offset = tmp['offset'] if self.witness: self.witnessScript = data[offset : len(data) - 4] self.lockTime = data[len(data) - 4:] else: self.lockTime = data[offset:offset + 4] def serialize(self, skipOutputLocktime=False, skipWitness=False): if skipWitness or (not self.witness): useWitness = False else: useWitness = True result = [] result.extend(self.version) if useWitness: result.append(0x00) result.append(0x01) writeVarint(len(self.inputs), result) for trinput in self.inputs: result.extend(trinput.serialize()) if not skipOutputLocktime: writeVarint(len(self.outputs), result) for troutput in self.outputs: result.extend(troutput.serialize()) if useWitness: result.extend(self.witnessScript) result.extend(self.lockTime) return result def serializeOutputs(self): result = [] writeVarint(len(self.outputs), result) for troutput in self.outputs: result.extend(troutput.serialize()) return result def __str__(self): buf = "Version : " + hexlify(self.version) + "\r\n" index = 1 for trinput in self.inputs: buf += "Input #" + str(index) + "\r\n" buf += str(trinput) index+=1 index = 1 for troutput in self.outputs: buf += "Output #" + str(index) + "\r\n" buf += str(troutput) index+=1 buf += "Locktime : " + hexlify(self.lockTime) + "\r\n" if self.witness: buf += "Witness script : " + hexlify(self.witnessScript) + "\r\n" return buf ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/btchip/bitcoinVarint.py0000664000175000017500000000375114506542032023762 0ustar00singalasingala""" ******************************************************************************* * BTChip Bitcoin Hardware Wallet Python API * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************** """ from .btchipException import BTChipException def readVarint(buffer, offset): varintSize = 0 value = 0 if (buffer[offset] < 0xfd): value = buffer[offset] varintSize = 1 elif (buffer[offset] == 0xfd): value = (buffer[offset + 2] << 8) | (buffer[offset + 1]) varintSize = 3 elif (buffer[offset] == 0xfe): value = (buffer[offset + 4] << 24) | (buffer[offset + 3] << 16) | (buffer[offset + 2] << 8) | (buffer[offset + 1]) varintSize = 5 else: raise BTChipException("unsupported varint") return { "value": value, "size": varintSize } def writeVarint(value, buffer): if (value < 0xfd): buffer.append(value) elif (value <= 0xffff): buffer.append(0xfd) buffer.append(value & 0xff) buffer.append((value >> 8) & 0xff) elif (value <= 0xffffffff): buffer.append(0xfe) buffer.append(value & 0xff) buffer.append((value >> 8) & 0xff) buffer.append((value >> 16) & 0xff) buffer.append((value >> 24) & 0xff) else: raise BTChipException("unsupported encoding") return buffer def getVarintSize(value): if (value < 0xfd): return 1 elif (value <= 0xffff): return 3 elif (value <= 0xffffffff): return 5 else: raise BTChipException("unsupported encoding") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/btchip/btchip.py0000664000175000017500000004013314506542032022413 0ustar00singalasingala""" ******************************************************************************* * BTChip Bitcoin Hardware Wallet Python API * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************** """ from .btchipComm import * from .bitcoinTransaction import * from .bitcoinVarint import * from .btchipException import * from .btchipHelpers import * from binascii import hexlify, unhexlify class btchip: BTCHIP_CLA = 0xe0 BTCHIP_CLA_COMMON_SDK = 0xb0 BTCHIP_JC_EXT_CLA = 0xf0 BTCHIP_INS_GET_APP_NAME_AND_VERSION = 0x01 BTCHIP_INS_SET_ALTERNATE_COIN_VERSION = 0x14 BTCHIP_INS_SETUP = 0x20 BTCHIP_INS_VERIFY_PIN = 0x22 BTCHIP_INS_GET_OPERATION_MODE = 0x24 BTCHIP_INS_SET_OPERATION_MODE = 0x26 BTCHIP_INS_SET_KEYMAP = 0x28 BTCHIP_INS_SET_COMM_PROTOCOL = 0x2a BTCHIP_INS_GET_WALLET_PUBLIC_KEY = 0x40 BTCHIP_INS_GET_TRUSTED_INPUT = 0x42 BTCHIP_INS_HASH_INPUT_START = 0x44 BTCHIP_INS_HASH_INPUT_FINALIZE = 0x46 BTCHIP_INS_HASH_SIGN = 0x48 BTCHIP_INS_HASH_INPUT_FINALIZE_FULL = 0x4a BTCHIP_INS_GET_INTERNAL_CHAIN_INDEX = 0x4c BTCHIP_INS_SIGN_MESSAGE = 0x4e BTCHIP_INS_GET_TRANSACTION_LIMIT = 0xa0 BTCHIP_INS_SET_TRANSACTION_LIMIT = 0xa2 BTCHIP_INS_IMPORT_PRIVATE_KEY = 0xb0 BTCHIP_INS_GET_PUBLIC_KEY = 0xb2 BTCHIP_INS_DERIVE_BIP32_KEY = 0xb4 BTCHIP_INS_SIGNVERIFY_IMMEDIATE = 0xb6 BTCHIP_INS_GET_RANDOM = 0xc0 BTCHIP_INS_GET_ATTESTATION = 0xc2 BTCHIP_INS_GET_FIRMWARE_VERSION = 0xc4 BTCHIP_INS_COMPOSE_MOFN_ADDRESS = 0xc6 BTCHIP_INS_GET_POS_SEED = 0xca BTCHIP_INS_EXT_GET_HALF_PUBLIC_KEY = 0x20 BTCHIP_INS_EXT_CACHE_PUT_PUBLIC_KEY = 0x22 BTCHIP_INS_EXT_CACHE_HAS_PUBLIC_KEY = 0x24 BTCHIP_INS_EXT_CACHE_GET_FEATURES = 0x26 OPERATION_MODE_WALLET = 0x01 OPERATION_MODE_RELAXED_WALLET = 0x02 OPERATION_MODE_SERVER = 0x04 OPERATION_MODE_DEVELOPER = 0x08 FEATURE_UNCOMPRESSED_KEYS = 0x01 FEATURE_RFC6979 = 0x02 FEATURE_FREE_SIGHASHTYPE = 0x04 FEATURE_NO_2FA_P2SH = 0x08 QWERTY_KEYMAP = bytearray(unhexlify("000000000000000000000000760f00d4ffffffc7000000782c1e3420212224342627252e362d3738271e1f202122232425263333362e37381f0405060708090a0b0c0d0e0f101112131415161718191a1b1c1d2f3130232d350405060708090a0b0c0d0e0f101112131415161718191a1b1c1d2f313035")) QWERTZ_KEYMAP = bytearray(unhexlify("000000000000000000000000760f00d4ffffffc7000000782c1e3420212224342627252e362d3738271e1f202122232425263333362e37381f0405060708090a0b0c0d0e0f101112131415161718191a1b1d1c2f3130232d350405060708090a0b0c0d0e0f101112131415161718191a1b1d1c2f313035")) AZERTY_KEYMAP = bytearray(unhexlify("08000000010000200100007820c8ffc3feffff07000000002c38202030341e21222d352e102e3637271e1f202122232425263736362e37101f1405060708090a0b0c0d0e0f331112130415161718191d1b1c1a2f64302f2d351405060708090a0b0c0d0e0f331112130415161718191d1b1c1a2f643035")) def __init__(self, dongle): self.dongle = dongle self.needKeyCache = False try: firmware = self.getFirmwareVersion()['version'] self.multiOutputSupported = tuple(map(int, (firmware.split(".")))) >= (1, 1, 4) if self.multiOutputSupported: self.scriptBlockLength = 50 else: self.scriptBlockLength = 255 except Exception: pass def getWalletPublicKey(self, path, showOnScreen=False, segwit=False, segwitNative=False, cashAddr=False): result = {} donglePath = parse_bip32_path(path) if self.needKeyCache: self.resolvePublicKeysInPath(path) apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_WALLET_PUBLIC_KEY, 0x01 if showOnScreen else 0x00, 0x03 if cashAddr else 0x02 if segwitNative else 0x01 if segwit else 0x00, len(donglePath) ] apdu.extend(donglePath) response = self.dongle.exchange(bytearray(apdu)) offset = 0 result['publicKey'] = response[offset + 1 : offset + 1 + response[offset]] offset = offset + 1 + response[offset] result['address'] = str(response[offset + 1 : offset + 1 + response[offset]]) offset = offset + 1 + response[offset] result['chainCode'] = response[offset : offset + 32] return result def getTrustedInput(self, transaction, index): result = {} # Header apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x00, 0x00 ] params = bytearray.fromhex("%.8x" % (index)) params.extend(transaction.version) writeVarint(len(transaction.inputs), params) apdu.append(len(params)) apdu.extend(params) self.dongle.exchange(bytearray(apdu)) # Each input for trinput in transaction.inputs: apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00 ] params = bytearray(trinput.prevOut) writeVarint(len(trinput.script), params) apdu.append(len(params)) apdu.extend(params) self.dongle.exchange(bytearray(apdu)) offset = 0 while True: blockLength = 251 if ((offset + blockLength) < len(trinput.script)): dataLength = blockLength else: dataLength = len(trinput.script) - offset params = bytearray(trinput.script[offset : offset + dataLength]) if ((offset + dataLength) == len(trinput.script)): params.extend(trinput.sequence) apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00, len(params) ] apdu.extend(params) self.dongle.exchange(bytearray(apdu)) offset += dataLength if (offset >= len(trinput.script)): break # Number of outputs apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00 ] params = [] writeVarint(len(transaction.outputs), params) apdu.append(len(params)) apdu.extend(params) self.dongle.exchange(bytearray(apdu)) # Each output indexOutput = 0 for troutput in transaction.outputs: apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00 ] params = bytearray(troutput.amount) writeVarint(len(troutput.script), params) apdu.append(len(params)) apdu.extend(params) self.dongle.exchange(bytearray(apdu)) offset = 0 while (offset < len(troutput.script)): blockLength = 255 if ((offset + blockLength) < len(troutput.script)): dataLength = blockLength else: dataLength = len(troutput.script) - offset apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00, dataLength ] apdu.extend(troutput.script[offset : offset + dataLength]) self.dongle.exchange(bytearray(apdu)) offset += dataLength # Locktime apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_TRUSTED_INPUT, 0x80, 0x00, len(transaction.lockTime) ] apdu.extend(transaction.lockTime) response = self.dongle.exchange(bytearray(apdu)) result['trustedInput'] = True result['value'] = response return result def startUntrustedTransaction(self, newTransaction, inputIndex, outputList, redeemScript, version=0x01, cashAddr=False, continueSegwit=False): # Start building a fake transaction with the passed inputs segwit = False if newTransaction: for passedOutput in outputList: if ('witness' in passedOutput) and passedOutput['witness']: segwit = True break if newTransaction: if segwit: p2 = 0x03 if cashAddr else 0x02 else: p2 = 0x00 else: p2 = 0x10 if continueSegwit else 0x80 apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x00, p2 ] params = bytearray([version, 0x00, 0x00, 0x00]) writeVarint(len(outputList), params) apdu.append(len(params)) apdu.extend(params) self.dongle.exchange(bytearray(apdu)) # Loop for each input currentIndex = 0 for passedOutput in outputList: if ('sequence' in passedOutput) and passedOutput['sequence']: sequence = bytearray(unhexlify(passedOutput['sequence'])) else: sequence = bytearray([0xFF, 0xFF, 0xFF, 0xFF]) # default sequence apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00 ] params = [] script = bytearray(redeemScript) if ('trustedInput' in passedOutput) and passedOutput['trustedInput']: params.append(0x01) elif ('witness' in passedOutput) and passedOutput['witness']: params.append(0x02) else: params.append(0x00) if ('trustedInput' in passedOutput) and passedOutput['trustedInput']: params.append(len(passedOutput['value'])) params.extend(passedOutput['value']) if currentIndex != inputIndex: script = bytearray() writeVarint(len(script), params) apdu.append(len(params)) apdu.extend(params) self.dongle.exchange(bytearray(apdu)) offset = 0 while(offset < len(script)): blockLength = 255 if ((offset + blockLength) < len(script)): dataLength = blockLength else: dataLength = len(script) - offset params = script[offset : offset + dataLength] if ((offset + dataLength) == len(script)): params.extend(sequence) apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00, len(params) ] apdu.extend(params) self.dongle.exchange(bytearray(apdu)) offset += blockLength if len(script) == 0: apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_START, 0x80, 0x00, len(sequence) ] apdu.extend(sequence) self.dongle.exchange(bytearray(apdu)) currentIndex += 1 def finalizeInput(self, outputAddress, amount, fees, changePath, rawTx=None): alternateEncoding = False donglePath = parse_bip32_path(changePath) if self.needKeyCache: self.resolvePublicKeysInPath(changePath) result = {} outputs = None if rawTx is not None: try: fullTx = bitcoinTransaction(bytearray(rawTx)) outputs = fullTx.serializeOutputs() if len(donglePath) != 0: apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, 0xFF, 0x00 ] params = [] params.extend(donglePath) apdu.append(len(params)) apdu.extend(params) response = self.dongle.exchange(bytearray(apdu)) offset = 0 while (offset < len(outputs)): blockLength = self.scriptBlockLength if ((offset + blockLength) < len(outputs)): dataLength = blockLength p1 = 0x00 else: dataLength = len(outputs) - offset p1 = 0x80 apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE_FULL, \ p1, 0x00, dataLength ] apdu.extend(outputs[offset : offset + dataLength]) response = self.dongle.exchange(bytearray(apdu)) offset += dataLength alternateEncoding = True except Exception: pass if not alternateEncoding: apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_INPUT_FINALIZE, 0x02, 0x00 ] params = [] params.append(len(outputAddress)) params.extend(bytearray(outputAddress)) writeHexAmountBE(btc_to_satoshi(str(amount)), params) writeHexAmountBE(btc_to_satoshi(str(fees)), params) params.extend(donglePath) apdu.append(len(params)) apdu.extend(params) response = self.dongle.exchange(bytearray(apdu)) result['confirmationNeeded'] = response[1 + response[0]] != 0x00 result['confirmationType'] = response[1 + response[0]] if result['confirmationType'] == 0x02: result['keycardData'] = response[1 + response[0] + 1:] if result['confirmationType'] == 0x03: offset = 1 + response[0] + 1 keycardDataLength = response[offset] offset = offset + 1 result['keycardData'] = response[offset : offset + keycardDataLength] offset = offset + keycardDataLength result['secureScreenData'] = response[offset:] if result['confirmationType'] == 0x04: offset = 1 + response[0] + 1 keycardDataLength = response[offset] result['keycardData'] = response[offset + 1 : offset + 1 + keycardDataLength] if outputs == None: result['outputData'] = response[1 : 1 + response[0]] else: result['outputData'] = outputs return result def untrustedHashSign(self, path, pin="", lockTime=0, sighashType=0x01): if isinstance(pin, str): pin = pin.encode('utf-8') donglePath = parse_bip32_path(path) if self.needKeyCache: self.resolvePublicKeysInPath(path) apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_HASH_SIGN, 0x00, 0x00 ] params = [] params.extend(donglePath) params.append(len(pin)) params.extend(bytearray(pin)) writeUint32BE(lockTime, params) params.append(sighashType) apdu.append(len(params)) apdu.extend(params) result = self.dongle.exchange(bytearray(apdu)) result[0] = 0x30 return result def signMessagePrepareV1(self, path, message): donglePath = parse_bip32_path(path) if self.needKeyCache: self.resolvePublicKeysInPath(path) result = {} apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGN_MESSAGE, 0x00, 0x00 ] params = [] params.extend(donglePath) params.append(len(message)) params.extend(bytearray(message)) apdu.append(len(params)) apdu.extend(params) response = self.dongle.exchange(bytearray(apdu)) result['confirmationNeeded'] = response[0] != 0x00 result['confirmationType'] = response[0] if result['confirmationType'] == 0x02: result['keycardData'] = response[1:] if result['confirmationType'] == 0x03: result['secureScreenData'] = response[1:] return result def signMessagePrepareV2(self, path, message): donglePath = parse_bip32_path(path) if self.needKeyCache: self.resolvePublicKeysInPath(path) result = {} offset = 0 encryptedOutputData = b"" while (offset < len(message)): params = []; if offset == 0: params.extend(donglePath) params.append((len(message) >> 8) & 0xff) params.append(len(message) & 0xff) p2 = 0x01 else: p2 = 0x80 blockLength = 255 - len(params) if ((offset + blockLength) < len(message)): dataLength = blockLength else: dataLength = len(message) - offset params.extend(bytearray(message[offset : offset + dataLength])) apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGN_MESSAGE, 0x00, p2 ] apdu.append(len(params)) apdu.extend(params) response = self.dongle.exchange(bytearray(apdu)) encryptedOutputData = encryptedOutputData + response[1 : 1 + response[0]] offset += blockLength result['confirmationNeeded'] = response[1 + response[0]] != 0x00 result['confirmationType'] = response[1 + response[0]] if result['confirmationType'] == 0x03: offset = 1 + response[0] + 1 result['secureScreenData'] = response[offset:] result['encryptedOutputData'] = encryptedOutputData return result def signMessagePrepare(self, path, message): try: result = self.signMessagePrepareV2(path, message) except BTChipException as e: if (e.sw == 0x6b00): # Old firmware version, try older method result = self.signMessagePrepareV1(path, message) else: raise return result def signMessageSign(self, pin=""): if isinstance(pin, str): pin = pin.encode('utf-8') apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_SIGN_MESSAGE, 0x80, 0x00 ] params = [] if pin is not None: params.append(len(pin)) params.extend(bytearray(pin)) else: params.append(0x00) apdu.append(len(params)) apdu.extend(params) response = self.dongle.exchange(bytearray(apdu)) return response def getAppName(self): apdu = [ self.BTCHIP_CLA_COMMON_SDK, self.BTCHIP_INS_GET_APP_NAME_AND_VERSION, 0x00, 0x00, 0x00 ] try: response = self.dongle.exchange(bytearray(apdu)) name_len = response[1] name = response[2:][:name_len] if b'OLOS' not in name: return name.decode('ascii') except BTChipException as e: if e.sw == 0x6faa: # ins not implemented" return None if e.sw == 0x6d00: # Not in an app, return just a string saying that return "not in an app" raise def getFirmwareVersion(self): result = {} apdu = [ self.BTCHIP_CLA, self.BTCHIP_INS_GET_FIRMWARE_VERSION, 0x00, 0x00, 0x00 ] try: response = self.dongle.exchange(bytearray(apdu)) except BTChipException as e: if (e.sw == 0x6985): response = [0x00, 0x00, 0x01, 0x04, 0x03 ] pass else: raise result['compressedKeys'] = (response[0] == 0x01) result['version'] = "%d.%d.%d" % (response[2], response[3], response[4]) result['major_version'] = response[2] result['minor_version'] = response[3] result['patch_version'] = response[4] result['specialVersion'] = response[1] return result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/btchip/btchipComm.py0000664000175000017500000001127314506542032023232 0ustar00singalasingala""" ******************************************************************************* * BTChip Bitcoin Hardware Wallet Python API * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************** """ from abc import ABCMeta, abstractmethod from .btchipException import * from .ledgerWrapper import wrapCommandAPDU, unwrapResponseAPDU from binascii import hexlify import time import os import struct import socket try: import hid HID = True except ImportError: HID = False try: from smartcard.Exceptions import NoCardException from smartcard.System import readers from smartcard.util import toHexString, toBytes SCARD = True except ImportError: SCARD = False class DongleWait(object): __metaclass__ = ABCMeta @abstractmethod def waitFirstResponse(self, timeout): pass class Dongle(object): __metaclass__ = ABCMeta @abstractmethod def exchange(self, apdu, timeout=20000): pass @abstractmethod def close(self): pass def setWaitImpl(self, waitImpl): self.waitImpl = waitImpl class HIDDongleHIDAPI(Dongle, DongleWait): def __init__(self, device, ledger=False, debug=False): self.device = device self.ledger = ledger self.debug = debug self.waitImpl = self self.opened = True def exchange(self, apdu, timeout=20000): if self.debug: print("=> %s" % hexlify(apdu)) if self.ledger: apdu = wrapCommandAPDU(0x0101, apdu, 64) padSize = len(apdu) % 64 tmp = apdu if padSize != 0: tmp.extend([0] * (64 - padSize)) offset = 0 while(offset != len(tmp)): data = tmp[offset:offset + 64] data = bytearray([0]) + data self.device.write(data) offset += 64 dataLength = 0 dataStart = 2 result = self.waitImpl.waitFirstResponse(timeout) if not self.ledger: if result[0] == 0x61: # 61xx : data available self.device.set_nonblocking(False) dataLength = result[1] dataLength += 2 if dataLength > 62: remaining = dataLength - 62 while(remaining != 0): if remaining > 64: blockLength = 64 else: blockLength = remaining result.extend(bytearray(self.device.read(65))[0:blockLength]) remaining -= blockLength swOffset = dataLength dataLength -= 2 self.device.set_nonblocking(True) else: swOffset = 0 else: self.device.set_nonblocking(False) while True: response = unwrapResponseAPDU(0x0101, result, 64) if response is not None: result = response dataStart = 0 swOffset = len(response) - 2 dataLength = len(response) - 2 self.device.set_nonblocking(True) break result.extend(bytearray(self.device.read(65))) sw = (result[swOffset] << 8) + result[swOffset + 1] response = result[dataStart : dataLength + dataStart] if self.debug: print("<= %s%.2x" % (hexlify(response), sw)) if sw != 0x9000: raise BTChipException("Invalid status %04x" % sw, sw) return response def waitFirstResponse(self, timeout): start = time.time() data = "" while len(data) == 0: data = self.device.read(65) if not len(data): if time.time() - start > timeout: raise BTChipException("Timeout") time.sleep(0.02) return bytearray(data) def close(self): if self.opened: try: self.device.close() except Exception: pass self.opened = False class DongleServer(Dongle): def __init__(self, server, port, debug=False): self.server = server self.port = port self.debug = debug self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: self.socket.connect((self.server, self.port)) except Exception: raise BTChipException("Proxy connection failed") def exchange(self, apdu, timeout=20000): if self.debug: print("=> %s" % hexlify(apdu)) self.socket.send(struct.pack(">I", len(apdu))) self.socket.send(apdu) size = struct.unpack(">I", self.socket.recv(4))[0] response = self.socket.recv(size) sw = struct.unpack(">H", self.socket.recv(2))[0] if self.debug: print("<= %s%.2x" % (hexlify(response), sw)) if sw != 0x9000: raise BTChipException("Invalid status %04x" % sw, sw) return bytearray(response) def close(self): try: self.socket.close() except Exception: pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/btchip/btchipException.py0000664000175000017500000000177414506542032024302 0ustar00singalasingala""" ******************************************************************************* * BTChip Bitcoin Hardware Wallet Python API * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************** """ class BTChipException(Exception): def __init__(self, message, sw=0x6f00): self.message = message self.sw = sw def __str__(self): buf = "Exception : " + self.message return buf ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710772394.0 ledger_bitcoin-0.4.0/ledger_bitcoin/btchip/btchipHelpers.py0000664000175000017500000000523314576050252023745 0ustar00singalasingala""" ******************************************************************************* * BTChip Bitcoin Hardware Wallet Python API * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************** """ import decimal import re # from pycoin SATOSHI_PER_COIN = decimal.Decimal(100_000_000) COIN_PER_SATOSHI = decimal.Decimal('0.00000001') def satoshi_to_btc(satoshi_count): if satoshi_count == 0: return decimal.Decimal(0) r = satoshi_count * COIN_PER_SATOSHI return r.normalize() def btc_to_satoshi(btc): return int(decimal.Decimal(btc) * SATOSHI_PER_COIN) # /from pycoin def writeUint32BE(value, buffer): buffer.append((value >> 24) & 0xff) buffer.append((value >> 16) & 0xff) buffer.append((value >> 8) & 0xff) buffer.append(value & 0xff) return buffer def writeUint32LE(value, buffer): buffer.append(value & 0xff) buffer.append((value >> 8) & 0xff) buffer.append((value >> 16) & 0xff) buffer.append((value >> 24) & 0xff) return buffer def writeHexAmount(value, buffer): buffer.append(value & 0xff) buffer.append((value >> 8) & 0xff) buffer.append((value >> 16) & 0xff) buffer.append((value >> 24) & 0xff) buffer.append((value >> 32) & 0xff) buffer.append((value >> 40) & 0xff) buffer.append((value >> 48) & 0xff) buffer.append((value >> 56) & 0xff) return buffer def writeHexAmountBE(value, buffer): buffer.append((value >> 56) & 0xff) buffer.append((value >> 48) & 0xff) buffer.append((value >> 40) & 0xff) buffer.append((value >> 32) & 0xff) buffer.append((value >> 24) & 0xff) buffer.append((value >> 16) & 0xff) buffer.append((value >> 8) & 0xff) buffer.append(value & 0xff) return buffer def parse_bip32_path(path): if len(path) == 0: return bytearray([ 0 ]) result = [] elements = path.split('/') if len(elements) > 10: raise BTChipException("Path too long") for pathElement in elements: element = re.split('\'|h|H', pathElement) if len(element) == 1: writeUint32BE(int(element[0]), result) else: writeUint32BE(0x80000000 | int(element[0]), result) return bytearray([ len(elements) ] + result) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/btchip/btchipUtils.py0000664000175000017500000000653014506542032023437 0ustar00singalasingala""" ******************************************************************************* * BTChip Bitcoin Hardware Wallet Python API * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************** """ from .btchipException import * from .bitcoinTransaction import * from .btchipHelpers import * def compress_public_key(publicKey): if publicKey[0] == 0x04: if (publicKey[64] & 1) != 0: prefix = 0x03 else: prefix = 0x02 result = [prefix] result.extend(publicKey[1:33]) return bytearray(result) elif publicKey[0] == 0x03 or publicKey[0] == 0x02: return publicKey else: raise BTChipException("Invalid public key format") def format_transaction(dongleOutputData, trustedInputsAndInputScripts, version=0x01, lockTime=0): transaction = bitcoinTransaction() transaction.version = [] writeUint32LE(version, transaction.version) for item in trustedInputsAndInputScripts: newInput = bitcoinInput() newInput.prevOut = item[0][4:4+36] newInput.script = item[1] if len(item) > 2: newInput.sequence = bytearray(item[2].decode('hex')) else: newInput.sequence = bytearray([0xff, 0xff, 0xff, 0xff]) transaction.inputs.append(newInput) result = transaction.serialize(True) result.extend(dongleOutputData) writeUint32LE(lockTime, result) return bytearray(result) def get_regular_input_script(sigHashtype, publicKey): if len(sigHashtype) >= 0x4c: raise BTChipException("Invalid sigHashtype") if len(publicKey) >= 0x4c: raise BTChipException("Invalid publicKey") result = [ len(sigHashtype) ] result.extend(sigHashtype) result.append(len(publicKey)) result.extend(publicKey) return bytearray(result) def write_pushed_data_size(data, buffer): if (len(data) > 0xffff): raise BTChipException("unsupported encoding") if (len(data) < 0x4c): buffer.append(len(data)) elif (len(data) > 255): buffer.append(0x4d) buffer.append(len(data) & 0xff) buffer.append((len(data) >> 8) & 0xff) else: buffer.append(0x4c) buffer.append(len(data)) return buffer def get_p2sh_input_script(redeemScript, sigHashtypeList): result = [ 0x00 ] for sigHashtype in sigHashtypeList: write_pushed_data_size(sigHashtype, result) result.extend(sigHashtype) write_pushed_data_size(redeemScript, result) result.extend(redeemScript) return bytearray(result) def get_p2pk_input_script(sigHashtype): if len(sigHashtype) >= 0x4c: raise BTChipException("Invalid sigHashtype") result = [ len(sigHashtype) ] result.extend(sigHashtype) return bytearray(result) def get_output_script(amountScriptArray): result = [ len(amountScriptArray) ] for amountScript in amountScriptArray: writeHexAmount(btc_to_satoshi(str(amountScript[0])), result) writeVarint(len(amountScript[1]), result) result.extend(amountScript[1]) return bytearray(result) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/btchip/ledgerWrapper.py0000664000175000017500000000623014506542032023745 0ustar00singalasingala""" ******************************************************************************* * BTChip Bitcoin Hardware Wallet Python API * (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************** """ import struct from .btchipException import BTChipException def wrapCommandAPDU(channel, command, packetSize): if packetSize < 3: raise BTChipException("Can't handle Ledger framing with less than 3 bytes for the report") sequenceIdx = 0 offset = 0 result = struct.pack(">HBHH", channel, 0x05, sequenceIdx, len(command)) sequenceIdx = sequenceIdx + 1 if len(command) > packetSize - 7: blockSize = packetSize - 7 else: blockSize = len(command) result += command[offset : offset + blockSize] offset = offset + blockSize while offset != len(command): result += struct.pack(">HBH", channel, 0x05, sequenceIdx) sequenceIdx = sequenceIdx + 1 if (len(command) - offset) > packetSize - 5: blockSize = packetSize - 5 else: blockSize = len(command) - offset result += command[offset : offset + blockSize] offset = offset + blockSize while (len(result) % packetSize) != 0: result += b"\x00" return bytearray(result) def unwrapResponseAPDU(channel, data, packetSize): sequenceIdx = 0 offset = 0 if ((data is None) or (len(data) < 7 + 5)): return None if struct.unpack(">H", data[offset : offset + 2])[0] != channel: raise BTChipException("Invalid channel") offset += 2 if data[offset] != 0x05: raise BTChipException("Invalid tag") offset += 1 if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx: raise BTChipException("Invalid sequence") offset += 2 responseLength = struct.unpack(">H", data[offset : offset + 2])[0] offset += 2 if len(data) < 7 + responseLength: return None if responseLength > packetSize - 7: blockSize = packetSize - 7 else: blockSize = responseLength result = data[offset : offset + blockSize] offset += blockSize while (len(result) != responseLength): sequenceIdx = sequenceIdx + 1 if (offset == len(data)): return None if struct.unpack(">H", data[offset : offset + 2])[0] != channel: raise BTChipException("Invalid channel") offset += 2 if data[offset] != 0x05: raise BTChipException("Invalid tag") offset += 1 if struct.unpack(">H", data[offset : offset + 2])[0] != sequenceIdx: raise BTChipException("Invalid sequence") offset += 2 if (responseLength - len(result)) > packetSize - 5: blockSize = packetSize - 5 else: blockSize = responseLength - len(result) result += data[offset : offset + blockSize] offset += blockSize return bytearray(result) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733838905.0 ledger_bitcoin-0.4.0/ledger_bitcoin/client.py0000664000175000017500000003655014726044071021163 0ustar00singalasingalafrom packaging.version import parse as parse_version from typing import Tuple, List, Mapping, Optional, Union import base64 from io import BytesIO, BufferedReader from .embit import base58 from .embit.base import EmbitError from .embit.descriptor import Descriptor from .embit.networks import NETWORKS from .command_builder import BitcoinCommandBuilder, BitcoinInsType from .common import Chain, read_uint, read_varint from .client_command import ClientCommandInterpreter, CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG, CCMD_YIELD_MUSIG_PUBNONCE_TAG from .client_base import Client, MusigPartialSignature, MusigPubNonce, SignPsbtYieldedObject, TransportClient, PartialSignature from .client_legacy import LegacyClient from .exception import DeviceException from .errors import UnknownDeviceError from .merkle import get_merkleized_map_commitment from .wallet import WalletPolicy, WalletType from .psbt import PSBT, normalize_psbt from ._serialize import deser_string from .bip0327 import key_agg, cbytes def parse_stream_to_map(f: BufferedReader) -> Mapping[bytes, bytes]: result = {} while True: try: key = deser_string(f) except Exception: break # Check for separator if len(key) == 0: break value = deser_string(f) result[key] = value return result def aggr_xpub(pubkeys: List[bytes], chain: Chain) -> str: BIP_328_CHAINCODE = bytes.fromhex( "868087ca02a6f974c4598924c36b57762d32cb45717167e300622c7167e38965") # sort the pubkeys prior to aggregation ctx = key_agg(list(sorted(pubkeys))) compressed_pubkey = cbytes(ctx.Q) # Serialize according to BIP-32 if chain == Chain.MAIN: version = 0x0488B21E else: version = 0x043587CF return base58.encode_check(b''.join([ version.to_bytes(4, byteorder='big'), b'\x00', # depth b'\x00\x00\x00\x00', # parent fingerprint b'\x00\x00\x00\x00', # child number BIP_328_CHAINCODE, compressed_pubkey ])) # Given a valid descriptor, replaces each musig() (if any) with the # corresponding synthetic xpub/tpub. def replace_musigs(desc: str, chain: Chain) -> str: while True: musig_start = desc.find("musig(") if musig_start == -1: break musig_end = desc.find(")", musig_start) if musig_end == -1: raise ValueError("Invalid descriptor template") key_and_origs = desc[musig_start+6:musig_end].split(",") pubkeys = [] for key_orig in key_and_origs: orig_end = key_orig.find("]") xpub = key_orig if orig_end == -1 else key_orig[orig_end+1:] pubkeys.append(base58.decode_check(xpub)[-33:]) # replace with the aggregate xpub desc = desc[:musig_start] + \ aggr_xpub(pubkeys, chain) + desc[musig_end+1:] return desc def _make_partial_signature(pubkey_augm: bytes, signature: bytes) -> PartialSignature: if len(pubkey_augm) == 64: # tapscript spend: pubkey_augm is the concatenation of: # - a 32-byte x-only pubkey # - the 32-byte tapleaf_hash return PartialSignature(signature=signature, pubkey=pubkey_augm[0:32], tapleaf_hash=pubkey_augm[32:]) else: # either legacy, segwit or taproot keypath spend # pubkey must be 32 (taproot x-only pubkey) or 33 bytes (compressed pubkey) if len(pubkey_augm) not in [32, 33]: raise UnknownDeviceError(f"Invalid pubkey length returned: {len(pubkey_augm)}") return PartialSignature(signature=signature, pubkey=pubkey_augm) def _decode_signpsbt_yielded_value(res: bytes) -> Tuple[int, SignPsbtYieldedObject]: res_buffer = BytesIO(res) input_index_or_tag = read_varint(res_buffer) if input_index_or_tag == CCMD_YIELD_MUSIG_PUBNONCE_TAG: input_index = read_varint(res_buffer) pubnonce = res_buffer.read(66) participant_pk = res_buffer.read(33) aggregate_pubkey = res_buffer.read(33) tapleaf_hash = res_buffer.read() if len(tapleaf_hash) == 0: tapleaf_hash = None return ( input_index, MusigPubNonce( participant_pubkey=participant_pk, aggregate_pubkey=aggregate_pubkey, tapleaf_hash=tapleaf_hash, pubnonce=pubnonce ) ) elif input_index_or_tag == CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG: input_index = read_varint(res_buffer) partial_signature = res_buffer.read(32) participant_pk = res_buffer.read(33) aggregate_pubkey = res_buffer.read(33) tapleaf_hash = res_buffer.read() if len(tapleaf_hash) == 0: tapleaf_hash = None return ( input_index, MusigPartialSignature( participant_pubkey=participant_pk, aggregate_pubkey=aggregate_pubkey, tapleaf_hash=tapleaf_hash, partial_signature=partial_signature ) ) else: # other values follow an encoding without an explicit tag, where the # first element is the input index. All the signature types are implemented # by the PartialSignature type (not to be confused with the musig Partial Signature). input_index = input_index_or_tag pubkey_augm_len = read_uint(res_buffer, 8) pubkey_augm = res_buffer.read(pubkey_augm_len) signature = res_buffer.read() return((input_index, _make_partial_signature(pubkey_augm, signature))) class NewClient(Client): # internal use for testing: if set to True, sign_psbt will not clone the psbt before converting to psbt version 2 _no_clone_psbt: bool = False def __init__(self, comm_client: TransportClient, chain: Chain = Chain.MAIN, debug: bool = False) -> None: super().__init__(comm_client, chain, debug) self.builder = BitcoinCommandBuilder() # Modifies the behavior of the base method by taking care of SW_INTERRUPTED_EXECUTION responses def _make_request( self, apdu: dict, client_intepreter: ClientCommandInterpreter = None ) -> Tuple[int, bytes]: sw, response = self._apdu_exchange(apdu) while sw == 0xE000: if not client_intepreter: raise RuntimeError("Unexpected SW_INTERRUPTED_EXECUTION received.") command_response = client_intepreter.execute(response) sw, response = self._apdu_exchange( self.builder.continue_interrupted(command_response) ) return sw, response def get_extended_pubkey(self, path: str, display: bool = False) -> str: sw, response = self._make_request(self.builder.get_extended_pubkey(path, display)) if sw != 0x9000: raise DeviceException(error_code=sw, ins=BitcoinInsType.GET_EXTENDED_PUBKEY) return response.decode() def register_wallet(self, wallet: WalletPolicy) -> Tuple[bytes, bytes]: if wallet.version not in [WalletType.WALLET_POLICY_V1, WalletType.WALLET_POLICY_V2]: raise ValueError("invalid wallet policy version") client_intepreter = ClientCommandInterpreter() client_intepreter.add_known_preimage(wallet.serialize()) client_intepreter.add_known_list([k.encode() for k in wallet.keys_info]) # necessary for version 1 of the protocol (introduced in version 2.1.0) client_intepreter.add_known_preimage(wallet.descriptor_template.encode()) sw, response = self._make_request( self.builder.register_wallet(wallet), client_intepreter ) if sw != 0x9000: raise DeviceException(error_code=sw, ins=BitcoinInsType.REGISTER_WALLET) if len(response) != 64: raise RuntimeError(f"Invalid response length: {len(response)}") wallet_id = response[0:32] wallet_hmac = response[32:64] # sanity check: for miniscripts, derive the first address independently with python-bip380 first_addr_device = self.get_wallet_address(wallet, wallet_hmac, 0, 0, False) if first_addr_device != self._derive_address_for_policy(wallet, False, 0): raise RuntimeError("Invalid address. Please update your Bitcoin app. If the problem persists, report a bug at https://github.com/LedgerHQ/app-bitcoin-new") return wallet_id, wallet_hmac def get_wallet_address( self, wallet: WalletPolicy, wallet_hmac: Optional[bytes], change: int, address_index: int, display: bool, ) -> str: if not isinstance(wallet, WalletPolicy) or wallet.version not in [WalletType.WALLET_POLICY_V1, WalletType.WALLET_POLICY_V2]: raise ValueError("wallet type must be WalletPolicy, with version either WALLET_POLICY_V1 or WALLET_POLICY_V2") if change != 0 and change != 1: raise ValueError("Invalid change") client_intepreter = ClientCommandInterpreter() client_intepreter.add_known_list([k.encode() for k in wallet.keys_info]) client_intepreter.add_known_preimage(wallet.serialize()) # necessary for version 1 of the protocol (introduced in version 2.1.0) client_intepreter.add_known_preimage(wallet.descriptor_template.encode()) sw, response = self._make_request( self.builder.get_wallet_address( wallet, wallet_hmac, address_index, change, display ), client_intepreter, ) if sw != 0x9000: raise DeviceException(error_code=sw, ins=BitcoinInsType.GET_WALLET_ADDRESS) result = response.decode() # sanity check: for miniscripts, derive the address independently with python-bip380 if result != self._derive_address_for_policy(wallet, change, address_index): raise RuntimeError("Invalid address. Please update your Bitcoin app. If the problem persists, report a bug at https://github.com/LedgerHQ/app-bitcoin-new") return result def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, SignPsbtYieldedObject]]: psbt = normalize_psbt(psbt) if psbt.version != 2: if self._no_clone_psbt: psbt.convert_to_v2() psbt_v2 = psbt else: psbt_v2 = PSBT() psbt_v2.deserialize(psbt.serialize()) # clone psbt psbt_v2.convert_to_v2() else: psbt_v2 = psbt psbt_bytes = base64.b64decode(psbt_v2.serialize()) f = BytesIO(psbt_bytes) # We parse the individual maps (global map, each input map, and each output map) from the psbt serialized as a # sequence of bytes, in order to produce the serialized Merkleized map commitments. Moreover, we prepare the # client interpreter to respond on queries on all the relevant Merkle trees and pre-images in the psbt. assert f.read(5) == b"psbt\xff" client_intepreter = ClientCommandInterpreter() client_intepreter.add_known_list([k.encode() for k in wallet.keys_info]) client_intepreter.add_known_preimage(wallet.serialize()) # necessary for version 1 of the protocol (introduced in version 2.1.0) client_intepreter.add_known_preimage(wallet.descriptor_template.encode()) global_map: Mapping[bytes, bytes] = parse_stream_to_map(f) client_intepreter.add_known_mapping(global_map) input_maps: List[Mapping[bytes, bytes]] = [] for _ in range(len(psbt_v2.inputs)): input_maps.append(parse_stream_to_map(f)) for m in input_maps: client_intepreter.add_known_mapping(m) output_maps: List[Mapping[bytes, bytes]] = [] for _ in range(len(psbt_v2.outputs)): output_maps.append(parse_stream_to_map(f)) for m in output_maps: client_intepreter.add_known_mapping(m) # We also add the Merkle tree of the input (resp. output) map commitments as a known tree input_commitments = [get_merkleized_map_commitment(m_in) for m_in in input_maps] output_commitments = [get_merkleized_map_commitment(m_out) for m_out in output_maps] client_intepreter.add_known_list(input_commitments) client_intepreter.add_known_list(output_commitments) sw, _ = self._make_request( self.builder.sign_psbt( global_map, input_maps, output_maps, wallet, wallet_hmac ), client_intepreter, ) if sw != 0x9000: raise DeviceException(error_code=sw, ins=BitcoinInsType.SIGN_PSBT) # parse results and return a structured version instead results = client_intepreter.yielded if any(len(x) <= 1 for x in results): raise RuntimeError("Invalid response") results_list: List[Tuple[int, SignPsbtYieldedObject]] = [] for res in results: input_index, obj = _decode_signpsbt_yielded_value(res) results_list.append((input_index, obj)) return results_list def get_master_fingerprint(self) -> bytes: sw, response = self._make_request(self.builder.get_master_fingerprint()) if sw != 0x9000: raise DeviceException(error_code=sw, ins=BitcoinInsType.GET_EXTENDED_PUBKEY) return response def sign_message(self, message: Union[str, bytes], bip32_path: str) -> str: if isinstance(message, str): message_bytes = message.encode("utf-8") else: message_bytes = message chunks = [message_bytes[64 * i: 64 * i + 64] for i in range((len(message_bytes) + 63) // 64)] client_intepreter = ClientCommandInterpreter() client_intepreter.add_known_list(chunks) sw, response = self._make_request(self.builder.sign_message(message_bytes, bip32_path), client_intepreter) if sw != 0x9000: raise DeviceException(error_code=sw, ins=BitcoinInsType.SIGN_MESSAGE) return base64.b64encode(response).decode('utf-8') def _derive_address_for_policy(self, wallet: WalletPolicy, change: bool, address_index: int) -> Optional[str]: desc_str = wallet.get_descriptor(change) # Since embit does not support musig() in descriptors, we replace each # occurrence with the corresponding aggregated xpub desc_str = replace_musigs(desc_str, self.chain) try: desc = Descriptor.from_string(desc_str) desc = desc.derive(address_index) net = NETWORKS['main'] if self.chain == Chain.MAIN else NETWORKS['test'] return desc.script_pubkey().address(net) except EmbitError: return None def createClient(comm_client: Optional[TransportClient] = None, chain: Chain = Chain.MAIN, debug: bool = False) -> Union[LegacyClient, NewClient]: if comm_client is None: comm_client = TransportClient("hid") base_client = Client(comm_client, chain, debug) app_name, app_version, _ = base_client.get_version() version = parse_version(app_version) # Use the legacy client if either: # - the name of the app is "Bitcoin Legacy" or "Bitcoin Test Legacy" (regardless of the version) # - the version is strictly less than 2.1 use_legacy = app_name in ["Bitcoin Legacy", "Bitcoin Test Legacy"] or version.major < 2 or (version.major == 2 and version.minor == 0) if use_legacy: return LegacyClient(comm_client, chain, debug) else: return NewClient(comm_client, chain, debug) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733838905.0 ledger_bitcoin-0.4.0/ledger_bitcoin/client_base.py0000664000175000017500000002571314726044071022154 0ustar00singalasingalafrom dataclasses import dataclass from typing import List, Tuple, Optional, Union, Literal from io import BytesIO from ledgercomm.interfaces.hid_device import HID from .transport import Transport from .common import Chain from .command_builder import DefaultInsType from .exception import DeviceException from .wallet import WalletPolicy from .psbt import PSBT from ._serialize import deser_string try: from speculos.client import ApduException except ImportError: # Speculos package not available, we use our own class class ApduException(Exception): def __init__(self, sw: int, data: bytes) -> None: super().__init__(f"Exception: invalid status 0x{sw:x}") self.sw = sw self.data = data class TransportClient: def __init__(self, interface: Literal['hid', 'tcp'] = "tcp", *, server: str = "127.0.0.1", port: int = 9999, path: Optional[str] = None, hid: Optional[HID] = None, debug: bool = False): self.transport = Transport('hid', path=path, hid=hid, debug=debug) if interface == 'hid' else Transport( interface, server=server, port=port, debug=debug) def apdu_exchange( self, cla: int, ins: int, data: bytes = b"", p1: int = 0, p2: int = 0 ) -> bytes: sw, data = self.transport.exchange(cla, ins, p1, p2, None, data) if sw != 0x9000: raise ApduException(sw, data) return data def apdu_exchange_nowait( self, cla: int, ins: int, data: bytes = b"", p1: int = 0, p2: int = 0 ): raise NotImplementedError() def stop(self) -> None: self.transport.close() def print_apdu(apdu_dict: dict) -> None: serialized_apdu = b''.join([ apdu_dict["cla"].to_bytes(1, byteorder='big'), apdu_dict["ins"].to_bytes(1, byteorder='big'), apdu_dict["p1"].to_bytes(1, byteorder='big'), apdu_dict["p2"].to_bytes(1, byteorder='big'), len(apdu_dict["data"]).to_bytes(1, byteorder='big'), apdu_dict["data"] ]) print(f"=> {serialized_apdu.hex()}") def print_response(sw: int, data: bytes) -> None: print(f"<= {data.hex()}{sw.to_bytes(2, byteorder='big').hex()}") @dataclass(frozen=True) class PartialSignature: """Represents a partial signature returned by sign_psbt. Such objects can be added to the PSBT. It always contains a pubkey and a signature. The pubkey is a compressed 33-byte for legacy and segwit Scripts, or 32-byte x-only key for taproot. The signature is in the format it would be pushed on the scriptSig or the witness stack, therefore of variable length, and possibly concatenated with the SIGHASH flag byte if appropriate. The tapleaf_hash is also filled if signing for a tapscript. Note: not to be confused with 'partial signature' of protocols like MuSig2; """ pubkey: bytes signature: bytes tapleaf_hash: Optional[bytes] = None @dataclass(frozen=True) class MusigPubNonce: """Represents a pubnonce returned by sign_psbt during the first round of a Musig2 signing session. It always contains - the participant_pubkey, a 33-byte compressed pubkey; - aggregate_pubkey, the 33-byte compressed pubkey key that is the aggregate of all the participant pubkeys, with the necessary tweaks; its x-only version is the key present in the Script; - the 66-byte pubnonce. The tapleaf_hash is also filled if signing for a tapscript; `None` otherwise. """ participant_pubkey: bytes aggregate_pubkey: bytes tapleaf_hash: Optional[bytes] pubnonce: bytes @dataclass(frozen=True) class MusigPartialSignature: """Represents a partial signature returned by sign_psbt during the second round of a Musig2 signing session. It always contains - the participant_pubkey, a 33-byte compressed pubkey; - aggregate_pubkey, the 33-byte compressed pubkey key that is the aggregate of all the participant pubkeys, with the necessary tweaks; its x-only version is the key present in the Script; - the partial_signature, the 32-byte partial signature for this participant. The tapleaf_hash is also filled if signing for a tapscript; `None` otherwise """ participant_pubkey: bytes aggregate_pubkey: bytes tapleaf_hash: Optional[bytes] partial_signature: bytes SignPsbtYieldedObject = Union[PartialSignature, MusigPubNonce, MusigPartialSignature] class Client: def __init__(self, transport_client: TransportClient, chain: Chain = Chain.MAIN, debug: bool = False) -> None: self.transport_client = transport_client self.chain = chain self.debug = debug def _apdu_exchange(self, apdu: dict) -> Tuple[int, bytes]: try: if self.debug: print_apdu(apdu) response = self.transport_client.apdu_exchange(**apdu) if self.debug: print_response(0x9000, response) return 0x9000, response except ApduException as e: if self.debug: print_response(e.sw, e.data) return e.sw, e.data def _make_request(self, apdu: dict) -> Tuple[int, bytes]: return self._apdu_exchange(apdu) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.transport_client.stop() def stop(self) -> None: """Stops the transport_client.""" self.transport_client.stop() def get_version(self) -> Tuple[str, str, bytes]: """Queries the hardware wallet for the currently running app's name, version and state flags. Returns ------- Tuple[str, str, bytes] The first element is the app's name, as a short string. The second element is the app's version. The third element is a binary string representing the platform's global state (pin lock etc). """ sw, response = self._make_request( {"cla": 0xB0, "ins": DefaultInsType.GET_VERSION, "p1": 0, "p2": 0, "data": b''}) if sw != 0x9000: raise DeviceException( error_code=sw, ins=DefaultInsType.GET_VERSION) r = BytesIO(response) format = r.read(1) app_name = deser_string(r) app_version = deser_string(r) app_flags = deser_string(r) if format != b'\1' or app_name == b'' or app_version == b'' or app_flags == b'': raise DeviceException(error_code=sw, ins=DefaultInsType.GET_VERSION, message="Invalid format returned by GET_VERSION") return app_name.decode(), app_version.decode(), app_flags def get_extended_pubkey(self, path: str, display: bool = False) -> str: """Gets the serialized extended public key for certain BIP32 path. Optionally, validate with the user. Parameters ---------- path : str BIP32 path of the public key you want. display : bool Whether you want to display address and ask confirmation on the device. Returns ------- str The requested serialized extended public key. """ raise NotImplementedError def register_wallet(self, wallet: WalletPolicy) -> Tuple[bytes, bytes]: """Registers a wallet policy with the user. After approval returns the wallet id and hmac to be stored on the client. Parameters ---------- wallet : WalletPolicy The Wallet policy to register on the device. Returns ------- Tuple[bytes, bytes] The first element the tuple is the 32-bytes wallet id. The second element is the hmac. """ raise NotImplementedError def get_wallet_address( self, wallet: WalletPolicy, wallet_hmac: Optional[bytes], change: int, address_index: int, display: bool, ) -> str: """For a given wallet that was already registered on the device (or a standard wallet that does not need registration), returns the address for a certain `change`/`address_index` combination. Parameters ---------- wallet : WalletPolicy The registered wallet policy, or a standard wallet policy. wallet_hmac: Optional[bytes] For a registered wallet, the hmac obtained at wallet registration. `None` for a standard wallet policy. change: int 0 for a standard receive address, 1 for a change address. Other values are invalid. address_index: int The address index in the last step of the BIP32 derivation. display: bool Whether you want to display address and ask confirmation on the device. Returns ------- str The requested address. """ raise NotImplementedError def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, SignPsbtYieldedObject]]: """Signs a PSBT using a registered wallet (or a standard wallet that does not need registration). Signature requires explicit approval from the user. Parameters ---------- psbt : PSBT | bytes | str A PSBT of version 0 or 2, with all the necessary information to sign the inputs already filled in; what the required fields changes depending on the type of input. The non-witness UTXO must be present for both legacy and SegWit inputs, or the hardware wallet will reject signing (this will change for Taproot inputs). The argument can be either a `PSBT` object, or `bytes`, or a base64-encoded `str`. wallet : WalletPolicy The registered wallet policy, or a standard wallet policy. wallet_hmac: Optional[bytes] For a registered wallet, the hmac obtained at wallet registration. `None` for a standard wallet policy. Returns ------- List[Tuple[int, PartialSignature]] A list of tuples returned by the hardware wallets, where each element is a tuple of: - an integer, the index of the input being signed; - an instance of `PartialSignature`. """ raise NotImplementedError def get_master_fingerprint(self) -> bytes: """Gets the fingerprint of the master public key, as per BIP-32. Returns ------- bytes The fingerprint of the master public key, as an array of 4 bytes. """ raise NotImplementedError def sign_message(self, message: Union[str, bytes], bip32_path: str) -> str: """ Sign a message (bitcoin message signing). Signs a message using the legacy Bitcoin Core signed message format. The message is signed with the key at the given path. :param message: The message to be signed. First encoded as bytes if not already. :param bip32_path: The BIP 32 derivation for the key to sign the message with. :return: The signature """ raise NotImplementedError ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733838905.0 ledger_bitcoin-0.4.0/ledger_bitcoin/client_command.py0000664000175000017500000002625014726044071022655 0ustar00singalasingalafrom enum import IntEnum from typing import List, Mapping from collections import deque from hashlib import sha256 from .common import ByteStreamParser, sha256, write_varint from .merkle import MerkleTree, element_hash class ClientCommandCode(IntEnum): YIELD = 0x10 GET_PREIMAGE = 0x40 GET_MERKLE_LEAF_PROOF = 0x41 GET_MERKLE_LEAF_INDEX = 0x42 GET_MORE_ELEMENTS = 0xA0 CCMD_YIELD_MUSIG_PUBNONCE_TAG = 0xFFFFFFFF CCMD_YIELD_MUSIG_PARTIALSIGNATURE_TAG = 0xFFFFFFFE class ClientCommand: def execute(self, request: bytes) -> bytes: raise NotImplementedError("Subclasses should implement this method.") @property def code(self) -> int: raise NotImplementedError("Subclasses should implement this method.") class YieldCommand(ClientCommand): def __init__(self, results: List[bytes]): self.results = results @property def code(self) -> int: return ClientCommandCode.YIELD def execute(self, request: bytes) -> bytes: self.results.append(request[1:]) # only skip the first byte (command code) return b"" class GetPreimageCommand(ClientCommand): def __init__(self, known_preimages: Mapping[bytes, bytes], queue: "deque[bytes]"): self.queue = queue self.known_preimages = known_preimages @property def code(self) -> int: return ClientCommandCode.GET_PREIMAGE def execute(self, request: bytes) -> bytes: req = ByteStreamParser(request[1:]) if req.read_bytes(1) != b'\0': raise RuntimeError(f"Unsupported request: the first byte should be 0") req_hash = req.read_bytes(32) req.assert_empty() if req_hash in self.known_preimages: known_preimage = self.known_preimages[req_hash] preimage_len_out = write_varint(len(known_preimage)) # We can send at most 255 - len(preimage_len_out) - 1 bytes in a single message; # the rest will be stored for GET_MORE_ELEMENTS max_payload_size = 255 - len(preimage_len_out) - 1 payload_size = min(max_payload_size, len(known_preimage)) if payload_size < len(known_preimage): # split into list of length-1 bytes elements extra_elements = [ known_preimage[i: i + 1] for i in range(payload_size, len(known_preimage)) ] # add to the queue any remaining extra bytes self.queue.extend(extra_elements) return ( preimage_len_out + payload_size.to_bytes(1, byteorder="big") + known_preimage[:payload_size] ) # not found raise RuntimeError(f"Requested unknown preimage for: {req_hash.hex()}") class GetMerkleLeafProofCommand(ClientCommand): def __init__(self, known_trees: Mapping[bytes, MerkleTree], queue: "deque[bytes]"): self.queue = queue self.known_trees = known_trees @property def code(self) -> int: return ClientCommandCode.GET_MERKLE_LEAF_PROOF def execute(self, request: bytes) -> bytes: req = ByteStreamParser(request[1:]) root = req.read_bytes(32) tree_size = req.read_varint() leaf_index = req.read_varint() req.assert_empty() if not root in self.known_trees: raise ValueError(f"Unknown Merkle root: {root.hex()}.") mt: MerkleTree = self.known_trees[root] if leaf_index >= tree_size or len(mt) != tree_size: raise ValueError(f"Invalid index or tree size.") if len(self.queue) != 0: raise RuntimeError( "This command should not execute when the queue is not empty." ) proof = mt.prove_leaf(leaf_index) # Compute how many elements we can fit in 255 - 32 - 1 - 1 = 221 bytes n_response_elements = min((255 - 32 - 1 - 1) // 32, len(proof)) n_leftover_elements = len(proof) - n_response_elements # Add to the queue any proof elements that do not fit the response if (n_leftover_elements > 0): self.queue.extend(proof[-n_leftover_elements:]) return b"".join( [ mt.get(leaf_index), len(proof).to_bytes(1, byteorder="big"), n_response_elements.to_bytes(1, byteorder="big"), *proof[:n_response_elements], ] ) class GetMerkleLeafIndexCommand(ClientCommand): def __init__(self, known_trees: Mapping[bytes, MerkleTree]): self.known_trees = known_trees @property def code(self) -> int: return ClientCommandCode.GET_MERKLE_LEAF_INDEX def execute(self, request: bytes) -> bytes: req = ByteStreamParser(request[1:]) root = req.read_bytes(32) leaf_hash = req.read_bytes(32) req.assert_empty() if root not in self.known_trees: raise ValueError(f"Unknown Merkle root: {root.hex()}.") try: leaf_index = self.known_trees[root].leaf_index(leaf_hash) found = 1 except ValueError: leaf_index = 0 found = 0 return found.to_bytes(1, byteorder="big") + write_varint(leaf_index) class GetMoreElementsCommand(ClientCommand): def __init__(self, queue: "deque[bytes]"): self.queue = queue @property def code(self) -> int: return ClientCommandCode.GET_MORE_ELEMENTS def execute(self, request: bytes) -> bytes: if len(request) != 1: raise ValueError("Wrong request length.") if len(self.queue) == 0: raise ValueError("No elements to get.") element_len = len(self.queue[0]) if any(len(el) != element_len for el in self.queue): raise ValueError( "The queue contains elements of different byte length, which is not expected." ) # pop from the queue, keeping the total response length at most 255 response_elements = bytearray() n_added_elements = 0 while len(self.queue) > 0 and len(response_elements) + element_len <= 253: response_elements.extend(self.queue.popleft()) n_added_elements += 1 return b"".join( [ n_added_elements.to_bytes(1, byteorder="big"), element_len.to_bytes(1, byteorder="big"), bytes(response_elements), ] ) class ClientCommandInterpreter: """Interpreter for the client-side commands. This class keeps has methods to keep track of: - known preimages - known Merkle trees from lists of elements Moreover, it containes the state that is relevant for the interpreted client side commands: - a queue of bytes that contains any bytes that could not fit in a response from the GET_PREIMAGE client command (when a preimage is too long to fit in a single message) or the GET_MERKLE_LEAF_PROOF command (which returns a Merkle proof, which might be too long to fit in a single message). The data in the queue is returned in one (or more) successive GET_MORE_ELEMENTS commands from the hardware wallet. Finally, it keeps track of the yielded values (that is, the values sent from the hardware wallet with a YIELD client command). Attributes ---------- yielded: list[bytes] A list of all the value sent by the Hardware Wallet with a YIELD client command during thw processing of an APDU. """ def __init__(self): self.known_preimages: Mapping[bytes, bytes] = {} self.known_trees: Mapping[bytes, MerkleTree] = {} self.yielded: List[bytes] = [] queue = deque() commands = [ YieldCommand(self.yielded), GetPreimageCommand(self.known_preimages, queue), GetMerkleLeafIndexCommand(self.known_trees), GetMerkleLeafProofCommand(self.known_trees, queue), GetMoreElementsCommand(queue), ] self.commands = {cmd.code: cmd for cmd in commands} def execute(self, hw_response: bytes) -> bytes: """Interprets the client command requested by the hardware wallet, returning the appropriate response and updating the client interpreter's internal state if needed. Parameters ---------- hw_response : bytes The data content of the SW_INTERRUPTED_EXECUTION sent by the hardware wallet. Returns ------- bytes The result of the execution of the appropriate client side command, containing the response to be sent via INS_CONTINUE. """ if len(hw_response) == 0: raise RuntimeError( "Unexpected empty SW_INTERRUPTED_EXECUTION response from hardware wallet." ) cmd_code = hw_response[0] if cmd_code not in self.commands: raise RuntimeError( "Unexpected command code: 0x{:02X}".format(cmd_code) ) return self.commands[cmd_code].execute(hw_response) def add_known_preimage(self, element: bytes) -> None: """Adds a preimage to the list of known preimages. The client must respond with `element` when a GET_PREIMAGE command is sent with `sha256(element)` in its request. Parameters ---------- element : bytes An array of bytes whose preimage must be known to the client during an APDU execution. """ self.known_preimages[sha256(element)] = element def add_known_list(self, elements: List[bytes]) -> None: """Adds a known Merkleized list. Builds the Merkle tree of `elements`, and adds it to the Merkle trees known to the client (mapped by Merkle root `mt_root`). moreover, adds all the leafs (after adding the b'\0' prefix) to the list of known preimages. If `el` is one of `elements`, the client must respond with b'\0' + `el` when a GET_PREIMAGE client command is sent with `sha256(b'\0' + el)`. Moreover, the commands GET_MERKLE_LEAF_INDEX and GET_MERKLE_LEAF_PROOF must correctly answer queries relative to the Merkle whose root is `mt_root`. Parameters ---------- elements : List[bytes] A list of `bytes` corresponding to the leafs of the Merkle tree. """ for el in elements: self.add_known_preimage(b"\x00" + el) mt = MerkleTree(element_hash(el) for el in elements) self.known_trees[mt.root] = mt def add_known_mapping(self, mapping: Mapping[bytes, bytes]) -> None: """Adds the Merkle trees of keys, and the Merkle tree of values (ordered by key) of a mapping of bytes to bytes. Adds the Merkle tree of the list of keys, and the Merkle tree of the list of corresponding values, with the same semantics as the `add_known_list` applied separately to the two lists. Parameters ---------- mapping : Mapping[bytes, bytes] A mapping whose keys and values are `bytes`. """ items_sorted = list(sorted(mapping.items())) keys = [i[0] for i in items_sorted] values = [i[1] for i in items_sorted] self.add_known_list(keys) self.add_known_list(values) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/client_legacy.py0000664000175000017500000003610414623570367022512 0ustar00singalasingala""" This module provides a compatibility layer between the python client of the Ledger Nano Bitcoin app v2 and the v1.6.5, by translating client requests to the API of the app v1.6.5. The bulk of the code is taken from bitcoin-core/HWI, with the necessary adaptations. https://github.com/bitcoin-core/HWI/tree/a109bcd53d24a52e72f26af3ecbabb64b292ff0c, """ import struct import re import base64 from .client_base import PartialSignature from .client import Client, TransportClient from typing import List, Tuple, Optional, Union from .common import AddressType, Chain, hash160 from .key import ExtendedKey, parse_path from .psbt import PSBT, normalize_psbt from .wallet import WalletPolicy from ._script import is_p2sh, is_witness, is_p2wpkh, is_p2wsh from .btchip.btchip import btchip from .btchip.btchipUtils import compress_public_key from .btchip.bitcoinTransaction import bitcoinTransaction def get_address_type_for_policy(policy: WalletPolicy) -> AddressType: if policy.descriptor_template in ["pkh(@0/**)", "pkh(@0/<0;1>/*)"]: return AddressType.LEGACY elif policy.descriptor_template in ["wpkh(@0/**)", "wpkh(@0/<0:1>/*)"]: return AddressType.WIT elif policy.descriptor_template in ["sh(wpkh(@0/**))", "sh(wpkh(@0/<0;1>/*))"]: return AddressType.SH_WIT else: raise ValueError("Invalid or unsupported policy") # minimal checking of string keypath # taken from HWI def check_keypath(key_path: str) -> bool: parts = re.split("/", key_path) if parts[0] != "m": return False # strip hardening chars for index in parts[1:]: index_int = re.sub('[hH\']', '', index) if not index_int.isdigit(): return False if int(index_int) > 0x80000000: return False return True class DongleAdaptor: # TODO: type for comm_client def __init__(self, comm_client): self.comm_client = comm_client def exchange(self, apdu: Union[bytes, bytearray]) -> bytearray: cla = apdu[0] ins = apdu[1] p1 = apdu[2] p2 = apdu[3] lc = apdu[4] data = apdu[5:] assert len(data) == lc return bytearray(self.comm_client.apdu_exchange(cla, ins, data, p1, p2)) class LegacyClient(Client): """Wrapper for Ledger Bitcoin app before version 2.0.0.""" def __init__(self, comm_client: TransportClient, chain: Chain = Chain.MAIN, debug: bool = False): super().__init__(comm_client, chain, debug) self.app = btchip(DongleAdaptor(comm_client)) if self.app.getAppName() not in ["Bitcoin", "Bitcoin Legacy", "Bitcoin Test", "Bitcoin Test Legacy", "app"]: raise ValueError("Ledger is not in either the Bitcoin or Bitcoin Testnet app") def get_extended_pubkey(self, path: str, display: bool = False) -> str: # mostly taken from HWI path = path[2:] path = path.replace('h', '\'') path = path.replace('H', '\'') # This call returns raw uncompressed pubkey, chaincode pubkey = self.app.getWalletPublicKey(path, display) int_path = parse_path(path) if len(path) > 0: parent_path = "" for ind in path.split("/")[:-1]: parent_path += ind + "/" parent_path = parent_path[:-1] # Get parent key fingerprint parent = self.app.getWalletPublicKey(parent_path) fpr = hash160(compress_public_key(parent["publicKey"]))[:4] child = int_path[-1] # Special case for m else: child = 0 fpr = b"\x00\x00\x00\x00" xpub = ExtendedKey( version=ExtendedKey.MAINNET_PUBLIC if self.chain == Chain.MAIN else ExtendedKey.TESTNET_PUBLIC, depth=len(path.split("/")) if len(path) > 0 else 0, parent_fingerprint=fpr, child_num=child, chaincode=pubkey["chainCode"], privkey=None, pubkey=compress_public_key(pubkey["publicKey"]), ) return xpub.to_string() def register_wallet(self, wallet: WalletPolicy) -> Tuple[bytes, bytes]: raise NotImplementedError # legacy app does not have this functionality def get_wallet_address( self, wallet: WalletPolicy, wallet_hmac: Optional[bytes], change: int, address_index: int, display: bool, ) -> str: # TODO: check keypath if wallet_hmac is not None or wallet.n_keys != 1: raise NotImplementedError("Policy wallets are only supported from version 2.0.0. Please update your Ledger hardware wallet") if not isinstance(wallet, WalletPolicy): raise ValueError("Invalid wallet policy type, it must be WalletPolicy") key_info = wallet.keys_info[0] try: first_slash_pos = key_info.index("/") key_origin_end = key_info.index("]") except ValueError: raise ValueError("Could not extract key origin information") if key_info[0] != '[': raise ValueError("Key must have key origin information") key_origin_path = key_info[first_slash_pos + 1: key_origin_end] addr_type = get_address_type_for_policy(wallet) p2sh_p2wpkh = addr_type == AddressType.SH_WIT bech32 = addr_type == AddressType.WIT output = self.app.getWalletPublicKey(f"{key_origin_path}/{change}/{address_index}", display, p2sh_p2wpkh or bech32, bech32) assert isinstance(output["address"], str) return output['address'][12:-2] # HACK: A bug in getWalletPublicKey results in the address being returned as the string "bytearray(b'
')". This extracts the actual address to work around this. def sign_psbt(self, psbt: Union[PSBT, bytes, str], wallet: WalletPolicy, wallet_hmac: Optional[bytes]) -> List[Tuple[int, PartialSignature]]: if wallet_hmac is not None or wallet.n_keys != 1: raise NotImplementedError("Policy wallets are only supported from version 2.0.0. Please update your Ledger hardware wallet") if not isinstance(wallet, WalletPolicy): raise ValueError("Invalid wallet policy type, it must be WalletPolicy") if wallet.descriptor_template not in ["pkh(@0/**)", "pkh(@0/<0;1>/*)", "wpkh(@0/**)", "wpkh(@0/<0;1>/*)", "sh(wpkh(@0/**))", "sh(wpkh(@0/<0;1>/*))"]: raise NotImplementedError("Unsupported policy") psbt = normalize_psbt(psbt) # the rest of the code is basically the HWI code, and it ignores wallet tx = psbt #c_tx = tx.get_unsigned_tx() c_tx = tx.tx tx_bytes = c_tx.serialize_with_witness() # Master key fingerprint master_fpr = hash160(compress_public_key(self.app.getWalletPublicKey('')["publicKey"]))[:4] # An entry per input, each with 0 to many keys to sign with all_signature_attempts: List[List[Tuple[str, bytes]]] = [[]] * len(c_tx.vin) # Get the app version to determine whether to use Trusted Input for segwit version = self.app.getFirmwareVersion() use_trusted_segwit = (version['major_version'] == 1 and version['minor_version'] >= 4) or version['major_version'] > 1 # NOTE: We only support signing Segwit inputs, where we can skip over non-segwit # inputs, or non-segwit inputs, where *all* inputs are non-segwit. This is due # to Ledger's mutually exclusive signing steps for each type. segwit_inputs = [] # Legacy style inputs legacy_inputs = [] has_segwit = False has_legacy = False script_codes: List[bytes] = [b""] * len(c_tx.vin) # Detect changepath, (p2sh-)p2(w)pkh only change_path = '' for txout, i_num in zip(c_tx.vout, range(len(c_tx.vout))): # Find which wallet key could be change based on hdsplit: m/.../1/k # Wallets shouldn't be sending to change address as user action # otherwise this will get confused for pubkey, origin in tx.outputs[i_num].hd_keypaths.items(): if origin.fingerprint == master_fpr and len(origin.path) > 1 and origin.path[-2] == 1: # For possible matches, check if pubkey matches possible template if hash160(pubkey) in txout.scriptPubKey or hash160(bytearray.fromhex("0014") + hash160(pubkey)) in txout.scriptPubKey: change_path = '' for index in origin.path: change_path += str(index) + "/" change_path = change_path[:-1] for txin, psbt_in, i_num in zip(c_tx.vin, tx.inputs, range(len(c_tx.vin))): seq_hex = txin.nSequence.to_bytes(4, byteorder="little").hex() scriptcode = b"" utxo = None if psbt_in.witness_utxo: utxo = psbt_in.witness_utxo if psbt_in.non_witness_utxo: if txin.prevout.hash != psbt_in.non_witness_utxo.sha256: raise ValueError('Input {} has a non_witness_utxo with the wrong hash'.format(i_num)) utxo = psbt_in.non_witness_utxo.vout[txin.prevout.n] if utxo is None: raise Exception("PSBT is missing input utxo information, cannot sign") scriptcode = utxo.scriptPubKey if is_p2sh(scriptcode): if len(psbt_in.redeem_script) == 0: continue scriptcode = psbt_in.redeem_script is_wit, _, _ = is_witness(scriptcode) segwit_inputs.append({"value": txin.prevout.serialize() + struct.pack(" bytes: master_pubkey = self.app.getWalletPublicKey("") return hash160(compress_public_key(master_pubkey["publicKey"]))[:4] def sign_message(self, message: Union[str, bytes], keypath: str) -> str: # copied verbatim from HWI if not check_keypath(keypath): raise ValueError("Invalid keypath") if isinstance(message, str): message = bytearray(message, 'utf-8') else: message = bytearray(message) keypath = keypath[2:] # First display on screen what address you're signing for self.app.getWalletPublicKey(keypath, True) self.app.signMessagePrepare(keypath, message) signature = self.app.signMessageSign() # Make signature into standard bitcoin format rLength = signature[3] r = int.from_bytes(signature[4: 4 + rLength], byteorder="big", signed=True) s = int.from_bytes(signature[4 + rLength + 2:], byteorder="big", signed=True) sig = bytearray(chr(27 + 4 + (signature[0] & 0x01)), 'utf8') + r.to_bytes(32, byteorder="big", signed=False) + s.to_bytes(32, byteorder="big", signed=False) return base64.b64encode(sig).decode('utf-8') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698934586.0 ledger_bitcoin-0.4.0/ledger_bitcoin/command_builder.py0000664000175000017500000001346114520727472023032 0ustar00singalasingalaimport enum from typing import List, Tuple, Mapping, Union, Iterator, Optional from .common import bip32_path_from_string, write_varint from .merkle import get_merkleized_map_commitment, MerkleTree, element_hash from .wallet import WalletPolicy # p2 encodes the protocol version implemented CURRENT_PROTOCOL_VERSION = 1 def chunkify(data: bytes, chunk_len: int) -> Iterator[Tuple[bool, bytes]]: size: int = len(data) if size <= chunk_len: yield True, data return chunk: int = size // chunk_len remaining: int = size % chunk_len offset: int = 0 for i in range(chunk): yield False, data[offset: offset + chunk_len] offset += chunk_len if remaining: yield True, data[offset:] class DefaultInsType(enum.IntEnum): GET_VERSION = 0x01 class BitcoinInsType(enum.IntEnum): GET_EXTENDED_PUBKEY = 0x00 REGISTER_WALLET = 0x02 GET_WALLET_ADDRESS = 0x03 SIGN_PSBT = 0x04 GET_MASTER_FINGERPRINT = 0x05 SIGN_MESSAGE = 0x10 class FrameworkInsType(enum.IntEnum): CONTINUE_INTERRUPTED = 0x01 class BitcoinCommandBuilder: """APDU command builder for the Bitcoin application.""" CLA_DEFAULT: int = 0xB0 CLA_BITCOIN: int = 0xE1 CLA_FRAMEWORK: int = 0xF8 def serialize( self, cla: int, ins: Union[int, enum.IntEnum], p1: int = 0, p2: int = CURRENT_PROTOCOL_VERSION, cdata: bytes = b"", ) -> dict: """Serialize the whole APDU command (header + data). Parameters ---------- cla : int Instruction class: CLA (1 byte) ins : Union[int, IntEnum] Instruction code: INS (1 byte) p1 : int Instruction parameter 1: P1 (1 byte). p2 : int Instruction parameter 2: P2 (1 byte). cdata : bytes Bytes of command data. Returns ------- dict Dictionary representing the APDU message. """ return {"cla": cla, "ins": ins, "p1": p1, "p2": p2, "data": cdata} def get_extended_pubkey(self, bip32_path: str, display: bool = False): bip32_path: List[bytes] = bip32_path_from_string(bip32_path) cdata: bytes = b"".join([ b'\1' if display else b'\0', len(bip32_path).to_bytes(1, byteorder="big"), *bip32_path ]) return self.serialize( cla=self.CLA_BITCOIN, ins=BitcoinInsType.GET_EXTENDED_PUBKEY, cdata=cdata, ) def register_wallet(self, wallet: WalletPolicy): wallet_bytes = wallet.serialize() return self.serialize( cla=self.CLA_BITCOIN, ins=BitcoinInsType.REGISTER_WALLET, cdata=write_varint(len(wallet_bytes)) + wallet_bytes, ) def get_wallet_address( self, wallet: WalletPolicy, wallet_hmac: Optional[bytes], address_index: int, change: bool, display: bool, ): cdata: bytes = b"".join( [ b'\1' if display else b'\0', # 1 byte wallet.id, # 32 bytes wallet_hmac if wallet_hmac is not None else b'\0' * 32, # 32 bytes b"\1" if change else b"\0", # 1 byte address_index.to_bytes(4, byteorder="big"), # 4 bytes ] ) return self.serialize( cla=self.CLA_BITCOIN, ins=BitcoinInsType.GET_WALLET_ADDRESS, cdata=cdata, ) def sign_psbt( self, global_mapping: Mapping[bytes, bytes], input_mappings: List[Mapping[bytes, bytes]], output_mappings: List[Mapping[bytes, bytes]], wallet: WalletPolicy, wallet_hmac: Optional[bytes], ): cdata = bytearray() cdata += get_merkleized_map_commitment(global_mapping) cdata += write_varint(len(input_mappings)) cdata += MerkleTree( [ element_hash(get_merkleized_map_commitment(m_in)) for m_in in input_mappings ] ).root cdata += write_varint(len(output_mappings)) cdata += MerkleTree( [ element_hash(get_merkleized_map_commitment(m_out)) for m_out in output_mappings ] ).root cdata += wallet.id cdata += wallet_hmac if wallet_hmac is not None else b'\0' * 32 return self.serialize( cla=self.CLA_BITCOIN, ins=BitcoinInsType.SIGN_PSBT, cdata=bytes(cdata) ) def get_master_fingerprint(self): return self.serialize( cla=self.CLA_BITCOIN, ins=BitcoinInsType.GET_MASTER_FINGERPRINT ) def sign_message(self, message: bytes, bip32_path: str): cdata = bytearray() bip32_path: List[bytes] = bip32_path_from_string(bip32_path) # split message in 64-byte chunks (last chunk can be smaller) n_chunks = (len(message) + 63) // 64 chunks = [message[64 * i: 64 * i + 64] for i in range(n_chunks)] cdata += len(bip32_path).to_bytes(1, byteorder="big") cdata += b''.join(bip32_path) cdata += write_varint(len(message)) cdata += MerkleTree(element_hash(c) for c in chunks).root return self.serialize( cla=self.CLA_BITCOIN, ins=BitcoinInsType.SIGN_MESSAGE, cdata=bytes(cdata) ) def continue_interrupted(self, cdata: bytes): """Command builder for CONTINUE. Returns ------- bytes APDU command for CONTINUE. """ return self.serialize( cla=self.CLA_FRAMEWORK, ins=FrameworkInsType.CONTINUE_INTERRUPTED, cdata=cdata, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716206848.0 ledger_bitcoin-0.4.0/ledger_bitcoin/common.py0000664000175000017500000001112014622636400021155 0ustar00singalasingalafrom io import BytesIO from typing import List, Optional, Literal from enum import Enum from typing import Union import hashlib UINT64_MAX: int = 18446744073709551615 UINT32_MAX: int = 4294967295 UINT16_MAX: int = 65535 # from bitcoin-core/HWI class Chain(Enum): """ The blockchain network to use """ MAIN = 0 #: Bitcoin Main network TEST = 1 #: Bitcoin Test network REGTEST = 2 #: Bitcoin Core Regression Test network SIGNET = 3 #: Bitcoin Signet def __str__(self) -> str: return self.name.lower() def __repr__(self) -> str: return str(self) @staticmethod def argparse(s: str) -> Union['Chain', str]: try: return Chain[s.upper()] except KeyError: return s # from bitcoin-core/HWI class AddressType(Enum): """ The type of address to use """ LEGACY = 1 #: Legacy address type. P2PKH for single sig, P2SH for scripts. WIT = 2 #: Native segwit v0 address type. P2WPKH for single sig, P2WPSH for scripts. SH_WIT = 3 #: Nested segwit v0 address type. P2SH-P2WPKH for single sig, P2SH-P2WPSH for scripts. TAP = 4 #: Segwit v1 Taproot address type. P2TR always. def __str__(self) -> str: return self.name.lower() def __repr__(self) -> str: return str(self) @staticmethod def argparse(s: str) -> Union['AddressType', str]: try: return AddressType[s.upper()] except KeyError: return s def bip32_path_from_string(path: str) -> List[bytes]: splitted_path: List[str] = path.split("/") if not splitted_path: raise Exception(f"BIP32 path format error: '{path}'") if "m" in splitted_path and splitted_path[0] == "m": splitted_path = splitted_path[1:] return [int(p).to_bytes(4, byteorder="big") if "'" not in p else (0x80000000 | int(p[:-1])).to_bytes(4, byteorder="big") for p in splitted_path] def write_varint(n: int) -> bytes: if n <= 0xFC: return n.to_bytes(1, byteorder="little") if n <= UINT16_MAX: return b"\xFD" + n.to_bytes(2, byteorder="little") if n <= UINT32_MAX: return b"\xFE" + n.to_bytes(4, byteorder="little") if n <= UINT64_MAX: return b"\xFF" + n.to_bytes(8, byteorder="little") raise ValueError(f"Can't write to varint: '{n}'!") def read_varint(buf: BytesIO, prefix: Optional[bytes] = None) -> int: b: bytes = prefix if prefix else buf.read(1) if not b: raise ValueError(f"Can't read prefix: '{b}'!") n: int = {b"\xfd": 2, b"\xfe": 4, b"\xff": 8}.get(b, 1) # default to 1 b = buf.read(n) if n > 1 else b if len(b) != n: raise ValueError("Can't read varint!") return int.from_bytes(b, byteorder="little") def read_uint(buf: BytesIO, bit_len: int, byteorder: Literal['big', 'little'] = 'little') -> int: size: int = bit_len // 8 b: bytes = buf.read(size) if len(b) < size: raise ValueError(f"Can't read u{bit_len} in buffer!") return int.from_bytes(b, byteorder) def serialize_str(value: str) -> bytes: return len(value.encode()).to_bytes(1, byteorder="big") + value.encode() def ripemd160(x: bytes) -> bytes: try: h = hashlib.new("ripemd160") h.update(x) return h.digest() except BaseException: # ripemd160 is not always present in hashlib. # Fallback to custom implementation if missing. from . import ripemd return ripemd.ripemd160(x) def sha256(s: bytes) -> bytes: return hashlib.new('sha256', s).digest() def hash160(s: bytes) -> bytes: return ripemd160(sha256(s)) def hash256(s: bytes) -> bytes: return sha256(sha256(s)) class ByteStreamParser: def __init__(self, input: bytes): self.stream = BytesIO(input) def assert_empty(self) -> bytes: if self.stream.read(1) != b'': raise ValueError("Byte stream was expected to be empty") def read_bytes(self, n: int) -> bytes: result = self.stream.read(n) if len(result) < n: raise ValueError("Byte stream exhausted") return result def read_uint(self, n: int, byteorder: Literal['big', 'little'] = "big") -> int: return int.from_bytes(self.read_bytes(n), byteorder) def read_varint(self) -> int: prefix = self.read_uint(1) if prefix == 253: return self.read_uint(2, 'little') elif prefix == 254: return self.read_uint(4, 'little') elif prefix == 255: return self.read_uint(8, 'little') else: return prefix ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741351045.8503704 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/0000775000175000017500000000000014762564206020431 5ustar00singalasingala././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/__init__.py0000664000175000017500000000000014623570367022531 0ustar00singalasingala././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/base.py0000664000175000017500000000662014623570367021722 0ustar00singalasingala"""Base classes""" from io import BytesIO from binascii import hexlify, unhexlify class EmbitError(Exception): """Generic Embit error""" pass class EmbitBase: @classmethod def read_from(cls, stream, *args, **kwargs): """All classes should be readable from stream""" raise NotImplementedError( "%s doesn't implement reading from stream" % cls.__name__ ) @classmethod def parse(cls, s: bytes, *args, **kwargs): """Parse raw bytes""" stream = BytesIO(s) res = cls.read_from(stream, *args, **kwargs) if len(stream.read(1)) > 0: raise EmbitError("Unexpected extra bytes") return res def write_to(self, stream, *args, **kwargs) -> int: """All classes should be writable to stream""" raise NotImplementedError( "%s doesn't implement writing to stream" % type(self).__name__ ) def serialize(self, *args, **kwargs) -> bytes: """Serialize instance to raw bytes""" stream = BytesIO() self.write_to(stream, *args, **kwargs) return stream.getvalue() def to_string(self, *args, **kwargs) -> str: """ String representation. If not implemented - uses hex or calls to_base58() method if defined. """ if hasattr(self, "to_base58"): res = self.to_base58(*args, **kwargs) if not isinstance(res, str): raise ValueError("to_base58() must return string") return res return hexlify(self.serialize(*args, **kwargs)).decode() @classmethod def from_string(cls, s, *args, **kwargs): """Create class instance from string""" if hasattr(cls, "from_base58"): return cls.from_base58(s, *args, **kwargs) return cls.parse(unhexlify(s)) def __str__(self): """Internally calls `to_string()` method with no arguments""" return self.to_string() def __repr__(self): try: return type(self).__name__ + "(%s)" % str(self) except: return type(self).__name__ + "()" def __eq__(self, other): """Compare two objects by checking their serializations""" if not hasattr(other, "serialize"): return False return self.serialize() == other.serialize() def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(self.serialize()) class EmbitKey(EmbitBase): def sec(self) -> bytes: """ Any EmbitKey should implement sec() method that returns a sec-serialized public key """ raise NotImplementedError( "%s doesn't implement sec() method" % type(self).__name__ ) def xonly(self) -> bytes: """xonly representation of the key""" return self.sec()[1:33] @property def is_private(self) -> bool: """ Any EmbitKey should implement `is_private` property to distinguish between private and public keys. """ raise NotImplementedError( "%s doesn't implement is_private property" % type(self).__name__ ) def __lt__(self, other): # for lexagraphic ordering return self.sec() < other.sec() def __gt__(self, other): # for lexagraphic ordering return self.sec() > other.sec() def __hash__(self): return hash(self.serialize()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/base58.py0000664000175000017500000000367214623570367022103 0ustar00singalasingala# Partially copy-pasted from python-bitcoinlib: # https://github.com/petertodd/python-bitcoinlib/blob/master/bitcoin/base58.py """Base58 encoding and decoding""" import binascii from . import hashes B58_DIGITS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" def encode(b: bytes) -> str: """Encode bytes to a base58-encoded string""" # Convert big-endian bytes to integer n = int("0x0" + binascii.hexlify(b).decode("utf8"), 16) # Divide that integer into bas58 chars = [] while n > 0: n, r = divmod(n, 58) chars.append(B58_DIGITS[r]) result = "".join(chars[::-1]) pad = 0 for c in b: if c == 0: pad += 1 else: break return B58_DIGITS[0] * pad + result def decode(s: str) -> bytes: """Decode a base58-encoding string, returning bytes""" if not s: return b"" # Convert the string to an integer n = 0 for c in s: n *= 58 if c not in B58_DIGITS: raise ValueError("Character %r is not a valid base58 character" % c) digit = B58_DIGITS.index(c) n += digit # Convert the integer to bytes h = "%x" % n if len(h) % 2: h = "0" + h res = binascii.unhexlify(h.encode("utf8")) # Add padding back. pad = 0 for c in s[:-1]: if c == B58_DIGITS[0]: pad += 1 else: break return b"\x00" * pad + res def encode_check(b: bytes) -> str: """Encode bytes to a base58-encoded string with a checksum""" return encode(b + hashes.double_sha256(b)[0:4]) def decode_check(s: str) -> bytes: """Decode a base58-encoding string with checksum check. Returns bytes without checksum """ b = decode(s) checksum = hashes.double_sha256(b[:-4])[:4] if b[-4:] != checksum: raise ValueError( "Checksum mismatch: expected %r, calculated %r" % (b[-4:], checksum) ) return b[:-4] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/bech32.py0000664000175000017500000001205514623570367022055 0ustar00singalasingala# Copyright (c) 2017 Pieter Wuille # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """Reference implementation for Bech32 and segwit addresses.""" from .misc import const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" BECH32_CONST = const(1) BECH32M_CONST = const(0x2BC830A3) class Encoding: """Enumeration type to list the various supported encodings.""" BECH32 = 1 BECH32M = 2 def bech32_polymod(values): """Internal function that computes the Bech32 checksum.""" generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3] chk = 1 for value in values: top = chk >> 25 chk = (chk & 0x1FFFFFF) << 5 ^ value for i in range(5): chk ^= generator[i] if ((top >> i) & 1) else 0 return chk def bech32_hrp_expand(hrp: str): """Expand the HRP into values for checksum computation.""" return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] def bech32_verify_checksum(hrp, data): """Verify a checksum given HRP and converted data characters.""" check = bech32_polymod(bech32_hrp_expand(hrp) + data) if check == BECH32_CONST: return Encoding.BECH32 elif check == BECH32M_CONST: return Encoding.BECH32M else: return None def bech32_create_checksum(encoding, hrp, data): """Compute the checksum values given HRP and data.""" values = bech32_hrp_expand(hrp) + data const = BECH32M_CONST if encoding == Encoding.BECH32M else BECH32_CONST polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] def bech32_encode(encoding, hrp, data): """Compute a Bech32 or Bech32m string given HRP and data values.""" combined = data + bech32_create_checksum(encoding, hrp, data) return hrp + "1" + "".join([CHARSET[d] for d in combined]) def bech32_decode(bech): """Validate a Bech32/Bech32m string, and determine HRP and data.""" if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or ( bech.lower() != bech and bech.upper() != bech ): return (None, None, None) bech = bech.lower() pos = bech.rfind("1") if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: return (None, None, None) if not all(x in CHARSET for x in bech[pos + 1 :]): return (None, None, None) hrp = bech[:pos] data = [CHARSET.find(x) for x in bech[pos + 1 :]] encoding = bech32_verify_checksum(hrp, data) if encoding is None: return (None, None, None) return (encoding, hrp, data[:-6]) def convertbits(data, frombits, tobits, pad=True): """General power-of-2 base conversion.""" acc = 0 bits = 0 ret = [] maxv = (1 << tobits) - 1 max_acc = (1 << (frombits + tobits - 1)) - 1 for value in data: if value < 0 or (value >> frombits): return None acc = ((acc << frombits) | value) & max_acc bits += frombits while bits >= tobits: bits -= tobits ret.append((acc >> bits) & maxv) if pad: if bits: ret.append((acc << (tobits - bits)) & maxv) elif bits >= frombits or ((acc << (tobits - bits)) & maxv): return None return ret def decode(hrp, addr): """Decode a segwit address.""" encoding, hrpgot, data = bech32_decode(addr) if hrpgot != hrp: return (None, None) decoded = convertbits(data[1:], 5, 8, False) if decoded is None or len(decoded) < 2 or len(decoded) > 40: return (None, None) if data[0] > 16: return (None, None) if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: return (None, None) if (data[0] == 0 and encoding != Encoding.BECH32) or ( data[0] != 0 and encoding != Encoding.BECH32M ): return (None, None) return (data[0], decoded) def encode(hrp, witver, witprog): """Encode a segwit address.""" encoding = Encoding.BECH32 if witver == 0 else Encoding.BECH32M ret = bech32_encode(encoding, hrp, [witver] + convertbits(witprog, 8, 5)) if decode(hrp, ret) == (None, None): return None return ret ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/bip32.py0000664000175000017500000002365514623570367021736 0ustar00singalasingalafrom . import ec from .base import EmbitKey, EmbitError from .misc import copy, const, secp256k1 from .networks import NETWORKS from . import base58 from . import hashes import hmac from binascii import hexlify HARDENED_INDEX = const(0x80000000) class HDError(EmbitError): pass class HDKey(EmbitKey): """HD Private or Public key""" def __init__( self, key: EmbitKey, # more specifically, PrivateKey or PublicKey chain_code: bytes, version=None, depth: int = 0, fingerprint: bytes = b"\x00\x00\x00\x00", child_number: int = 0, ): self.key = key if len(key.serialize()) != 32 and len(key.serialize()) != 33: raise HDError("Invalid key. Should be private or compressed public") if version is not None: self.version = version else: if len(key.serialize()) == 32: self.version = NETWORKS["main"]["xprv"] else: self.version = NETWORKS["main"]["xpub"] self.chain_code = chain_code self.depth = depth self.fingerprint = fingerprint self._my_fingerprint = b"" self.child_number = child_number # check that base58[1:4] is "prv" or "pub" if self.is_private and self.to_base58()[1:4] != "prv": raise HDError("Invalid version") if not self.is_private and self.to_base58()[1:4] != "pub": raise HDError("Invalid version") @classmethod def from_seed(cls, seed: bytes, version=NETWORKS["main"]["xprv"]): """Creates a root private key from 64-byte seed""" raw = hmac.new(b"Bitcoin seed", seed, digestmod="sha512").digest() private_key = ec.PrivateKey(raw[:32]) chain_code = raw[32:] return cls(private_key, chain_code, version=version) @classmethod def from_base58(cls, s: str): b = base58.decode_check(s) return cls.parse(b) @property def my_fingerprint(self) -> bytes: if not self._my_fingerprint: sec = self.sec() self._my_fingerprint = hashes.hash160(sec)[:4] return self._my_fingerprint @property def is_private(self) -> bool: """checks if the HDKey is private or public""" return self.key.is_private @property def secret(self): if not self.is_private: raise HDError("Key is not private") return self.key.secret def write_to(self, stream, version=None) -> int: if version is None: version = self.version res = stream.write(version) res += stream.write(bytes([self.depth])) res += stream.write(self.fingerprint) res += stream.write(self.child_number.to_bytes(4, "big")) res += stream.write(self.chain_code) if self.is_private: res += stream.write(b"\x00") res += stream.write(self.key.serialize()) return res def to_base58(self, version=None) -> str: b = self.serialize(version) res = base58.encode_check(b) if res[1:4] == "prv" and not self.is_private: raise HDError("Invalid version for private key") if res[1:4] == "pub" and self.is_private: raise HDError("Invalid version for public key") return res @classmethod def from_string(cls, s: str): return cls.from_base58(s) def to_string(self, version=None): return self.to_base58(version) @classmethod def read_from(cls, stream): version = stream.read(4) depth = stream.read(1)[0] fingerprint = stream.read(4) child_number = int.from_bytes(stream.read(4), "big") chain_code = stream.read(32) k = stream.read(33) if k[0] == 0: key = ec.PrivateKey.parse(k[1:]) else: key = ec.PublicKey.parse(k) if len(version) < 4 or len(fingerprint) < 4 or len(chain_code) < 32: raise HDError("Not enough bytes") hd = cls( key, chain_code, version=version, depth=depth, fingerprint=fingerprint, child_number=child_number, ) subver = hd.to_base58()[1:4] if subver != "prv" and subver != "pub": raise HDError("Invalid version") if depth == 0 and child_number != 0: raise HDError("zero depth with non-zero index") if depth == 0 and fingerprint != b"\x00\x00\x00\x00": raise HDError("zero depth with non-zero parent") return hd def to_public(self, version=None): if not self.is_private: raise HDError("Already public") if version is None: # detect network for net in NETWORKS: for k in NETWORKS[net]: if "prv" in k and NETWORKS[net][k] == self.version: # xprv -> xpub, zprv -> zpub etc version = NETWORKS[net][k.replace("prv", "pub")] break if version is None: raise HDError("Can't find proper version. Provide it with version keyword") return self.__class__( self.key.get_public_key(), self.chain_code, version=version, depth=self.depth, fingerprint=self.fingerprint, child_number=self.child_number, ) def get_public_key(self): return self.key.get_public_key() if self.is_private else self.key def sec(self) -> bytes: """Returns SEC serialization of the public key""" return self.key.sec() def xonly(self) -> bytes: return self.key.xonly() def taproot_tweak(self, h=b""): return HDKey( self.key.taproot_tweak(h), self.chain_code, version=self.version, depth=self.depth, fingerprint=self.fingerprint, child_number=self.child_number, ) def child(self, index: int, hardened: bool = False): """Derives a child HDKey""" if index > 0xFFFFFFFF: raise HDError("Index should be less then 2^32") if hardened and index < HARDENED_INDEX: index += HARDENED_INDEX if index >= HARDENED_INDEX: hardened = True if hardened and not self.is_private: raise HDError("Can't do hardened with public key") # we need pubkey for fingerprint anyways sec = self.sec() fingerprint = hashes.hash160(sec)[:4] if hardened: data = b"\x00" + self.key.serialize() + index.to_bytes(4, "big") else: data = sec + index.to_bytes(4, "big") raw = hmac.new(self.chain_code, data, digestmod="sha512").digest() secret = raw[:32] chain_code = raw[32:] if self.is_private: secret = secp256k1.ec_privkey_add(secret, self.key.serialize()) key = ec.PrivateKey(secret) else: # copy of internal secp256k1 point structure point = copy(self.key._point) point = secp256k1.ec_pubkey_add(point, secret) key = ec.PublicKey(point) return HDKey( key, chain_code, version=self.version, depth=self.depth + 1, fingerprint=fingerprint, child_number=index, ) def derive(self, path): """path: int array or a string starting with m/""" if isinstance(path, str): # string of the form m/44h/0'/ind path = parse_path(path) child = self for idx in path: child = child.child(idx) return child def sign(self, msg_hash: bytes) -> ec.Signature: """signs a hash of the message with the private key""" if not self.is_private: raise HDError("HD public key can't sign") return self.key.sign(msg_hash) def schnorr_sign(self, msg_hash): if not self.is_private: raise HDError("HD public key can't sign") return self.key.schnorr_sign(msg_hash) def verify(self, sig, msg_hash) -> bool: return self.key.verify(sig, msg_hash) def schnorr_verify(self, sig, msg_hash) -> bool: return self.key.schnorr_verify(sig, msg_hash) def __eq__(self, other): # skip version return self.serialize()[4:] == other.serialize()[4:] def __hash__(self): return hash(self.serialize()) def detect_version(path, default="xprv", network=None) -> bytes: """ Detects slip-132 version from the path for certain network. Trying to be smart, use if you want, but with care. """ key = default net = network if network is None: net = NETWORKS["main"] if isinstance(path, str): path = parse_path(path) if len(path) == 0: return network[key] if path[0] == HARDENED_INDEX + 84: key = "z" + default[1:] elif path[0] == HARDENED_INDEX + 49: key = "y" + default[1:] elif path[0] == HARDENED_INDEX + 48: if len(path) >= 4: if path[3] == HARDENED_INDEX + 1: key = "Y" + default[1:] elif path[3] == HARDENED_INDEX + 2: key = "Z" + default[1:] if network is None and len(path) > 1 and path[1] == HARDENED_INDEX + 1: net = NETWORKS["test"] return net[key] def _parse_der_item(e: str) -> int: if e[-1] in {"h", "H", "'"}: return int(e[:-1]) + HARDENED_INDEX else: return int(e) def parse_path(path: str) -> list: """converts derivation path of the form m/44h/1'/0'/0/32 to int array""" arr = path.rstrip("/").split("/") if arr[0] == "m": arr = arr[1:] if len(arr) == 0: return [] return [_parse_der_item(e) for e in arr] def path_to_str(path: list, fingerprint=None) -> str: s = "m" if fingerprint is None else hexlify(fingerprint).decode() for el in path: if el >= HARDENED_INDEX: s += "/%dh" % (el - HARDENED_INDEX) else: s += "/%d" % el return s ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/compact.py0000664000175000017500000000215714623570367022437 0ustar00singalasingala""" Compact Int parsing / serialization """ import io def to_bytes(i: int) -> bytes: """encodes an integer as a compact int""" if i < 0: raise ValueError("integer can't be negative: {}".format(i)) order = 0 while i >> (8 * (2**order)): order += 1 if order == 0: if i < 0xFD: return bytes([i]) order = 1 if order > 3: raise ValueError("integer too large: {}".format(i)) return bytes([0xFC + order]) + i.to_bytes(2**order, "little") def from_bytes(b: bytes) -> int: s = io.BytesIO(b) res = read_from(s) if len(s.read(1)) > 0: raise ValueError("Too many bytes") return res def read_from(stream) -> int: """reads a compact integer from a stream""" c = stream.read(1) if not isinstance(c, bytes): raise TypeError("Bytes must be returned from stream.read()") if len(c) != 1: raise RuntimeError("Can't read one byte from the stream") i = c[0] if i >= 0xFD: bytes_to_read = 2 ** (i - 0xFC) return int.from_bytes(stream.read(bytes_to_read), "little") else: return i ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741351045.8503704 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/descriptor/0000775000175000017500000000000014762564206022607 5ustar00singalasingala././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/descriptor/__init__.py0000664000175000017500000000012714623570367024721 0ustar00singalasingalafrom . import miniscript from .descriptor import Descriptor from .arguments import Key ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/descriptor/arguments.py0000664000175000017500000003725014623570367025176 0ustar00singalasingalafrom binascii import hexlify, unhexlify from .base import DescriptorBase from .errors import ArgumentError from .. import bip32, ec, compact, hashes from ..bip32 import HARDENED_INDEX from ..misc import read_until class KeyOrigin: def __init__(self, fingerprint: bytes, derivation: list): self.fingerprint = fingerprint self.derivation = derivation @classmethod def from_string(cls, s: str): arr = s.split("/") mfp = unhexlify(arr[0]) assert len(mfp) == 4 arr[0] = "m" path = "/".join(arr) derivation = bip32.parse_path(path) return cls(mfp, derivation) def __str__(self): return bip32.path_to_str(self.derivation, fingerprint=self.fingerprint) class AllowedDerivation(DescriptorBase): # xpub/<0;1>/* - <0;1> is a set of allowed branches, wildcard * is stored as None def __init__(self, indexes=[[0, 1], None]): # check only one wildcard if ( len( [i for i in indexes if i is None or (isinstance(i, list) and None in i)] ) > 1 ): raise ArgumentError("Only one wildcard is allowed") # check only one set is in the derivation if len([i for i in indexes if isinstance(i, list)]) > 1: raise ArgumentError("Only one set of branches is allowed") self.indexes = indexes @property def is_wildcard(self): return None in self.indexes def fill(self, idx, branch_index=None): # None is ok if idx is not None and (idx < 0 or idx >= HARDENED_INDEX): raise ArgumentError("Hardened indexes are not allowed in wildcard") arr = [i for i in self.indexes] for i, el in enumerate(arr): if el is None: arr[i] = idx if isinstance(el, list): if branch_index is None: arr[i] = el[0] else: if branch_index < 0 or branch_index >= len(el): raise ArgumentError("Invalid branch index") arr[i] = el[branch_index] return arr def branch(self, branch_index): arr = self.fill(None, branch_index) return type(self)(arr) def check_derivation(self, derivation: list): if len(derivation) != len(self.indexes): return None branch_idx = 0 # default branch if no branches in descriptor idx = None for i, el in enumerate(self.indexes): der = derivation[i] if isinstance(el, int): if el != der: return None # branch elif isinstance(el, list): if der not in el: return None branch_idx = el.index(der) # wildcard elif el is None: idx = der # shouldn't happen else: raise ArgumentError("Strange derivation index...") if branch_idx is not None and idx is not None: return idx, branch_idx @classmethod def default(cls): return AllowedDerivation([[0, 1], None]) @property def branches(self): for el in self.indexes: if isinstance(el, list): return el return None @property def has_hardend(self): for idx in self.indexes: if isinstance(idx, int) and idx >= HARDENED_INDEX: return True if ( isinstance(idx, list) and len([i for i in idx if i >= HARDENED_INDEX]) > 0 ): return True return False @classmethod def from_string(cls, der: str, allow_hardened=False, allow_set=True): if len(der) == 0: return None indexes = [ cls.parse_element(d, allow_hardened, allow_set) for d in der.split("/") ] return cls(indexes) @classmethod def parse_element(cls, d: str, allow_hardened=False, allow_set=True): # wildcard if d == "*": return None # branch set - legacy `{m,n}` if d[0] == "{" and d[-1] == "}": if not allow_set: raise ArgumentError("Set is not allowed in derivation %s" % d) return [ cls.parse_element(dd, allow_hardened, allow_set=False) for dd in d[1:-1].split(",") ] # branch set - multipart `` if d[0] == "<" and d[-1] == ">": if not allow_set: raise ArgumentError("Set is not allowed in derivation %s" % d) return [ cls.parse_element(dd, allow_hardened, allow_set=False) for dd in d[1:-1].split(";") ] idx = 0 if d[-1] in ["h", "H", "'"]: if not allow_hardened: raise ArgumentError("Hardened derivation is not allowed in %s" % d) idx = HARDENED_INDEX d = d[:-1] i = int(d) if i < 0 or i >= HARDENED_INDEX: raise ArgumentError( "Derivation index can be in a range [0, %d)" % HARDENED_INDEX ) return idx + i def __str__(self): r = "" for idx in self.indexes: if idx is None: r += "/*" if isinstance(idx, int): if idx >= HARDENED_INDEX: r += "/%dh" % (idx - HARDENED_INDEX) else: r += "/%d" % idx if isinstance(idx, list): r += "/<" r += ";".join( [ str(i) if i < HARDENED_INDEX else str(i - HARDENED_INDEX) + "h" for i in idx ] ) r += ">" return r class Key(DescriptorBase): def __init__( self, key, origin=None, derivation=None, taproot=False, xonly_repr=False, ): self.origin = origin self.key = key self.taproot = taproot self.xonly_repr = xonly_repr and taproot if not hasattr(key, "derive") and derivation: raise ArgumentError("Key %s doesn't support derivation" % key) self.allowed_derivation = derivation def __len__(self): return 34 - int(self.taproot) # <33:sec> or <32:xonly> @property def my_fingerprint(self): if self.is_extended: return self.key.my_fingerprint return None @property def fingerprint(self): if self.origin is not None: return self.origin.fingerprint else: if self.is_extended: return self.key.my_fingerprint return None @property def derivation(self): return [] if self.origin is None else self.origin.derivation @classmethod def read_from(cls, s, taproot: bool = False): """ Reads key argument from stream. If taproot is set to True - allows both x-only and sec pubkeys. If taproot is False - will raise when finds xonly pubkey. """ first = s.read(1) origin = None if first == b"[": prefix, char = read_until(s, b"]") if char != b"]": raise ArgumentError("Invalid key - missing ]") origin = KeyOrigin.from_string(prefix.decode()) else: s.seek(-1, 1) k, char = read_until(s, b",)/") der = b"" # there is a following derivation if char == b"/": der, char = read_until(s, b"<{,)") # legacy branches: {a,b,c...} if char == b"{": der += b"{" branch, char = read_until(s, b"}") if char is None: raise ArgumentError("Failed reading the key, missing }") der += branch + b"}" rest, char = read_until(s, b",)") der += rest # multipart descriptor: elif char == b"<": der += b"<" branch, char = read_until(s, b">") if char is None: raise ArgumentError("Failed reading the key, missing >") der += branch + b">" rest, char = read_until(s, b",)") der += rest if char is not None: s.seek(-1, 1) # parse key k, xonly_repr = cls.parse_key(k, taproot) # parse derivation allow_hardened = isinstance(k, bip32.HDKey) and isinstance(k.key, ec.PrivateKey) derivation = AllowedDerivation.from_string( der.decode(), allow_hardened=allow_hardened ) return cls(k, origin, derivation, taproot, xonly_repr) @classmethod def parse_key(cls, key: bytes, taproot: bool = False): # convert to string k = key.decode() if len(k) in [66, 130] and k[:2] in ["02", "03", "04"]: # bare public key return ec.PublicKey.parse(unhexlify(k)), False elif taproot and len(k) == 64: # x-only pubkey return ec.PublicKey.parse(b"\x02" + unhexlify(k)), True elif k[1:4] in ["pub", "prv"]: # bip32 key return bip32.HDKey.from_base58(k), False else: return ec.PrivateKey.from_wif(k), False @property def is_extended(self): return isinstance(self.key, bip32.HDKey) def check_derivation(self, derivation_path): rest = None # full derivation path if self.fingerprint == derivation_path.fingerprint: origin = self.derivation if origin == derivation_path.derivation[: len(origin)]: rest = derivation_path.derivation[len(origin) :] # short derivation path if self.my_fingerprint == derivation_path.fingerprint: rest = derivation_path.derivation if self.allowed_derivation is None or rest is None: return None return self.allowed_derivation.check_derivation(rest) def get_public_key(self): return ( self.key.get_public_key() if (self.is_extended or self.is_private) else self.key ) def sec(self): return self.key.sec() def xonly(self): return self.key.xonly() def taproot_tweak(self, h=b""): assert self.taproot return self.key.taproot_tweak(h) def serialize(self): if self.taproot: return self.sec()[1:33] return self.sec() def compile(self): d = self.serialize() return compact.to_bytes(len(d)) + d @property def prefix(self): if self.origin: return "[%s]" % self.origin return "" @property def suffix(self): return "" if self.allowed_derivation is None else str(self.allowed_derivation) @property def can_derive(self): return self.allowed_derivation is not None and hasattr(self.key, "derive") @property def branches(self): return self.allowed_derivation.branches if self.allowed_derivation else None @property def num_branches(self): return 1 if self.branches is None else len(self.branches) def branch(self, branch_index=None): der = ( self.allowed_derivation.branch(branch_index) if self.allowed_derivation is not None else None ) return type(self)(self.key, self.origin, der, self.taproot) @property def is_wildcard(self): return self.allowed_derivation.is_wildcard if self.allowed_derivation else False def derive(self, idx, branch_index=None): # nothing to derive if self.allowed_derivation is None: return self der = self.allowed_derivation.fill(idx, branch_index=branch_index) k = self.key.derive(der) if self.origin: origin = KeyOrigin(self.origin.fingerprint, self.origin.derivation + der) else: origin = KeyOrigin(self.key.child(0).fingerprint, der) # empty derivation derivation = None return type(self)(k, origin, derivation, self.taproot) @property def is_private(self): return isinstance(self.key, ec.PrivateKey) or ( self.is_extended and self.key.is_private ) def to_public(self): if not self.is_private: return self if isinstance(self.key, ec.PrivateKey): return type(self)( self.key.get_public_key(), self.origin, self.allowed_derivation, self.taproot, ) else: return type(self)( self.key.to_public(), self.origin, self.allowed_derivation, self.taproot ) @property def private_key(self): if not self.is_private: raise ArgumentError("Key is not private") # either HDKey.key or just the key return self.key.key if self.is_extended else self.key @property def secret(self): return self.private_key.secret def to_string(self, version=None): if isinstance(self.key, ec.PublicKey): k = self.key.sec() if not self.xonly_repr else self.key.xonly() return self.prefix + hexlify(k).decode() if isinstance(self.key, bip32.HDKey): return self.prefix + self.key.to_base58(version) + self.suffix if isinstance(self.key, ec.PrivateKey): return self.prefix + self.key.wif() return self.prefix + self.key @classmethod def from_string(cls, s, taproot=False): return cls.parse(s.encode(), taproot) class KeyHash(Key): @classmethod def parse_key(cls, k: bytes, *args, **kwargs): # convert to string kd = k.decode() # raw 20-byte hash if len(kd) == 40: return kd, False return super().parse_key(k, *args, **kwargs) def serialize(self, *args, **kwargs): if isinstance(self.key, str): return unhexlify(self.key) # TODO: should it be xonly? if self.taproot: return hashes.hash160(self.key.sec()[1:33]) return hashes.hash160(self.key.sec()) def __len__(self): return 21 # <20:pkh> def compile(self): d = self.serialize() return compact.to_bytes(len(d)) + d class Number(DescriptorBase): def __init__(self, num): self.num = num @classmethod def read_from(cls, s, taproot=False): num = 0 char = s.read(1) while char in b"0123456789": num = 10 * num + int(char.decode()) char = s.read(1) s.seek(-1, 1) return cls(num) def compile(self): if self.num == 0: return b"\x00" if self.num <= 16: return bytes([80 + self.num]) b = self.num.to_bytes(32, "little").rstrip(b"\x00") if b[-1] >= 128: b += b"\x00" return bytes([len(b)]) + b def __len__(self): return len(self.compile()) def __str__(self): return "%d" % self.num class Raw(DescriptorBase): LEN = 32 def __init__(self, raw): if len(raw) != self.LEN * 2: raise ArgumentError("Invalid raw element length: %d" % len(raw)) self.raw = unhexlify(raw) @classmethod def read_from(cls, s, taproot=False): return cls(s.read(2 * cls.LEN).decode()) def __str__(self): return hexlify(self.raw).decode() def compile(self): return compact.to_bytes(len(self.raw)) + self.raw def __len__(self): return len(compact.to_bytes(self.LEN)) + self.LEN class Raw32(Raw): LEN = 32 def __len__(self): return 33 class Raw20(Raw): LEN = 20 def __len__(self): return 21 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/descriptor/base.py0000664000175000017500000000113614623570367024075 0ustar00singalasingalafrom io import BytesIO from ..base import EmbitBase class DescriptorBase(EmbitBase): """ Descriptor is purely text-based, so parse/serialize do the same as from/to_string, just returning ascii bytes instead of ascii string. """ @classmethod def from_string(cls, s: str, *args, **kwargs): return cls.parse(s.encode(), *args, **kwargs) def serialize(self, *args, **kwargs) -> bytes: stream = BytesIO() self.write_to(stream) return stream.getvalue() def to_string(self, *args, **kwargs) -> str: return self.serialize().decode() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/descriptor/checksum.py0000664000175000017500000000262014623570367024764 0ustar00singalasingalafrom .errors import DescriptorError def polymod(c: int, val: int) -> int: c0 = c >> 35 c = ((c & 0x7FFFFFFFF) << 5) ^ val if c0 & 1: c ^= 0xF5DEE51989 if c0 & 2: c ^= 0xA9FDCA3312 if c0 & 4: c ^= 0x1BAB10E32D if c0 & 8: c ^= 0x3706B1677A if c0 & 16: c ^= 0x644D626FFD return c def checksum(desc: str) -> str: """Calculate checksum of desciptor string""" INPUT_CHARSET = ( "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVW" 'XYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#"\\ ' ) CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" c = 1 cls = 0 clscount = 0 for ch in desc: pos = INPUT_CHARSET.find(ch) if pos == -1: raise DescriptorError("Invalid character '%s' in the input string" % ch) c = polymod(c, pos & 31) cls = cls * 3 + (pos >> 5) clscount += 1 if clscount == 3: c = polymod(c, cls) cls = 0 clscount = 0 if clscount > 0: c = polymod(c, cls) for j in range(0, 8): c = polymod(c, 0) c ^= 1 ret = [CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31] for j in range(0, 8)] return "".join(ret) def add_checksum(desc: str) -> str: """Add checksum to descriptor string""" if "#" in desc: desc = desc.split("#")[0] return desc + "#" + checksum(desc) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/descriptor/descriptor.py0000664000175000017500000002762614623570367025355 0ustar00singalasingalafrom io import BytesIO from .. import script from ..networks import NETWORKS from .errors import DescriptorError from .base import DescriptorBase from .miniscript import Miniscript, Multi, Sortedmulti from .arguments import Key from .taptree import TapTree class Descriptor(DescriptorBase): def __init__( self, miniscript=None, sh=False, wsh=True, key=None, wpkh=True, taproot=False, taptree=None, ): # TODO: add support for taproot scripts # Should: # - accept taptree without a key # - accept key without taptree # - raise if miniscript is not None, but taproot=True # - raise if taptree is not None, but taproot=False if key is None and miniscript is None and taptree is None: raise DescriptorError("Provide a key, miniscript or taptree") if miniscript is not None: # will raise if can't verify miniscript.verify() if miniscript.type != "B": raise DescriptorError("Top level miniscript should be 'B'") # check all branches have the same length branches = { len(k.branches) for k in miniscript.keys if k.branches is not None } if len(branches) > 1: raise DescriptorError("All branches should have the same length") self.sh = sh self.wsh = wsh self.key = key self.miniscript = miniscript self.wpkh = wpkh self.taproot = taproot self.taptree = taptree or TapTree() # make sure all keys are either taproot or not for k in self.keys: k.taproot = taproot @property def script_len(self): if self.taproot: return 34 # OP_1 <32:xonly> if self.miniscript: return len(self.miniscript) if self.wpkh: return 22 # 00 <20:pkh> return 25 # OP_DUP OP_HASH160 <20:pkh> OP_EQUALVERIFY OP_CHECKSIG @property def num_branches(self): return max([k.num_branches for k in self.keys]) def branch(self, branch_index=None): if self.miniscript: return type(self)( self.miniscript.branch(branch_index), self.sh, self.wsh, None, self.wpkh, self.taproot, ) else: return type(self)( None, self.sh, self.wsh, self.key.branch(branch_index), self.wpkh, self.taproot, self.taptree.branch(branch_index), ) @property def is_wildcard(self): return any([key.is_wildcard for key in self.keys]) @property def is_wrapped(self): return self.sh and self.is_segwit @property def is_legacy(self): return not (self.is_segwit or self.is_taproot) @property def is_segwit(self): return ( (self.wsh and self.miniscript) or (self.wpkh and self.key) or self.taproot ) @property def is_pkh(self): return self.key is not None and not self.taproot @property def is_taproot(self): return self.taproot @property def is_basic_multisig(self) -> bool: # TODO: should be true for taproot basic multisig with NUMS as internal key # Sortedmulti is subclass of Multi return bool(self.miniscript and isinstance(self.miniscript, Multi)) @property def is_sorted(self) -> bool: return bool(self.is_basic_multisig and isinstance(self.miniscript, Sortedmulti)) def scriptpubkey_type(self): if self.is_taproot: return "p2tr" if self.sh: return "p2sh" if self.is_pkh: if self.is_legacy: return "p2pkh" if self.is_segwit: return "p2wpkh" else: return "p2wsh" @property def brief_policy(self): if self.taptree: return "taptree" if self.key: return "single key" if self.is_basic_multisig: return ( str(self.miniscript.args[0]) + " of " + str(len(self.keys)) + " multisig" + (" (sorted)" if self.is_sorted else "") ) return "miniscript" @property def full_policy(self): if (self.key and not self.taptree) or self.is_basic_multisig: return self.brief_policy s = str(self.miniscript or self) for i, k in enumerate(self.keys): s = s.replace(str(k), chr(65 + i)) return s def derive(self, idx, branch_index=None): if self.miniscript: return type(self)( self.miniscript.derive(idx, branch_index), self.sh, self.wsh, None, self.wpkh, self.taproot, ) else: return type(self)( None, self.sh, self.wsh, self.key.derive(idx, branch_index), self.wpkh, self.taproot, self.taptree.derive(idx, branch_index), ) def to_public(self): if self.miniscript: return type(self)( self.miniscript.to_public(), self.sh, self.wsh, None, self.wpkh, self.taproot, ) else: return type(self)( None, self.sh, self.wsh, self.key.to_public(), self.wpkh, self.taproot, self.taptree.to_public(), ) def owns(self, psbt_scope): """Checks if psbt input or output belongs to this descriptor""" # we can't check if we don't know script_pubkey if psbt_scope.script_pubkey is None: return False # quick check of script_pubkey type if psbt_scope.script_pubkey.script_type() != self.scriptpubkey_type(): return False for pub, der in psbt_scope.bip32_derivations.items(): # check of the fingerprints for k in self.keys: if not k.is_extended: continue res = k.check_derivation(der) if res: idx, branch_idx = res sc = self.derive(idx, branch_index=branch_idx).script_pubkey() # if derivation is found but scriptpubkey doesn't match - fail return sc == psbt_scope.script_pubkey for pub, (leafs, der) in psbt_scope.taproot_bip32_derivations.items(): # check of the fingerprints for k in self.keys: if not k.is_extended: continue res = k.check_derivation(der) if res: idx, branch_idx = res sc = self.derive(idx, branch_index=branch_idx).script_pubkey() # if derivation is found but scriptpubkey doesn't match - fail return sc == psbt_scope.script_pubkey return False def check_derivation(self, derivation_path): for k in self.keys: # returns a tuple branch_idx, idx der = k.check_derivation(derivation_path) if der is not None: return der return None def witness_script(self): if self.wsh and self.miniscript is not None: return script.Script(self.miniscript.compile()) def redeem_script(self): if not self.sh: return None if self.miniscript: if not self.wsh: return script.Script(self.miniscript.compile()) else: return script.p2wsh(script.Script(self.miniscript.compile())) else: return script.p2wpkh(self.key) def script_pubkey(self): # covers sh-wpkh, sh and sh-wsh if self.taproot: return script.p2tr(self.key, self.taptree) if self.sh: return script.p2sh(self.redeem_script()) if self.wsh: return script.p2wsh(self.witness_script()) if self.miniscript: return script.Script(self.miniscript.compile()) if self.wpkh: return script.p2wpkh(self.key) return script.p2pkh(self.key) def address(self, network=NETWORKS["main"]): return self.script_pubkey().address(network) @property def keys(self): if self.taptree and self.key: return [self.key] + self.taptree.keys elif self.taptree: return self.taptree.keys elif self.key: return [self.key] return self.miniscript.keys @classmethod def from_string(cls, desc): s = BytesIO(desc.encode()) res = cls.read_from(s) left = s.read() if len(left) > 0 and not left.startswith(b"#"): raise DescriptorError("Unexpected characters after descriptor: %r" % left) return res @classmethod def read_from(cls, s): # starts with sh(wsh()), sh() or wsh() start = s.read(7) sh = False wsh = False wpkh = False is_miniscript = True taproot = False taptree = TapTree() if start.startswith(b"tr("): taproot = True s.seek(-4, 1) elif start.startswith(b"sh(wsh("): sh = True wsh = True elif start.startswith(b"wsh("): sh = False wsh = True s.seek(-3, 1) elif start.startswith(b"sh(wpkh"): is_miniscript = False sh = True wpkh = True assert s.read(1) == b"(" elif start.startswith(b"wpkh("): is_miniscript = False wpkh = True s.seek(-2, 1) elif start.startswith(b"pkh("): is_miniscript = False s.seek(-3, 1) elif start.startswith(b"sh("): sh = True wsh = False s.seek(-4, 1) else: raise ValueError("Invalid descriptor (starts with '%s')" % start.decode()) # taproot always has a key, and may have taptree miniscript if taproot: miniscript = None key = Key.read_from(s, taproot=True) nbrackets = 1 c = s.read(1) # TODO: should it be ok to pass just taptree without a key? # check if we have taptree after the key if c != b",": s.seek(-1, 1) else: taptree = TapTree.read_from(s) elif is_miniscript: miniscript = Miniscript.read_from(s) key = None nbrackets = int(sh) + int(wsh) # single key for sure else: miniscript = None key = Key.read_from(s, taproot=taproot) nbrackets = 1 + int(sh) end = s.read(nbrackets) if end != b")" * nbrackets: raise ValueError( "Invalid descriptor (expected ')' but ends with '%s')" % end.decode() ) return cls( miniscript, sh=sh, wsh=wsh, key=key, wpkh=wpkh, taproot=taproot, taptree=taptree, ) def to_string(self): if self.taproot: if self.taptree: return "tr(%s,%s)" % (self.key, self.taptree) return "tr(%s)" % self.key if self.miniscript is not None: res = str(self.miniscript) if self.wsh: res = "wsh(%s)" % res else: if self.wpkh: res = "wpkh(%s)" % self.key else: res = "pkh(%s)" % self.key if self.sh: res = "sh(%s)" % res return res ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/descriptor/errors.py0000664000175000017500000000033214623570367024474 0ustar00singalasingalafrom ..base import EmbitError class DescriptorError(EmbitError): pass class MiniscriptError(DescriptorError): pass class ArgumentError(MiniscriptError): pass class KeyError(ArgumentError): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/descriptor/miniscript.py0000664000175000017500000006544314623570367025357 0ustar00singalasingalafrom ..misc import read_until from .errors import MiniscriptError from .base import DescriptorBase from .arguments import Key, KeyHash, Number, Raw32, Raw20 class Miniscript(DescriptorBase): def __init__(self, *args, **kwargs): self.args = args self.taproot = kwargs.get("taproot", False) def compile(self): return self.inner_compile() def verify(self): for arg in self.args: if isinstance(arg, Miniscript): arg.verify() @property def keys(self): return sum( [arg.keys for arg in self.args if isinstance(arg, Miniscript)], [k for k in self.args if isinstance(k, Key) or isinstance(k, KeyHash)], ) def derive(self, idx, branch_index=None): args = [ arg.derive(idx, branch_index) if hasattr(arg, "derive") else arg for arg in self.args ] return type(self)(*args, taproot=self.taproot) def to_public(self): args = [ arg.to_public() if hasattr(arg, "to_public") else arg for arg in self.args ] return type(self)(*args, taproot=self.taproot) def branch(self, branch_index): args = [ arg.branch(branch_index) if hasattr(arg, "branch") else arg for arg in self.args ] return type(self)(*args, taproot=self.taproot) @property def properties(self): return self.PROPS @property def type(self): return self.TYPE @classmethod def read_from(cls, s, taproot=False): op, char = read_until(s, b"(,)") op = op.decode() wrappers = "" if ":" in op: wrappers, op = op.split(":") if op not in OPERATOR_NAMES: raise MiniscriptError("Unknown operator '%s'" % op) # number of arguments, classes of args, compile fn, type, validity checker MiniscriptCls = OPERATORS[OPERATOR_NAMES.index(op)] if MiniscriptCls.NARGS != 0 and char != b"(": raise MiniscriptError("Missing operator") if MiniscriptCls.NARGS is None or MiniscriptCls.NARGS > 0: args = MiniscriptCls.read_arguments(s, taproot=taproot) else: s.seek(-1, 1) args = [] miniscript = MiniscriptCls(*args, taproot=taproot) for w in reversed(wrappers): if w not in WRAPPER_NAMES: raise MiniscriptError("Unknown wrapper") WrapperCls = WRAPPERS[WRAPPER_NAMES.index(w)] miniscript = WrapperCls(miniscript, taproot=taproot) return miniscript @classmethod def read_arguments(cls, s, taproot=False): args = [] if cls.NARGS is None: if type(cls.ARGCLS) == tuple: firstcls, nextcls = cls.ARGCLS else: firstcls, nextcls = cls.ARGCLS, cls.ARGCLS args.append(firstcls.read_from(s, taproot=taproot)) while True: char = s.read(1) if char == b",": args.append(nextcls.read_from(s, taproot=taproot)) elif char == b")": break else: raise MiniscriptError( "Expected , or ), got: %s" % (char + s.read()) ) else: for i in range(cls.NARGS): args.append(cls.ARGCLS.read_from(s, taproot=taproot)) if i < cls.NARGS - 1: char = s.read(1) if char != b",": raise MiniscriptError("Missing arguments, %s" % char) char = s.read(1) if char != b")": raise MiniscriptError("Expected ) got %s" % (char + s.read())) return args def __str__(self): return type(self).NAME + "(" + ",".join([str(arg) for arg in self.args]) + ")" def __len__(self): """Length of the compiled script, override this if you know the length""" return len(self.compile()) def len_args(self): return sum([len(arg) for arg in self.args]) ########### Known fragments (miniscript operators) ############## class OneArg(Miniscript): NARGS = 1 # small handy functions @property def arg(self): return self.args[0] @property def carg(self): return self.arg.compile() class NumberZero(Miniscript): # 0 NARGS = 0 NAME = "0" TYPE = "B" PROPS = "zud" def inner_compile(self): return b"\x00" def __len__(self): return 1 class NumberOne(Miniscript): # 1 NARGS = 0 NAME = "1" TYPE = "B" PROPS = "zu" def inner_compile(self): return b"\x51" def __len__(self): return 1 class PkK(OneArg): # NAME = "pk_k" ARGCLS = Key TYPE = "K" PROPS = "ondu" def inner_compile(self): return self.carg def __len__(self): return self.len_args() class PkH(OneArg): # DUP HASH160 EQUALVERIFY NAME = "pk_h" ARGCLS = KeyHash TYPE = "K" PROPS = "ndu" def inner_compile(self): return b"\x76\xa9" + self.carg + b"\x88" def __len__(self): return self.len_args() + 3 class Older(OneArg): # CHECKSEQUENCEVERIFY NAME = "older" ARGCLS = Number TYPE = "B" PROPS = "z" def inner_compile(self): return self.carg + b"\xb2" def verify(self): super().verify() if (self.arg.num < 1) or (self.arg.num >= 0x80000000): raise MiniscriptError( "%s should have an argument in range [1, 0x80000000)" % self.NAME ) def __len__(self): return self.len_args() + 1 class After(Older): # CHECKLOCKTIMEVERIFY NAME = "after" def inner_compile(self): return self.carg + b"\xb1" class Sha256(OneArg): # SIZE <32> EQUALVERIFY SHA256 EQUAL NAME = "sha256" ARGCLS = Raw32 TYPE = "B" PROPS = "ondu" def inner_compile(self): return b"\x82" + Number(32).compile() + b"\x88\xa8" + self.carg + b"\x87" def __len__(self): return self.len_args() + 6 class Hash256(Sha256): # SIZE <32> EQUALVERIFY HASH256 EQUAL NAME = "hash256" def inner_compile(self): return b"\x82" + Number(32).compile() + b"\x88\xaa" + self.carg + b"\x87" class Ripemd160(Sha256): # SIZE <32> EQUALVERIFY RIPEMD160 EQUAL NAME = "ripemd160" ARGCLS = Raw20 def inner_compile(self): return b"\x82" + Number(32).compile() + b"\x88\xa6" + self.carg + b"\x87" class Hash160(Ripemd160): # SIZE <32> EQUALVERIFY HASH160 EQUAL NAME = "hash160" def inner_compile(self): return b"\x82" + Number(32).compile() + b"\x88\xa9" + self.carg + b"\x87" class AndOr(Miniscript): # [X] NOTIF [Z] ELSE [Y] ENDIF NAME = "andor" NARGS = 3 ARGCLS = Miniscript @property def type(self): # same as Y/Z return self.args[1].type def verify(self): # requires: X is Bdu; Y and Z are both B, K, or V super().verify() if self.args[0].type != "B": raise MiniscriptError("andor: X should be 'B'") px = self.args[0].properties if "d" not in px and "u" not in px: raise MiniscriptError("andor: X should be 'du'") if self.args[1].type != self.args[2].type: raise MiniscriptError("andor: Y and Z should have the same types") if self.args[1].type not in "BKV": raise MiniscriptError("andor: Y and Z should be B K or V") @property def properties(self): # props: z=zXzYzZ; o=zXoYoZ or oXzYzZ; u=uYuZ; d=dZ props = "" px, py, pz = [arg.properties for arg in self.args] if "z" in px and "z" in py and "z" in pz: props += "z" if ("z" in px and "o" in py and "o" in pz) or ( "o" in px and "z" in py and "z" in pz ): props += "o" if "u" in py and "u" in pz: props += "u" if "d" in pz: props += "d" return props def inner_compile(self): return ( self.args[0].compile() + b"\x64" + self.args[2].compile() + b"\x67" + self.args[1].compile() + b"\x68" ) def __len__(self): return self.len_args() + 3 class AndV(Miniscript): # [X] [Y] NAME = "and_v" NARGS = 2 ARGCLS = Miniscript def inner_compile(self): return self.args[0].compile() + self.args[1].compile() def __len__(self): return self.len_args() def verify(self): # X is V; Y is B, K, or V super().verify() if self.args[0].type != "V": raise MiniscriptError("and_v: X should be 'V'") if self.args[1].type not in "BKV": raise MiniscriptError("and_v: Y should be B K or V") @property def type(self): # same as Y return self.args[1].type @property def properties(self): # z=zXzY; o=zXoY or zYoX; n=nX or zXnY; u=uY px, py = [arg.properties for arg in self.args] props = "" if "z" in px and "z" in py: props += "z" if ("z" in px and "o" in py) or ("z" in py and "o" in px): props += "o" if "n" in px or ("z" in px and "n" in py): props += "n" if "u" in py: props += "u" return props class AndB(Miniscript): # [X] [Y] BOOLAND NAME = "and_b" NARGS = 2 ARGCLS = Miniscript TYPE = "B" def inner_compile(self): return self.args[0].compile() + self.args[1].compile() + b"\x9a" def __len__(self): return self.len_args() + 1 def verify(self): # X is B; Y is W super().verify() if self.args[0].type != "B": raise MiniscriptError("and_b: X should be B") if self.args[1].type != "W": raise MiniscriptError("and_b: Y should be W") @property def properties(self): # z=zXzY; o=zXoY or zYoX; n=nX or zXnY; d=dXdY; u px, py = [arg.properties for arg in self.args] props = "" if "z" in px and "z" in py: props += "z" if ("z" in px and "o" in py) or ("z" in py and "o" in px): props += "o" if "n" in px or ("z" in px and "n" in py): props += "n" if "d" in px and "d" in py: props += "d" props += "u" return props class AndN(Miniscript): # [X] NOTIF 0 ELSE [Y] ENDIF # andor(X,Y,0) NAME = "and_n" NARGS = 2 ARGCLS = Miniscript def inner_compile(self): return ( self.args[0].compile() + b"\x64" + Number(0).compile() + b"\x67" + self.args[1].compile() + b"\x68" ) def __len__(self): return self.len_args() + 4 @property def type(self): # same as Y/Z return self.args[1].type def verify(self): # requires: X is Bdu; Y and Z are both B, K, or V super().verify() if self.args[0].type != "B": raise MiniscriptError("and_n: X should be 'B'") px = self.args[0].properties if "d" not in px and "u" not in px: raise MiniscriptError("and_n: X should be 'du'") if self.args[1].type != "B": raise MiniscriptError("and_n: Y should be B") @property def properties(self): # props: z=zXzYzZ; o=zXoYoZ or oXzYzZ; u=uYuZ; d=dZ props = "" px, py = [arg.properties for arg in self.args] pz = "zud" if "z" in px and "z" in py and "z" in pz: props += "z" if ("z" in px and "o" in py and "o" in pz) or ( "o" in px and "z" in py and "z" in pz ): props += "o" if "u" in py and "u" in pz: props += "u" if "d" in pz: props += "d" return props class OrB(Miniscript): # [X] [Z] BOOLOR NAME = "or_b" NARGS = 2 ARGCLS = Miniscript TYPE = "B" def inner_compile(self): return self.args[0].compile() + self.args[1].compile() + b"\x9b" def __len__(self): return self.len_args() + 1 def verify(self): # X is Bd; Z is Wd super().verify() if self.args[0].type != "B": raise MiniscriptError("or_b: X should be B") if "d" not in self.args[0].properties: raise MiniscriptError("or_b: X should be d") if self.args[1].type != "W": raise MiniscriptError("or_b: Z should be W") if "d" not in self.args[1].properties: raise MiniscriptError("or_b: Z should be d") @property def properties(self): # z=zXzZ; o=zXoZ or zZoX; d; u props = "" px, pz = [arg.properties for arg in self.args] if "z" in px and "z" in pz: props += "z" if ("z" in px and "o" in pz) or ("z" in pz and "o" in px): props += "o" props += "du" return props class OrC(Miniscript): # [X] NOTIF [Z] ENDIF NAME = "or_c" NARGS = 2 ARGCLS = Miniscript TYPE = "V" def inner_compile(self): return self.args[0].compile() + b"\x64" + self.args[1].compile() + b"\x68" def __len__(self): return self.len_args() + 2 def verify(self): # X is Bdu; Z is V super().verify() if self.args[0].type != "B": raise MiniscriptError("or_c: X should be B") if self.args[1].type != "V": raise MiniscriptError("or_c: Z should be V") px = self.args[0].properties if "d" not in px or "u" not in px: raise MiniscriptError("or_c: X should be du") @property def properties(self): # z=zXzZ; o=oXzZ props = "" px, pz = [arg.properties for arg in self.args] if "z" in px and "z" in pz: props += "z" if "o" in px and "z" in pz: props += "o" return props class OrD(Miniscript): # [X] IFDUP NOTIF [Z] ENDIF NAME = "or_d" NARGS = 2 ARGCLS = Miniscript TYPE = "B" def inner_compile(self): return self.args[0].compile() + b"\x73\x64" + self.args[1].compile() + b"\x68" def __len__(self): return self.len_args() + 3 def verify(self): # X is Bdu; Z is B super().verify() if self.args[0].type != "B": raise MiniscriptError("or_d: X should be B") if self.args[1].type != "B": raise MiniscriptError("or_d: Z should be B") px = self.args[0].properties if "d" not in px or "u" not in px: raise MiniscriptError("or_d: X should be du") @property def properties(self): # z=zXzZ; o=oXzZ; d=dZ; u=uZ props = "" px, pz = [arg.properties for arg in self.args] if "z" in px and "z" in pz: props += "z" if "o" in px and "z" in pz: props += "o" if "d" in pz: props += "d" if "u" in pz: props += "u" return props class OrI(Miniscript): # IF [X] ELSE [Z] ENDIF NAME = "or_i" NARGS = 2 ARGCLS = Miniscript def inner_compile(self): return ( b"\x63" + self.args[0].compile() + b"\x67" + self.args[1].compile() + b"\x68" ) def __len__(self): return self.len_args() + 3 def verify(self): # both are B, K, or V super().verify() if self.args[0].type != self.args[1].type: raise MiniscriptError("or_i: X and Z should be the same type") if self.args[0].type not in "BKV": raise MiniscriptError("or_i: X and Z should be B K or V") @property def type(self): return self.args[0].type @property def properties(self): # o=zXzZ; u=uXuZ; d=dX or dZ props = "" px, pz = [arg.properties for arg in self.args] if "z" in px and "z" in pz: props += "o" if "u" in px and "u" in pz: props += "u" if "d" in px or "d" in pz: props += "d" return props class Thresh(Miniscript): # [X1] [X2] ADD ... [Xn] ADD ... EQUAL NAME = "thresh" NARGS = None ARGCLS = (Number, Miniscript) TYPE = "B" def inner_compile(self): return ( self.args[1].compile() + b"".join([arg.compile() + b"\x93" for arg in self.args[2:]]) + self.args[0].compile() + b"\x87" ) def __len__(self): return self.len_args() + len(self.args) - 1 def verify(self): # 1 <= k <= n; X1 is Bdu; others are Wdu super().verify() if self.args[0].num < 1 or self.args[0].num >= len(self.args): raise MiniscriptError( "thresh: Invalid k! Should be 1 <= k <= %d, got %d" % (len(self.args) - 1, self.args[0].num) ) if self.args[1].type != "B": raise MiniscriptError("thresh: X1 should be B") px = self.args[1].properties if "d" not in px or "u" not in px: raise MiniscriptError("thresh: X1 should be du") for i, arg in enumerate(self.args[2:]): if arg.type != "W": raise MiniscriptError("thresh: X%d should be W" % (i + 1)) p = arg.properties if "d" not in p or "u" not in p: raise MiniscriptError("thresh: X%d should be du" % (i + 1)) @property def properties(self): # z=all are z; o=all are z except one is o; d; u props = "" parr = [arg.properties for arg in self.args[1:]] zarr = ["z" for p in parr if "z" in p] if len(zarr) == len(parr): props += "z" noz = [p for p in parr if "z" not in p] if len(noz) == 1 and "o" in noz[0]: props += "o" props += "du" return props class Multi(Miniscript): # ... CHECKMULTISIG NAME = "multi" NARGS = None ARGCLS = (Number, Key) TYPE = "B" PROPS = "ndu" _expected_taproot = False def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.taproot is not self._expected_taproot: raise MiniscriptError( "%s can't be used if taproot is %s" % (self.NAME, self.taproot) ) def inner_compile(self): return ( b"".join([arg.compile() for arg in self.args]) + Number(len(self.args) - 1).compile() + b"\xae" ) def __len__(self): return self.len_args() + 2 def verify(self): super().verify() if self.args[0].num < 1 or self.args[0].num > (len(self.args) - 1): raise MiniscriptError( "multi: 1 <= k <= %d, got %d" % ((len(self.args) - 1), self.args[0].num) ) class Sortedmulti(Multi): # ... CHECKMULTISIG NAME = "sortedmulti" def inner_compile(self): return ( self.args[0].compile() + b"".join(sorted([arg.compile() for arg in self.args[1:]])) + Number(len(self.args) - 1).compile() + b"\xae" ) class MultiA(Multi): # CHECKSIG CHECKSIGADD ... CHECKSIGNADD NUMEQUAL NAME = "multi_a" _expected_taproot = True def inner_compile(self): return ( self.args[1].compile() + b"\xac" + b"".join([arg.compile() + b"\xba" for arg in self.args[2:]]) + self.args[0].compile() + b"\x9c" ) def __len__(self): return self.len_args() + len(self.args) class SortedmultiA(MultiA): # CHECKSIG CHECKSIGADD ... CHECKSIGNADD NUMEQUAL NAME = "sortedmulti_a" def inner_compile(self): keys = list(sorted([k.compile() for k in self.args[1:]])) return ( keys[0] + b"\xac" + b"".join([k + b"\xba" for k in keys[1:]]) + self.args[0].compile() + b"\x9c" ) class Pk(OneArg): # CHECKSIG NAME = "pk" ARGCLS = Key TYPE = "B" PROPS = "ondu" def inner_compile(self): return self.carg + b"\xac" def __len__(self): return self.len_args() + 1 class Pkh(OneArg): # DUP HASH160 EQUALVERIFY CHECKSIG NAME = "pkh" ARGCLS = KeyHash TYPE = "B" PROPS = "ndu" def inner_compile(self): return b"\x76\xa9" + self.carg + b"\x88\xac" def __len__(self): return self.len_args() + 4 # TODO: 0, 1 - they are without brackets, so it should be different... OPERATORS = [ NumberZero, NumberOne, PkK, PkH, Older, After, Sha256, Hash256, Ripemd160, Hash160, AndOr, AndV, AndB, AndN, OrB, OrC, OrD, OrI, Thresh, Multi, Sortedmulti, MultiA, SortedmultiA, Pk, Pkh, ] OPERATOR_NAMES = [cls.NAME for cls in OPERATORS] class Wrapper(OneArg): ARGCLS = Miniscript @property def op(self): return type(self).__name__.lower() def __str__(self): # more wrappers follow if isinstance(self.arg, Wrapper): return self.op + str(self.arg) # we are the last wrapper return self.op + ":" + str(self.arg) class A(Wrapper): # TOALTSTACK [X] FROMALTSTACK TYPE = "W" def inner_compile(self): return b"\x6b" + self.carg + b"\x6c" def __len__(self): return len(self.arg) + 2 def verify(self): super().verify() if self.arg.type != "B": raise MiniscriptError("a: X should be B") @property def properties(self): props = "" px = self.arg.properties if "d" in px: props += "d" if "u" in px: props += "u" return props class S(Wrapper): # SWAP [X] TYPE = "W" def inner_compile(self): return b"\x7c" + self.carg def __len__(self): return len(self.arg) + 1 def verify(self): super().verify() if self.arg.type != "B": raise MiniscriptError("s: X should be B") if "o" not in self.arg.properties: raise MiniscriptError("s: X should be o") @property def properties(self): props = "" px = self.arg.properties if "d" in px: props += "d" if "u" in px: props += "u" return props class C(Wrapper): # [X] CHECKSIG TYPE = "B" def inner_compile(self): return self.carg + b"\xac" def __len__(self): return len(self.arg) + 1 def verify(self): super().verify() if self.arg.type != "K": raise MiniscriptError("c: X should be K") @property def properties(self): props = "" px = self.arg.properties for p in ["o", "n", "d"]: if p in px: props += p props += "u" return props class T(Wrapper): # [X] 1 TYPE = "B" def inner_compile(self): return self.carg + Number(1).compile() def __len__(self): return len(self.arg) + 1 @property def properties(self): # z=zXzY; o=zXoY or zYoX; n=nX or zXnY; u=uY px = self.arg.properties py = "zu" props = "" if "z" in px and "z" in py: props += "z" if ("z" in px and "o" in py) or ("z" in py and "o" in px): props += "o" if "n" in px or ("z" in px and "n" in py): props += "n" if "u" in py: props += "u" return props class D(Wrapper): # DUP IF [X] ENDIF TYPE = "B" def inner_compile(self): return b"\x76\x63" + self.carg + b"\x68" def __len__(self): return len(self.arg) + 3 def verify(self): super().verify() if self.arg.type != "V": raise MiniscriptError("d: X should be V") if "z" not in self.arg.properties: raise MiniscriptError("d: X should be z") @property def properties(self): # https://github.com/bitcoin/bitcoin/pull/24906 if self.taproot: props = "ndu" else: props = "nd" px = self.arg.properties if "z" in px: props += "o" return props class V(Wrapper): # [X] VERIFY (or VERIFY version of last opcode in [X]) TYPE = "V" def inner_compile(self): """Checks last check code and makes it verify""" if self.carg[-1] in [0xAC, 0xAE, 0x9C, 0x87]: return self.carg[:-1] + bytes([self.carg[-1] + 1]) return self.carg + b"\x69" def verify(self): super().verify() if self.arg.type != "B": raise MiniscriptError("v: X should be B") @property def properties(self): props = "" px = self.arg.properties for p in ["z", "o", "n"]: if p in px: props += p return props class J(Wrapper): # SIZE 0NOTEQUAL IF [X] ENDIF TYPE = "B" def inner_compile(self): return b"\x82\x92\x63" + self.carg + b"\x68" def verify(self): super().verify() if self.arg.type != "B": raise MiniscriptError("j: X should be B") if "n" not in self.arg.properties: raise MiniscriptError("j: X should be n") @property def properties(self): props = "nd" px = self.arg.properties for p in ["o", "u"]: if p in px: props += p return props class N(Wrapper): # [X] 0NOTEQUAL TYPE = "B" def inner_compile(self): return self.carg + b"\x92" def __len__(self): return len(self.arg) + 1 def verify(self): super().verify() if self.arg.type != "B": raise MiniscriptError("n: X should be B") @property def properties(self): props = "u" px = self.arg.properties for p in ["z", "o", "n", "d"]: if p in px: props += p return props class L(Wrapper): # IF 0 ELSE [X] ENDIF TYPE = "B" def inner_compile(self): return b"\x63" + Number(0).compile() + b"\x67" + self.carg + b"\x68" def __len__(self): return len(self.arg) + 4 def verify(self): # both are B, K, or V super().verify() if self.arg.type != "B": raise MiniscriptError("or_i: X and Z should be the same type") @property def properties(self): # o=zXzZ; u=uXuZ; d=dX or dZ props = "d" pz = self.arg.properties if "z" in pz: props += "o" if "u" in pz: props += "u" return props class U(L): # IF [X] ELSE 0 ENDIF def inner_compile(self): return b"\x63" + self.carg + b"\x67" + Number(0).compile() + b"\x68" def __len__(self): return len(self.arg) + 4 WRAPPERS = [A, S, C, T, D, V, J, N, L, U] WRAPPER_NAMES = [w.__name__.lower() for w in WRAPPERS] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/descriptor/taptree.py0000664000175000017500000001124714623570367024633 0ustar00singalasingalafrom .errors import MiniscriptError from .base import DescriptorBase from .miniscript import Miniscript from ..hashes import tagged_hash from ..script import Script class TapLeaf(DescriptorBase): def __init__(self, miniscript=None, version=0xC0): self.miniscript = miniscript self.version = version def __str__(self): return str(self.miniscript) @classmethod def read_from(cls, s): ms = Miniscript.read_from(s, taproot=True) return cls(ms) def serialize(self): if self.miniscript is None: return b"" return bytes([self.version]) + Script(self.miniscript.compile()).serialize() @property def keys(self): return self.miniscript.keys def derive(self, *args, **kwargs): if self.miniscript is None: return type(self)(None, version=self.version) return type(self)( self.miniscript.derive(*args, **kwargs), self.version, ) def branch(self, *args, **kwargs): if self.miniscript is None: return type(self)(None, version=self.version) return type(self)( self.miniscript.branch(*args, **kwargs), self.version, ) def to_public(self, *args, **kwargs): if self.miniscript is None: return type(self)(None, version=self.version) return type(self)( self.miniscript.to_public(*args, **kwargs), self.version, ) def _tweak_helper(tree): # https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs if isinstance(tree, TapTree): tree = tree.tree if isinstance(tree, TapLeaf): # one leaf on this branch h = tagged_hash("TapLeaf", tree.serialize()) return ([(tree, b"")], h) left, left_h = _tweak_helper(tree[0]) right, right_h = _tweak_helper(tree[1]) ret = [(leaf, c + right_h) for leaf, c in left] + [ (leaf, c + left_h) for leaf, c in right ] if right_h < left_h: left_h, right_h = right_h, left_h return (ret, tagged_hash("TapBranch", left_h + right_h)) class TapTree(DescriptorBase): def __init__(self, tree=None): """tree can be None, TapLeaf or a tuple (taptree, taptree)""" self.tree = tree # make sure all keys are taproot for k in self.keys: k.taproot = True def __bool__(self): return bool(self.tree) def tweak(self): if self.tree is None: return b"" _, h = _tweak_helper(self.tree) return h @property def keys(self): if self.tree is None: return [] if isinstance(self.tree, TapLeaf): return self.tree.keys left, right = self.tree return left.keys + right.keys @classmethod def read_from(cls, s): c = s.read(1) if len(c) == 0: return cls() if c == b"{": # more than one miniscript left = cls.read_from(s) c = s.read(1) if c == b"}": return left if c != b",": raise MiniscriptError("Invalid taptree syntax: expected ','") right = cls.read_from(s) if s.read(1) != b"}": raise MiniscriptError("Invalid taptree syntax: expected '}'") return cls((left, right)) s.seek(-1, 1) ms = TapLeaf.read_from(s) return cls(ms) def derive(self, *args, **kwargs): if self.tree is None: return type(self)(None) if isinstance(self.tree, TapLeaf): return type(self)(self.tree.derive(*args, **kwargs)) left, right = self.tree return type(self)((left.derive(*args, **kwargs), right.derive(*args, **kwargs))) def branch(self, *args, **kwargs): if self.tree is None: return type(self)(None) if isinstance(self.tree, TapLeaf): return type(self)(self.tree.branch(*args, **kwargs)) left, right = self.tree return type(self)((left.branch(*args, **kwargs), right.branch(*args, **kwargs))) def to_public(self, *args, **kwargs): if self.tree is None: return type(self)(None) if isinstance(self.tree, TapLeaf): return type(self)(self.tree.to_public(*args, **kwargs)) left, right = self.tree return type(self)( (left.to_public(*args, **kwargs), right.to_public(*args, **kwargs)) ) def __str__(self): if self.tree is None: return "" if isinstance(self.tree, TapLeaf): return str(self.tree) (left, right) = self.tree return "{%s,%s}" % (left, right) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/ec.py0000664000175000017500000002005114623570367021371 0ustar00singalasingalafrom . import base58 from . import hashes from .misc import secp256k1 from .networks import NETWORKS from .base import EmbitBase, EmbitError, EmbitKey from binascii import hexlify, unhexlify class ECError(EmbitError): pass class Signature(EmbitBase): def __init__(self, sig): self._sig = sig def write_to(self, stream) -> int: return stream.write(secp256k1.ecdsa_signature_serialize_der(self._sig)) @classmethod def read_from(cls, stream): der = stream.read(2) der += stream.read(der[1]) return cls(secp256k1.ecdsa_signature_parse_der(der)) class SchnorrSig(EmbitBase): def __init__(self, sig): assert len(sig) == 64 self._sig = sig def write_to(self, stream) -> int: return stream.write(self._sig) @classmethod def read_from(cls, stream): return cls(stream.read(64)) class PublicKey(EmbitKey): def __init__(self, point: bytes, compressed: bool = True): self._point = point self.compressed = compressed @classmethod def read_from(cls, stream): b = stream.read(1) if b not in [b"\x02", b"\x03", b"\x04"]: raise ECError("Invalid public key") if b == b"\x04": b += stream.read(64) else: b += stream.read(32) try: point = secp256k1.ec_pubkey_parse(b) except Exception as e: raise ECError(str(e)) compressed = b[0] != 0x04 return cls(point, compressed) def sec(self) -> bytes: """Sec representation of the key""" flag = secp256k1.EC_COMPRESSED if self.compressed else secp256k1.EC_UNCOMPRESSED return secp256k1.ec_pubkey_serialize(self._point, flag) def xonly(self) -> bytes: return self.sec()[1:33] def taproot_tweak(self, h=b""): """Returns a tweaked public key""" x = self.xonly() tweak = hashes.tagged_hash("TapTweak", x + h) if not secp256k1.ec_seckey_verify(tweak): raise EmbitError("Tweak is too large") point = secp256k1.ec_pubkey_parse(b"\x02" + x) pub = secp256k1.ec_pubkey_add(point, tweak) sec = secp256k1.ec_pubkey_serialize(pub) return PublicKey.from_xonly(sec[1:33]) def write_to(self, stream) -> int: return stream.write(self.sec()) def serialize(self) -> bytes: return self.sec() def verify(self, sig, msg_hash) -> bool: return bool(secp256k1.ecdsa_verify(sig._sig, msg_hash, self._point)) def _xonly(self): """Returns internal representation of the xonly-pubkey (64 bytes)""" pub, _ = secp256k1.xonly_pubkey_from_pubkey(self._point) return pub @classmethod def from_xonly(cls, data: bytes): assert len(data) == 32 return cls.parse(b"\x02" + data) def schnorr_verify(self, sig, msg_hash) -> bool: return bool(secp256k1.schnorrsig_verify(sig._sig, msg_hash, self._xonly())) @classmethod def from_string(cls, s): return cls.parse(unhexlify(s)) @property def is_private(self) -> bool: return False def to_string(self): return hexlify(self.sec()).decode() def __lt__(self, other): # for lexagraphic ordering return self.sec() < other.sec() def __gt__(self, other): # for lexagraphic ordering return self.sec() > other.sec() def __eq__(self, other): return self.sec() == other.sec() def __hash__(self): return hash(self._point) class PrivateKey(EmbitKey): def __init__(self, secret, compressed: bool = True, network=NETWORKS["main"]): """Creates a private key from 32-byte array""" if len(secret) != 32: raise ECError("Secret should be 32-byte array") if not secp256k1.ec_seckey_verify(secret): raise ECError("Secret is not valid (larger then N?)") self.compressed = compressed self._secret = secret self.network = network def wif(self, network=None) -> str: """Export private key as Wallet Import Format string. Prefix 0x80 is used for mainnet, 0xEF for testnet. This class doesn't store this information though. """ if network is None: network = self.network prefix = network["wif"] b = prefix + self._secret if self.compressed: b += bytes([0x01]) return base58.encode_check(b) @property def secret(self): return self._secret def sec(self) -> bytes: """Sec representation of the corresponding public key""" return self.get_public_key().sec() def xonly(self) -> bytes: return self.sec()[1:] def taproot_tweak(self, h=b""): """Returns a tweaked private key""" sec = self.sec() negate = sec[0] != 0x02 x = sec[1:33] tweak = hashes.tagged_hash("TapTweak", x + h) if not secp256k1.ec_seckey_verify(tweak): raise EmbitError("Tweak is too large") if negate: secret = secp256k1.ec_privkey_negate(self._secret) else: secret = self._secret res = secp256k1.ec_privkey_add(secret, tweak) pk = PrivateKey(res) if pk.sec()[0] == 0x03: pk = PrivateKey(secp256k1.ec_privkey_negate(res)) return pk @classmethod def from_wif(cls, s): """Import private key from Wallet Import Format string.""" b = base58.decode_check(s) prefix = b[:1] network = None for net in NETWORKS: if NETWORKS[net]["wif"] == prefix: network = NETWORKS[net] secret = b[1:33] compressed = False if len(b) not in [33, 34]: raise ECError("Wrong WIF length") if len(b) == 34: if b[-1] == 0x01: compressed = True else: raise ECError("Wrong WIF compressed flag") return cls(secret, compressed, network) # to unify API def to_base58(self, network=None) -> str: return self.wif(network) @classmethod def from_base58(cls, s): return cls.from_wif(s) def get_public_key(self) -> PublicKey: return PublicKey(secp256k1.ec_pubkey_create(self._secret), self.compressed) def to_public(self) -> PublicKey: """Alias to get_public_key for API consistency""" return self.get_public_key() def sign(self, msg_hash, grind=True) -> Signature: sig = Signature(secp256k1.ecdsa_sign(msg_hash, self._secret)) if grind: counter = 1 while len(sig.serialize()) > 70: sig = Signature( secp256k1.ecdsa_sign( msg_hash, self._secret, None, counter.to_bytes(32, "little") ) ) counter += 1 # just in case we get in infinite loop for some reason if counter > 200: break return sig def schnorr_sign(self, msg_hash) -> SchnorrSig: return SchnorrSig(secp256k1.schnorrsig_sign(msg_hash, self._secret)) def verify(self, sig, msg_hash) -> bool: return self.get_public_key().verify(sig, msg_hash) def schnorr_verify(self, sig, msg_hash) -> bool: return self.get_public_key().schnorr_verify(sig, msg_hash) def write_to(self, stream) -> int: # return a copy of the secret return stream.write(self._secret) def ecdh(self, public_key: PublicKey, hashfn=None, data=None) -> bytes: pubkey_point = secp256k1.ec_pubkey_parse(public_key.sec()) return secp256k1.ecdh(pubkey_point, self._secret, hashfn, data) @classmethod def read_from(cls, stream): # just to unify the API return cls(stream.read(32)) @property def is_private(self) -> bool: return True # Nothing up my sleeve point for no-internal-key taproot # see https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs NUMS_PUBKEY = PublicKey.from_string( "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/hashes.py0000664000175000017500000000230614623570367022260 0ustar00singalasingalaimport hashlib try: # this will work with micropython and python < 3.10 # but will raise and exception if ripemd is not supported (python3.10, openssl 3) hashlib.new("ripemd160") def ripemd160(msg: bytes) -> bytes: return hashlib.new("ripemd160", msg).digest() except: # otherwise use pure python implementation from .util.py_ripemd160 import ripemd160 def double_sha256(msg: bytes) -> bytes: """sha256(sha256(msg)) -> bytes""" return hashlib.sha256(hashlib.sha256(msg).digest()).digest() def hash160(msg: bytes) -> bytes: """ripemd160(sha256(msg)) -> bytes""" return ripemd160(hashlib.sha256(msg).digest()) def sha256(msg: bytes) -> bytes: """one-line sha256(msg) -> bytes""" return hashlib.sha256(msg).digest() def tagged_hash(tag: str, data: bytes) -> bytes: """BIP-Schnorr tag-specific key derivation""" hashtag = hashlib.sha256(tag.encode()).digest() return hashlib.sha256(hashtag + hashtag + data).digest() def tagged_hash_init(tag: str, data: bytes = b""): """Prepares a tagged hash function to digest extra data""" hashtag = hashlib.sha256(tag.encode()).digest() h = hashlib.sha256(hashtag + hashtag + data) return h ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/misc.py0000664000175000017500000000334314623570367021742 0ustar00singalasingala"""Misc utility functions used across embit""" import sys # implementation-specific functions and libraries: if sys.implementation.name == "micropython": from micropython import const import secp256k1 else: from .util import secp256k1 def const(x): return x try: # if urandom is available from os module: from os import urandom as urandom except ImportError: # otherwise - try reading from /dev/urandom def urandom(n: int) -> bytes: with open("/dev/urandom", "rb") as f: return f.read(n) def getrandbits(k: int) -> int: b = urandom(k // 8 + 1) return int.from_bytes(b, "big") % (2**k) def secure_randint(vmin: int, vmax: int) -> int: """ Normal random.randint uses PRNG that is not suitable for cryptographic applications. This one uses os.urandom for randomness. """ import math assert vmax > vmin delta = vmax - vmin nbits = math.ceil(math.log2(delta + 1)) randn = getrandbits(nbits) while randn > delta: randn = getrandbits(nbits) return vmin + randn def copy(a: bytes) -> bytes: """Ugly copy that works everywhere incl micropython""" if len(a) == 0: return b"" return a[:1] + a[1:] def read_until(s, chars=b",)(#"): """Read from stream until one of `char` characters. By default `chars=,)(#`. Return a tuple (result: bytes, char: bytes | None) where result is bytes read from the stream until char, char contains this character or None if the end of stream reached. """ res = b"" chunk = b"" while True: chunk = s.read(1) if len(chunk) == 0: return res, None if chunk in chars: return res, chunk res += chunk ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/networks.py0000664000175000017500000000432014623570367022657 0ustar00singalasingalafrom .misc import const NETWORKS = { "main": { "name": "Mainnet", "wif": b"\x80", "p2pkh": b"\x00", "p2sh": b"\x05", "bech32": "bc", "xprv": b"\x04\x88\xad\xe4", "xpub": b"\x04\x88\xb2\x1e", "yprv": b"\x04\x9d\x78\x78", "zprv": b"\x04\xb2\x43\x0c", "Yprv": b"\x02\x95\xb0\x05", "Zprv": b"\x02\xaa\x7a\x99", "ypub": b"\x04\x9d\x7c\xb2", "zpub": b"\x04\xb2\x47\x46", "Ypub": b"\x02\x95\xb4\x3f", "Zpub": b"\x02\xaa\x7e\xd3", "bip32": const(0), # coin type for bip32 derivation }, "test": { "name": "Testnet", "wif": b"\xEF", "p2pkh": b"\x6F", "p2sh": b"\xC4", "bech32": "tb", "xprv": b"\x04\x35\x83\x94", "xpub": b"\x04\x35\x87\xcf", "yprv": b"\x04\x4a\x4e\x28", "zprv": b"\x04\x5f\x18\xbc", "Yprv": b"\x02\x42\x85\xb5", "Zprv": b"\x02\x57\x50\x48", "ypub": b"\x04\x4a\x52\x62", "zpub": b"\x04\x5f\x1c\xf6", "Ypub": b"\x02\x42\x89\xef", "Zpub": b"\x02\x57\x54\x83", "bip32": const(1), }, "regtest": { "name": "Regtest", "wif": b"\xEF", "p2pkh": b"\x6F", "p2sh": b"\xC4", "bech32": "bcrt", "xprv": b"\x04\x35\x83\x94", "xpub": b"\x04\x35\x87\xcf", "yprv": b"\x04\x4a\x4e\x28", "zprv": b"\x04\x5f\x18\xbc", "Yprv": b"\x02\x42\x85\xb5", "Zprv": b"\x02\x57\x50\x48", "ypub": b"\x04\x4a\x52\x62", "zpub": b"\x04\x5f\x1c\xf6", "Ypub": b"\x02\x42\x89\xef", "Zpub": b"\x02\x57\x54\x83", "bip32": const(1), }, "signet": { "name": "Signet", "wif": b"\xEF", "p2pkh": b"\x6F", "p2sh": b"\xC4", "bech32": "tb", "xprv": b"\x04\x35\x83\x94", "xpub": b"\x04\x35\x87\xcf", "yprv": b"\x04\x4a\x4e\x28", "zprv": b"\x04\x5f\x18\xbc", "Yprv": b"\x02\x42\x85\xb5", "Zprv": b"\x02\x57\x50\x48", "ypub": b"\x04\x4a\x52\x62", "zpub": b"\x04\x5f\x1c\xf6", "Ypub": b"\x02\x42\x89\xef", "Zpub": b"\x02\x57\x54\x83", "bip32": const(1), }, } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/script.py0000664000175000017500000001432214623570367022312 0ustar00singalasingalafrom .networks import NETWORKS from . import base58 from . import bech32 from . import hashes from . import compact from .base import EmbitBase, EmbitError SIGHASH_ALL = 1 class Script(EmbitBase): def __init__(self, data=b""): self.data = data def address(self, network=NETWORKS["main"]): script_type = self.script_type() data = self.data if script_type is None: raise ValueError("This type of script doesn't have address representation") if script_type == "p2pkh": d = network["p2pkh"] + data[3:23] return base58.encode_check(d) if script_type == "p2sh": d = network["p2sh"] + data[2:22] return base58.encode_check(d) if script_type in ["p2wpkh", "p2wsh", "p2tr"]: ver = data[0] # FIXME: should be one of OP_N if ver > 0: ver = ver % 0x50 return bech32.encode(network["bech32"], ver, data[2:]) # we should never get here raise ValueError("Unsupported script type") def push(self, data): self.data += compact.to_bytes(len(data)) + data def script_type(self): data = self.data # OP_DUP OP_HASH160 <20:hash160(pubkey)> OP_EQUALVERIFY OP_CHECKSIG if len(data) == 25 and data[:3] == b"\x76\xa9\x14" and data[-2:] == b"\x88\xac": return "p2pkh" # OP_HASH160 <20:hash160(script)> OP_EQUAL if len(data) == 23 and data[:2] == b"\xa9\x14" and data[-1] == 0x87: return "p2sh" # 0 <20:hash160(pubkey)> if len(data) == 22 and data[:2] == b"\x00\x14": return "p2wpkh" # 0 <32:sha256(script)> if len(data) == 34 and data[:2] == b"\x00\x20": return "p2wsh" # OP_1 if len(data) == 34 and data[:2] == b"\x51\x20": return "p2tr" # unknown type return None def write_to(self, stream): res = stream.write(compact.to_bytes(len(self.data))) res += stream.write(self.data) return res @classmethod def read_from(cls, stream): l = compact.read_from(stream) data = stream.read(l) if len(data) != l: raise ValueError("Cant read %d bytes" % l) return cls(data) @classmethod def from_address(cls, addr: str): """ Decodes a bitcoin address and returns corresponding scriptpubkey. """ return address_to_scriptpubkey(addr) def __eq__(self, other): return self.data == other.data def __ne__(self, other): return self.data != other.data def __hash__(self): return hash(self.data) def __len__(self): return len(self.data) class Witness(EmbitBase): def __init__(self, items=[]): self.items = items[:] def write_to(self, stream): res = stream.write(compact.to_bytes(len(self.items))) for item in self.items: res += stream.write(compact.to_bytes(len(item))) res += stream.write(item) return res @classmethod def read_from(cls, stream): num = compact.read_from(stream) items = [] for i in range(num): l = compact.read_from(stream) data = stream.read(l) items.append(data) return cls(items) def __hash__(self): return hash(self.items) def __len__(self): return len(self.items) def p2pkh(pubkey): """Return Pay-To-Pubkey-Hash ScriptPubkey""" return Script(b"\x76\xa9\x14" + hashes.hash160(pubkey.sec()) + b"\x88\xac") def p2sh(script): """Return Pay-To-Script-Hash ScriptPubkey""" return Script(b"\xa9\x14" + hashes.hash160(script.data) + b"\x87") def p2wpkh(pubkey): """Return Pay-To-Witness-Pubkey-Hash ScriptPubkey""" return Script(b"\x00\x14" + hashes.hash160(pubkey.sec())) def p2wsh(script): """Return Pay-To-Witness-Pubkey-Hash ScriptPubkey""" return Script(b"\x00\x20" + hashes.sha256(script.data)) def p2tr(pubkey, script_tree=None): """Return Pay-To-Taproot ScriptPubkey""" if script_tree is None: h = b"" else: h = script_tree.tweak() output_pubkey = pubkey.taproot_tweak(h) return Script(b"\x51\x20" + output_pubkey.xonly()) def p2pkh_from_p2wpkh(script): """Convert p2wpkh to p2pkh script""" return Script(b"\x76\xa9" + script.serialize()[2:] + b"\x88\xac") def multisig(m: int, pubkeys): if m <= 0 or m > 16: raise ValueError("m must be between 1 and 16") n = len(pubkeys) if n < m or n > 16: raise ValueError("Number of pubkeys must be between %d and 16" % m) data = bytes([80 + m]) for pubkey in pubkeys: sec = pubkey.sec() data += bytes([len(sec)]) + sec # OP_m ... OP_n OP_CHECKMULTISIG data += bytes([80 + n, 0xAE]) return Script(data) def address_to_scriptpubkey(addr): # try with base58 address try: data = base58.decode_check(addr) prefix = data[:1] for net in NETWORKS.values(): if prefix == net["p2pkh"]: return Script(b"\x76\xa9\x14" + data[1:] + b"\x88\xac") elif prefix == net["p2sh"]: return Script(b"\xa9\x14" + data[1:] + b"\x87") except: # fail - then it's bech32 address hrp = addr.split("1")[0] ver, data = bech32.decode(hrp, addr) if ver not in [0, 1] or len(data) not in [20, 32]: raise EmbitError("Invalid bech32 address") if ver == 1 and len(data) != 32: raise EmbitError("Invalid bech32 address") # OP_1..OP_N if ver > 0: ver += 0x50 return Script(bytes([ver, len(data)] + data)) def script_sig_p2pkh(signature, pubkey, sighash=SIGHASH_ALL): sec = pubkey.sec() der = signature.serialize() + bytes([sighash]) data = compact.to_bytes(len(der)) + der + compact.to_bytes(len(sec)) + sec return Script(data) def script_sig_p2sh(redeem_script): """Creates scriptsig for p2sh""" # FIXME: implement for legacy p2sh as well return Script(redeem_script.serialize()) def witness_p2wpkh(signature, pubkey, sighash=SIGHASH_ALL): return Witness([signature.serialize() + bytes([sighash]), pubkey.sec()]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741351045.8513703 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/util/0000775000175000017500000000000014762564206021406 5ustar00singalasingala././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/util/__init__.py0000664000175000017500000000014014623570367023513 0ustar00singalasingalafrom . import secp256k1 try: from micropython import const except: const = lambda x: x ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/util/ctypes_secp256k1.py0000664000175000017500000010763614623570367025010 0ustar00singalasingalaimport ctypes, os import ctypes.util import platform import threading from ctypes import ( cast, byref, c_char, c_byte, c_int, c_uint, c_char_p, c_size_t, c_void_p, c_uint64, create_string_buffer, CFUNCTYPE, POINTER, ) _lock = threading.Lock() # @locked decorator def locked(func): def wrapper(*args, **kwargs): with _lock: return func(*args, **kwargs) return wrapper # Flags to pass to context_create. CONTEXT_VERIFY = 0b0100000001 CONTEXT_SIGN = 0b1000000001 CONTEXT_NONE = 0b0000000001 # Flags to pass to ec_pubkey_serialize EC_COMPRESSED = 0b0100000010 EC_UNCOMPRESSED = 0b0000000010 def _copy(a: bytes) -> bytes: """Ugly copy that works everywhere incl micropython""" if len(a) == 0: return b"" return a[:1] + a[1:] def _find_library(): library_path = None extension = "" if platform.system() == "Darwin": extension = ".dylib" elif platform.system() == "Linux": extension = ".so" elif platform.system() == "Windows": extension = ".dll" path = os.path.join( os.path.dirname(__file__), "prebuilt/libsecp256k1_%s_%s%s" % (platform.system().lower(), platform.machine().lower(), extension), ) if os.path.isfile(path): return path # try searching if not library_path: library_path = ctypes.util.find_library("libsecp256k1") if not library_path: library_path = ctypes.util.find_library("secp256k1") # library search failed if not library_path: if platform.system() == "Linux" and os.path.isfile( "/usr/local/lib/libsecp256k1.so.0" ): library_path = "/usr/local/lib/libsecp256k1.so.0" return library_path @locked def _init(flags=(CONTEXT_SIGN | CONTEXT_VERIFY)): library_path = _find_library() # meh, can't find library if not library_path: raise RuntimeError( "Can't find libsecp256k1 library. Make sure to compile and install it." ) secp256k1 = ctypes.cdll.LoadLibrary(library_path) secp256k1.secp256k1_context_create.argtypes = [c_uint] secp256k1.secp256k1_context_create.restype = c_void_p secp256k1.secp256k1_context_randomize.argtypes = [c_void_p, c_char_p] secp256k1.secp256k1_context_randomize.restype = c_int secp256k1.secp256k1_ec_seckey_verify.argtypes = [c_void_p, c_char_p] secp256k1.secp256k1_ec_seckey_verify.restype = c_int secp256k1.secp256k1_ec_privkey_negate.argtypes = [c_void_p, c_char_p] secp256k1.secp256k1_ec_privkey_negate.restype = c_int secp256k1.secp256k1_ec_pubkey_negate.argtypes = [c_void_p, c_char_p] secp256k1.secp256k1_ec_pubkey_negate.restype = c_int secp256k1.secp256k1_ec_privkey_tweak_add.argtypes = [c_void_p, c_char_p, c_char_p] secp256k1.secp256k1_ec_privkey_tweak_add.restype = c_int secp256k1.secp256k1_ec_privkey_tweak_mul.argtypes = [c_void_p, c_char_p, c_char_p] secp256k1.secp256k1_ec_privkey_tweak_mul.restype = c_int secp256k1.secp256k1_ec_pubkey_create.argtypes = [c_void_p, c_void_p, c_char_p] secp256k1.secp256k1_ec_pubkey_create.restype = c_int secp256k1.secp256k1_ec_pubkey_parse.argtypes = [c_void_p, c_char_p, c_char_p, c_int] secp256k1.secp256k1_ec_pubkey_parse.restype = c_int secp256k1.secp256k1_ec_pubkey_serialize.argtypes = [ c_void_p, c_char_p, c_void_p, c_char_p, c_uint, ] secp256k1.secp256k1_ec_pubkey_serialize.restype = c_int secp256k1.secp256k1_ec_pubkey_tweak_add.argtypes = [c_void_p, c_char_p, c_char_p] secp256k1.secp256k1_ec_pubkey_tweak_add.restype = c_int secp256k1.secp256k1_ec_pubkey_tweak_mul.argtypes = [c_void_p, c_char_p, c_char_p] secp256k1.secp256k1_ec_pubkey_tweak_mul.restype = c_int secp256k1.secp256k1_ecdsa_signature_parse_compact.argtypes = [ c_void_p, c_char_p, c_char_p, ] secp256k1.secp256k1_ecdsa_signature_parse_compact.restype = c_int secp256k1.secp256k1_ecdsa_signature_serialize_compact.argtypes = [ c_void_p, c_char_p, c_char_p, ] secp256k1.secp256k1_ecdsa_signature_serialize_compact.restype = c_int secp256k1.secp256k1_ecdsa_signature_parse_der.argtypes = [ c_void_p, c_char_p, c_char_p, c_uint, ] secp256k1.secp256k1_ecdsa_signature_parse_der.restype = c_int secp256k1.secp256k1_ecdsa_signature_serialize_der.argtypes = [ c_void_p, c_char_p, c_void_p, c_char_p, ] secp256k1.secp256k1_ecdsa_signature_serialize_der.restype = c_int secp256k1.secp256k1_ecdsa_sign.argtypes = [ c_void_p, c_char_p, c_char_p, c_char_p, c_void_p, c_char_p, ] secp256k1.secp256k1_ecdsa_sign.restype = c_int secp256k1.secp256k1_ecdsa_verify.argtypes = [c_void_p, c_char_p, c_char_p, c_char_p] secp256k1.secp256k1_ecdsa_verify.restype = c_int secp256k1.secp256k1_ec_pubkey_combine.argtypes = [ c_void_p, c_char_p, c_void_p, c_size_t, ] secp256k1.secp256k1_ec_pubkey_combine.restype = c_int # ecdh try: secp256k1.secp256k1_ecdh.argtypes = [ c_void_p, # ctx c_char_p, # output c_char_p, # point c_char_p, # scalar CFUNCTYPE, # hashfp c_void_p, # data ] secp256k1.secp256k1_ecdh.restype = c_int except: pass # schnorr sig try: secp256k1.secp256k1_xonly_pubkey_from_pubkey.argtypes = [ c_void_p, # ctx c_char_p, # xonly pubkey POINTER(c_int), # parity c_char_p, # pubkey ] secp256k1.secp256k1_xonly_pubkey_from_pubkey.restype = c_int secp256k1.secp256k1_schnorrsig_verify.argtypes = [ c_void_p, # ctx c_char_p, # sig c_char_p, # msg c_char_p, # pubkey ] secp256k1.secp256k1_schnorrsig_verify.restype = c_int secp256k1.secp256k1_schnorrsig_sign.argtypes = [ c_void_p, # ctx c_char_p, # sig c_char_p, # msg c_char_p, # keypair c_void_p, # nonce_function c_char_p, # extra data ] secp256k1.secp256k1_schnorrsig_sign.restype = c_int secp256k1.secp256k1_keypair_create.argtypes = [ c_void_p, # ctx c_char_p, # keypair c_char_p, # secret ] secp256k1.secp256k1_keypair_create.restype = c_int except: pass # recoverable module try: secp256k1.secp256k1_ecdsa_sign_recoverable.argtypes = [ c_void_p, c_char_p, c_char_p, c_char_p, c_void_p, c_void_p, ] secp256k1.secp256k1_ecdsa_sign_recoverable.restype = c_int secp256k1.secp256k1_ecdsa_recoverable_signature_parse_compact.argtypes = [ c_void_p, c_char_p, c_char_p, c_int, ] secp256k1.secp256k1_ecdsa_recoverable_signature_parse_compact.restype = c_int secp256k1.secp256k1_ecdsa_recoverable_signature_serialize_compact.argtypes = [ c_void_p, c_char_p, c_char_p, c_char_p, ] secp256k1.secp256k1_ecdsa_recoverable_signature_serialize_compact.restype = ( c_int ) secp256k1.secp256k1_ecdsa_recoverable_signature_convert.argtypes = [ c_void_p, c_char_p, c_char_p, ] secp256k1.secp256k1_ecdsa_recoverable_signature_convert.restype = c_int secp256k1.secp256k1_ecdsa_recover.argtypes = [ c_void_p, c_char_p, c_char_p, c_char_p, ] secp256k1.secp256k1_ecdsa_recover.restype = c_int except: pass # zkp modules try: # generator module secp256k1.secp256k1_generator_parse.argtypes = [c_void_p, c_char_p, c_char_p] secp256k1.secp256k1_generator_parse.restype = c_int secp256k1.secp256k1_generator_serialize.argtypes = [ c_void_p, c_char_p, c_char_p, ] secp256k1.secp256k1_generator_serialize.restype = c_int secp256k1.secp256k1_generator_generate.argtypes = [c_void_p, c_char_p, c_char_p] secp256k1.secp256k1_generator_generate.restype = c_int secp256k1.secp256k1_generator_generate_blinded.argtypes = [ c_void_p, c_char_p, c_char_p, c_char_p, ] secp256k1.secp256k1_generator_generate_blinded.restype = c_int # pederson commitments secp256k1.secp256k1_pedersen_commitment_parse.argtypes = [ c_void_p, c_char_p, c_char_p, ] secp256k1.secp256k1_pedersen_commitment_parse.restype = c_int secp256k1.secp256k1_pedersen_commitment_serialize.argtypes = [ c_void_p, c_char_p, c_char_p, ] secp256k1.secp256k1_pedersen_commitment_serialize.restype = c_int secp256k1.secp256k1_pedersen_commit.argtypes = [ c_void_p, c_char_p, c_char_p, c_uint64, c_char_p, ] secp256k1.secp256k1_pedersen_commit.restype = c_int secp256k1.secp256k1_pedersen_blind_generator_blind_sum.argtypes = [ c_void_p, # const secp256k1_context* ctx, POINTER(c_uint64), # const uint64_t *value, c_void_p, # const unsigned char* const* generator_blind, c_void_p, # unsigned char* const* blinding_factor, c_size_t, # size_t n_total, c_size_t, # size_t n_inputs ] secp256k1.secp256k1_pedersen_blind_generator_blind_sum.restype = c_int secp256k1.secp256k1_pedersen_verify_tally.argtypes = [ c_void_p, c_void_p, c_size_t, c_void_p, c_size_t, ] secp256k1.secp256k1_pedersen_verify_tally.restype = c_int # rangeproof secp256k1.secp256k1_rangeproof_rewind.argtypes = [ c_void_p, # ctx c_char_p, # vbf out POINTER(c_uint64), # value out c_char_p, # message out POINTER(c_uint64), # msg out len c_char_p, # nonce POINTER(c_uint64), # min value POINTER(c_uint64), # max value c_char_p, # pedersen commitment c_char_p, # range proof c_uint64, # proof len c_char_p, # extra commitment (scriptpubkey) c_uint64, # extra len c_char_p, # generator ] secp256k1.secp256k1_rangeproof_rewind.restype = c_int secp256k1.secp256k1_rangeproof_verify.argtypes = [ c_void_p, # ctx POINTER(c_uint64), # min value POINTER(c_uint64), # max value c_char_p, # pedersen commitment c_char_p, # proof c_uint64, # proof len c_char_p, # extra c_uint64, # extra len c_char_p, # generator ] secp256k1.secp256k1_rangeproof_verify.restype = c_int secp256k1.secp256k1_rangeproof_sign.argtypes = [ c_void_p, # ctx c_char_p, # proof POINTER(c_uint64), # plen c_uint64, # min_value c_char_p, # commit c_char_p, # blind c_char_p, # nonce c_int, # exp c_int, # min_bits c_uint64, # value c_char_p, # message c_uint64, # msg_len c_char_p, # extra_commit c_uint64, # extra_commit_len c_char_p, # gen ] secp256k1.secp256k1_rangeproof_sign.restype = c_int # musig secp256k1.secp256k1_musig_pubkey_combine.argtypes = [ c_void_p, c_void_p, c_char_p, c_void_p, c_void_p, c_size_t, ] secp256k1.secp256k1_musig_pubkey_combine.restype = c_int # surjection proofs secp256k1.secp256k1_surjectionproof_initialize.argtypes = [ c_void_p, # const secp256k1_context* ctx, c_char_p, # secp256k1_surjectionproof* proof, POINTER(c_size_t), # size_t *input_index, c_void_p, # c_char_p, # const secp256k1_fixed_asset_tag* fixed_input_tags, c_size_t, # const size_t n_input_tags, c_size_t, # const size_t n_input_tags_to_use, c_char_p, # const secp256k1_fixed_asset_tag* fixed_output_tag, c_size_t, # const size_t n_max_iterations, c_char_p, # const unsigned char *random_seed32 ] secp256k1.secp256k1_surjectionproof_initialize.restype = c_int secp256k1.secp256k1_surjectionproof_generate.argtypes = [ c_void_p, # const secp256k1_context* ctx, c_char_p, # secp256k1_surjectionproof* proof, c_char_p, # const secp256k1_generator* ephemeral_input_tags, c_size_t, # size_t n_ephemeral_input_tags, c_char_p, # const secp256k1_generator* ephemeral_output_tag, c_size_t, # size_t input_index, c_char_p, # const unsigned char *input_blinding_key, c_char_p, # const unsigned char *output_blinding_key ] secp256k1.secp256k1_surjectionproof_generate.restype = c_int secp256k1.secp256k1_surjectionproof_verify.argtypes = [ c_void_p, # const secp256k1_context* ctx, c_char_p, # const secp256k1_surjectionproof* proof, c_char_p, # const secp256k1_generator* ephemeral_input_tags, c_size_t, # size_t n_ephemeral_input_tags, c_char_p, # const secp256k1_generator* ephemeral_output_tag ] secp256k1.secp256k1_surjectionproof_verify.restype = c_int secp256k1.secp256k1_surjectionproof_serialize.argtypes = [ c_void_p, # const secp256k1_context* ctx, c_char_p, # unsigned char *output, POINTER(c_size_t), # size_t *outputlen, c_char_p, # const secp256k1_surjectionproof *proof ] secp256k1.secp256k1_surjectionproof_serialize.restype = c_int secp256k1.secp256k1_surjectionproof_serialized_size.argtypes = [ c_void_p, # const secp256k1_context* ctx, c_char_p, # const secp256k1_surjectionproof* proof ] secp256k1.secp256k1_surjectionproof_serialized_size.restype = c_size_t secp256k1.secp256k1_surjectionproof_parse.argtypes = [ c_void_p, c_char_p, c_char_p, c_size_t, ] secp256k1.secp256k1_surjectionproof_parse.restype = c_int except: pass secp256k1.ctx = secp256k1.secp256k1_context_create(flags) r = secp256k1.secp256k1_context_randomize(secp256k1.ctx, os.urandom(32)) return secp256k1 _secp = _init() # bindings equal to ones in micropython @locked def context_randomize(seed, context=_secp.ctx): if len(seed) != 32: raise ValueError("Seed should be 32 bytes long") if _secp.secp256k1_context_randomize(context, seed) == 0: raise RuntimeError("Failed to randomize context") @locked def ec_pubkey_create(secret, context=_secp.ctx): if len(secret) != 32: raise ValueError("Private key should be 32 bytes long") pub = bytes(64) r = _secp.secp256k1_ec_pubkey_create(context, pub, secret) if r == 0: raise ValueError("Invalid private key") return pub @locked def ec_pubkey_parse(sec, context=_secp.ctx): if len(sec) != 33 and len(sec) != 65: raise ValueError("Serialized pubkey should be 33 or 65 bytes long") if len(sec) == 33: if sec[0] != 0x02 and sec[0] != 0x03: raise ValueError("Compressed pubkey should start with 0x02 or 0x03") else: if sec[0] != 0x04: raise ValueError("Uncompressed pubkey should start with 0x04") pub = bytes(64) r = _secp.secp256k1_ec_pubkey_parse(context, pub, sec, len(sec)) if r == 0: raise ValueError("Failed parsing public key") return pub @locked def ec_pubkey_serialize(pubkey, flag=EC_COMPRESSED, context=_secp.ctx): if len(pubkey) != 64: raise ValueError("Pubkey should be 64 bytes long") if flag not in [EC_COMPRESSED, EC_UNCOMPRESSED]: raise ValueError("Invalid flag") sec = bytes(33) if (flag == EC_COMPRESSED) else bytes(65) sz = c_size_t(len(sec)) r = _secp.secp256k1_ec_pubkey_serialize(context, sec, byref(sz), pubkey, flag) if r == 0: raise ValueError("Failed to serialize pubkey") return sec @locked def ecdsa_signature_parse_compact(compact_sig, context=_secp.ctx): if len(compact_sig) != 64: raise ValueError("Compact signature should be 64 bytes long") sig = bytes(64) r = _secp.secp256k1_ecdsa_signature_parse_compact(context, sig, compact_sig) if r == 0: raise ValueError("Failed parsing compact signature") return sig @locked def ecdsa_signature_parse_der(der, context=_secp.ctx): sig = bytes(64) r = _secp.secp256k1_ecdsa_signature_parse_der(context, sig, der, len(der)) if r == 0: raise ValueError("Failed parsing compact signature") return sig @locked def ecdsa_signature_serialize_der(sig, context=_secp.ctx): if len(sig) != 64: raise ValueError("Signature should be 64 bytes long") der = bytes(78) # max sz = c_size_t(len(der)) r = _secp.secp256k1_ecdsa_signature_serialize_der(context, der, byref(sz), sig) if r == 0: raise ValueError("Failed serializing der signature") return der[: sz.value] @locked def ecdsa_signature_serialize_compact(sig, context=_secp.ctx): if len(sig) != 64: raise ValueError("Signature should be 64 bytes long") ser = bytes(64) r = _secp.secp256k1_ecdsa_signature_serialize_compact(context, ser, sig) if r == 0: raise ValueError("Failed serializing der signature") return ser @locked def ecdsa_signature_normalize(sig, context=_secp.ctx): if len(sig) != 64: raise ValueError("Signature should be 64 bytes long") sig2 = bytes(64) r = _secp.secp256k1_ecdsa_signature_normalize(context, sig2, sig) return sig2 @locked def ecdsa_verify(sig, msg, pub, context=_secp.ctx): if len(sig) != 64: raise ValueError("Signature should be 64 bytes long") if len(msg) != 32: raise ValueError("Message should be 32 bytes long") if len(pub) != 64: raise ValueError("Public key should be 64 bytes long") r = _secp.secp256k1_ecdsa_verify(context, sig, msg, pub) return bool(r) @locked def ecdsa_sign(msg, secret, nonce_function=None, extra_data=None, context=_secp.ctx): if len(msg) != 32: raise ValueError("Message should be 32 bytes long") if len(secret) != 32: raise ValueError("Secret key should be 32 bytes long") if extra_data and len(extra_data) != 32: raise ValueError("Extra data should be 32 bytes long") sig = bytes(64) r = _secp.secp256k1_ecdsa_sign( context, sig, msg, secret, nonce_function, extra_data ) if r == 0: raise ValueError("Failed to sign") return sig @locked def ec_seckey_verify(secret, context=_secp.ctx): if len(secret) != 32: raise ValueError("Secret should be 32 bytes long") return bool(_secp.secp256k1_ec_seckey_verify(context, secret)) @locked def ec_privkey_negate(secret, context=_secp.ctx): if len(secret) != 32: raise ValueError("Secret should be 32 bytes long") b = _copy(secret) _secp.secp256k1_ec_privkey_negate(context, b) return b @locked def ec_pubkey_negate(pubkey, context=_secp.ctx): if len(pubkey) != 64: raise ValueError("Pubkey should be a 64-byte structure") pub = _copy(pubkey) r = _secp.secp256k1_ec_pubkey_negate(context, pub) if r == 0: raise ValueError("Failed to negate pubkey") return pub @locked def ec_privkey_tweak_add(secret, tweak, context=_secp.ctx): if len(secret) != 32 or len(tweak) != 32: raise ValueError("Secret and tweak should both be 32 bytes long") t = _copy(tweak) if _secp.secp256k1_ec_privkey_tweak_add(context, secret, tweak) == 0: raise ValueError("Failed to tweak the secret") return None @locked def ec_pubkey_tweak_add(pub, tweak, context=_secp.ctx): if len(pub) != 64: raise ValueError("Public key should be 64 bytes long") if len(tweak) != 32: raise ValueError("Tweak should be 32 bytes long") t = _copy(tweak) if _secp.secp256k1_ec_pubkey_tweak_add(context, pub, tweak) == 0: raise ValueError("Failed to tweak the public key") return None @locked def ec_privkey_add(secret, tweak, context=_secp.ctx): if len(secret) != 32 or len(tweak) != 32: raise ValueError("Secret and tweak should both be 32 bytes long") # ugly copy that works in mpy and py s = _copy(secret) t = _copy(tweak) if _secp.secp256k1_ec_privkey_tweak_add(context, s, t) == 0: raise ValueError("Failed to tweak the secret") return s @locked def ec_pubkey_add(pub, tweak, context=_secp.ctx): if len(pub) != 64: raise ValueError("Public key should be 64 bytes long") if len(tweak) != 32: raise ValueError("Tweak should be 32 bytes long") p = _copy(pub) if _secp.secp256k1_ec_pubkey_tweak_add(context, p, tweak) == 0: raise ValueError("Failed to tweak the public key") return p @locked def ec_privkey_tweak_mul(secret, tweak, context=_secp.ctx): if len(secret) != 32 or len(tweak) != 32: raise ValueError("Secret and tweak should both be 32 bytes long") if _secp.secp256k1_ec_privkey_tweak_mul(context, secret, tweak) == 0: raise ValueError("Failed to tweak the secret") @locked def ec_pubkey_tweak_mul(pub, tweak, context=_secp.ctx): if len(pub) != 64: raise ValueError("Public key should be 64 bytes long") if len(tweak) != 32: raise ValueError("Tweak should be 32 bytes long") if _secp.secp256k1_ec_pubkey_tweak_mul(context, pub, tweak) == 0: raise ValueError("Failed to tweak the public key") @locked def ec_pubkey_combine(*args, context=_secp.ctx): pub = bytes(64) pubkeys = (c_char_p * len(args))(*args) r = _secp.secp256k1_ec_pubkey_combine(context, pub, pubkeys, len(args)) if r == 0: raise ValueError("Failed to combine pubkeys") return pub # ecdh @locked def ecdh(pubkey, scalar, hashfn=None, data=None, context=_secp.ctx): if not len(pubkey) == 64: raise ValueError("Pubkey should be 64 bytes long") if not len(scalar) == 32: raise ValueError("Scalar should be 32 bytes long") secret = bytes(32) if hashfn is None: res = _secp.secp256k1_ecdh(context, secret, pubkey, scalar, None, None) else: def _hashfn(out, x, y): x = ctypes.string_at(x, 32) y = ctypes.string_at(y, 32) try: res = hashfn(x, y, data) except Exception as e: return 0 out = cast(out, POINTER(c_char * 32)) out.contents.value = res return 1 HASHFN = CFUNCTYPE(c_int, c_void_p, c_void_p, c_void_p) res = _secp.secp256k1_ecdh( context, secret, pubkey, scalar, HASHFN(_hashfn), data ) if res != 1: raise RuntimeError("Failed to compute the shared secret") return secret # schnorrsig @locked def xonly_pubkey_from_pubkey(pubkey, context=_secp.ctx): if len(pubkey) != 64: raise ValueError("Pubkey should be 64 bytes long") pointer = POINTER(c_int) parity = pointer(c_int(0)) xonly_pub = bytes(64) res = _secp.secp256k1_xonly_pubkey_from_pubkey(context, xonly_pub, parity, pubkey) if res != 1: raise RuntimeError("Failed to convert the pubkey") return xonly_pub, bool(parity.contents.value) @locked def schnorrsig_verify(sig, msg, pubkey, context=_secp.ctx): assert len(sig) == 64 assert len(msg) == 32 assert len(pubkey) == 64 res = _secp.secp256k1_schnorrsig_verify(context, sig, msg, pubkey) return bool(res) @locked def keypair_create(secret, context=_secp.ctx): assert len(secret) == 32 keypair = bytes(96) r = _secp.secp256k1_keypair_create(context, keypair, secret) if r == 0: raise ValueError("Failed to create keypair") return keypair # not @locked because it uses keypair_create inside def schnorrsig_sign( msg, keypair, nonce_function=None, extra_data=None, context=_secp.ctx ): assert len(msg) == 32 if len(keypair) == 32: keypair = keypair_create(keypair, context=context) with _lock: assert len(keypair) == 96 sig = bytes(64) r = _secp.secp256k1_schnorrsig_sign( context, sig, msg, keypair, nonce_function, extra_data ) if r == 0: raise ValueError("Failed to sign") return sig # recoverable @locked def ecdsa_sign_recoverable(msg, secret, context=_secp.ctx): if len(msg) != 32: raise ValueError("Message should be 32 bytes long") if len(secret) != 32: raise ValueError("Secret key should be 32 bytes long") sig = bytes(65) r = _secp.secp256k1_ecdsa_sign_recoverable(context, sig, msg, secret, None, None) if r == 0: raise ValueError("Failed to sign") return sig @locked def ecdsa_recoverable_signature_serialize_compact(sig, context=_secp.ctx): if len(sig) != 65: raise ValueError("Recoverable signature should be 65 bytes long") ser = bytes(64) idx = bytes(1) r = _secp.secp256k1_ecdsa_recoverable_signature_serialize_compact( context, ser, idx, sig ) if r == 0: raise ValueError("Failed serializing der signature") return ser, idx[0] @locked def ecdsa_recoverable_signature_parse_compact(compact_sig, recid, context=_secp.ctx): if len(compact_sig) != 64: raise ValueError("Signature should be 64 bytes long") sig = bytes(65) r = _secp.secp256k1_ecdsa_recoverable_signature_parse_compact( context, sig, compact_sig, recid ) if r == 0: raise ValueError("Failed parsing compact signature") return sig @locked def ecdsa_recoverable_signature_convert(sigin, context=_secp.ctx): if len(sigin) != 65: raise ValueError("Recoverable signature should be 65 bytes long") sig = bytes(64) r = _secp.secp256k1_ecdsa_recoverable_signature_convert(context, sig, sigin) if r == 0: raise ValueError("Failed converting signature") return sig @locked def ecdsa_recover(sig, msghash, context=_secp.ctx): if len(sig) != 65: raise ValueError("Recoverable signature should be 65 bytes long") if len(msghash) != 32: raise ValueError("Message should be 32 bytes long") pub = bytes(64) r = _secp.secp256k1_ecdsa_recover(context, pub, sig, msghash) if r == 0: raise ValueError("Failed to recover public key") return pub # zkp modules @locked def pedersen_commitment_parse(inp, context=_secp.ctx): if len(inp) != 33: raise ValueError("Serialized commitment should be 33 bytes long") commit = bytes(64) r = _secp.secp256k1_pedersen_commitment_parse(context, commit, inp) if r == 0: raise ValueError("Failed to parse commitment") return commit @locked def pedersen_commitment_serialize(commit, context=_secp.ctx): if len(commit) != 64: raise ValueError("Commitment should be 64 bytes long") sec = bytes(33) r = _secp.secp256k1_pedersen_commitment_serialize(context, sec, commit) if r == 0: raise ValueError("Failed to serialize commitment") return sec @locked def pedersen_commit(vbf, value, gen, context=_secp.ctx): if len(gen) != 64: raise ValueError("Generator should be 64 bytes long") if len(vbf) != 32: raise ValueError(f"Blinding factor should be 32 bytes long, not {len(vbf)}") commit = bytes(64) r = _secp.secp256k1_pedersen_commit(context, commit, vbf, value, gen) if r == 0: raise ValueError("Failed to create commitment") return commit @locked def pedersen_blind_generator_blind_sum( values, gens, vbfs, num_inputs, context=_secp.ctx ): vals = (c_uint64 * len(values))(*values) vbf = bytes(vbfs[-1]) p = c_char_p(vbf) # obtain a pointer of various types address = cast(p, c_void_p).value vbfs_joined = (c_char_p * len(vbfs))(*vbfs[:-1], address) gens_joined = (c_char_p * len(gens))(*gens) res = _secp.secp256k1_pedersen_blind_generator_blind_sum( context, vals, gens_joined, vbfs_joined, len(values), num_inputs ) if res == 0: raise ValueError("Failed to get the last blinding factor.") res = (c_char * 32).from_address(address).raw assert len(res) == 32 return res @locked def pedersen_verify_tally(ins, outs, context=_secp.ctx): in_ptr = (c_char_p * len(ins))(*ins) out_ptr = (c_char_p * len(outs))(*outs) res = _secp.secp256k1_pedersen_verify_tally( context, in_ptr, len(in_ptr), out_ptr, len(out_ptr) ) return bool(res) # generator @locked def generator_parse(inp, context=_secp.ctx): if len(inp) != 33: raise ValueError("Serialized generator should be 33 bytes long") gen = bytes(64) r = _secp.secp256k1_generator_parse(context, gen, inp) if r == 0: raise ValueError("Failed to parse generator") return gen @locked def generator_generate(asset, context=_secp.ctx): if len(asset) != 32: raise ValueError("Asset should be 32 bytes long") gen = bytes(64) r = _secp.secp256k1_generator_generate(context, gen, asset) if r == 0: raise ValueError("Failed to generate generator") return gen @locked def generator_generate_blinded(asset, abf, context=_secp.ctx): if len(asset) != 32: raise ValueError("Asset should be 32 bytes long") if len(abf) != 32: raise ValueError("Asset blinding factor should be 32 bytes long") gen = bytes(64) r = _secp.secp256k1_generator_generate_blinded(context, gen, asset, abf) if r == 0: raise ValueError("Failed to generate generator") return gen @locked def generator_serialize(generator, context=_secp.ctx): if len(generator) != 64: raise ValueError("Generator should be 64 bytes long") sec = bytes(33) if _secp.secp256k1_generator_serialize(context, sec, generator) == 0: raise RuntimeError("Failed to serialize generator") return sec # rangeproof @locked def rangeproof_rewind( proof, nonce, value_commitment, script_pubkey, generator, message_length=64, context=_secp.ctx, ): if len(generator) != 64: raise ValueError("Generator should be 64 bytes long") if len(nonce) != 32: raise ValueError("Nonce should be 32 bytes long") if len(value_commitment) != 64: raise ValueError("Value commitment should be 64 bytes long") pointer = POINTER(c_uint64) msg = b"\x00" * message_length msglen = pointer(c_uint64(len(msg))) vbf_out = b"\x00" * 32 value_out = pointer(c_uint64(0)) min_value = pointer(c_uint64(0)) max_value = pointer(c_uint64(0)) res = _secp.secp256k1_rangeproof_rewind( context, vbf_out, value_out, msg, msglen, nonce, min_value, max_value, value_commitment, proof, len(proof), script_pubkey, len(script_pubkey), generator, ) if res != 1: raise RuntimeError("Failed to rewind the proof") return ( value_out.contents.value, vbf_out, msg[: msglen.contents.value], min_value.contents.value, max_value.contents.value, ) # rangeproof @locked def rangeproof_verify( proof, value_commitment, script_pubkey, generator, context=_secp.ctx ): if len(generator) != 64: raise ValueError("Generator should be 64 bytes long") if len(value_commitment) != 64: raise ValueError("Value commitment should be 64 bytes long") pointer = POINTER(c_uint64) min_value = pointer(c_uint64(0)) max_value = pointer(c_uint64(0)) res = _secp.secp256k1_rangeproof_verify( context, min_value, max_value, value_commitment, proof, len(proof), script_pubkey, len(script_pubkey), generator, ) if res != 1: raise RuntimeError("Failed to verify the proof") return min_value.contents.value, max_value.contents.value @locked def rangeproof_sign( nonce, value, value_commitment, vbf, message, extra, gen, min_value=1, exp=0, min_bits=52, context=_secp.ctx, ): if value == 0: min_value = 0 if len(gen) != 64: raise ValueError("Generator should be 64 bytes long") if len(nonce) != 32: raise ValueError("Nonce should be 32 bytes long") if len(value_commitment) != 64: raise ValueError("Value commitment should be 64 bytes long") if len(vbf) != 32: raise ValueError("Value blinding factor should be 32 bytes long") proof = bytes(5134) pointer = POINTER(c_uint64) prooflen = pointer(c_uint64(len(proof))) res = _secp.secp256k1_rangeproof_sign( context, proof, prooflen, min_value, value_commitment, vbf, nonce, exp, min_bits, value, message, len(message), extra, len(extra), gen, ) if res != 1: raise RuntimeError("Failed to generate the proof") return bytes(proof[: prooflen.contents.value]) @locked def musig_pubkey_combine(*args, context=_secp.ctx): pub = bytes(64) # TODO: strange that behaviour is different from pubkey_combine... pubkeys = b"".join(args) # (c_char_p * len(args))(*args) res = _secp.secp256k1_musig_pubkey_combine( context, None, pub, None, pubkeys, len(args) ) if res == 0: raise ValueError("Failed to combine pubkeys") return pub # surjection proof @locked def surjectionproof_initialize( in_tags, out_tag, seed, tags_to_use=None, iterations=100, context=_secp.ctx ): if tags_to_use is None: tags_to_use = min(3, len(in_tags)) if seed is None: seed = os.urandom(32) proof = bytes(4 + 8 + 256 // 8 + 32 * 257) pointer = POINTER(c_size_t) input_index = pointer(c_size_t(0)) input_tags = b"".join(in_tags) res = _secp.secp256k1_surjectionproof_initialize( context, proof, input_index, input_tags, len(in_tags), tags_to_use, out_tag, iterations, seed, ) if res == 0: raise RuntimeError("Failed to initialize the proof") return proof, input_index.contents.value @locked def surjectionproof_generate( proof, in_idx, in_tags, out_tag, in_abf, out_abf, context=_secp.ctx ): res = _secp.secp256k1_surjectionproof_generate( context, proof, b"".join(in_tags), len(in_tags), out_tag, in_idx, in_abf, out_abf, ) if not res: raise RuntimeError("Failed to generate surjection proof") return proof @locked def surjectionproof_verify(proof, in_tags, out_tag, context=_secp.ctx): res = _secp.secp256k1_surjectionproof_verify( context, proof, b"".join(in_tags), len(in_tags), out_tag ) return bool(res) @locked def surjectionproof_serialize(proof, context=_secp.ctx): s = _secp.secp256k1_surjectionproof_serialized_size(context, proof) b = bytes(s) pointer = POINTER(c_size_t) sz = pointer(c_size_t(s)) _secp.secp256k1_surjectionproof_serialize(context, b, sz, proof) if s != sz.contents.value: raise RuntimeError("Failed to serialize surjection proof - size mismatch") return b @locked def surjectionproof_parse(proof, context=_secp.ctx): parsed_proof = bytes(4 + 8 + 256 // 8 + 32 * 257) res = _secp.secp256k1_surjectionproof_parse( context, parsed_proof, proof, len(proof) ) if res == 0: raise RuntimeError("Failed to parse surjection proof") return parsed_proof ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/util/key.py0000664000175000017500000004536214623570367022563 0ustar00singalasingala""" Copy-paste from key.py in bitcoin test_framework. This is a fallback option if the library can't do ctypes bindings to secp256k1 library. """ import random import hmac import hashlib def TaggedHash(tag, data): ss = hashlib.sha256(tag.encode("utf-8")).digest() ss += ss ss += data return hashlib.sha256(ss).digest() def modinv(a, n): """Compute the modular inverse of a modulo n using the extended Euclidean Algorithm. See https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm#Modular_integers. """ # TODO: Change to pow(a, -1, n) available in Python 3.8 t1, t2 = 0, 1 r1, r2 = n, a while r2 != 0: q = r1 // r2 t1, t2 = t2, t1 - q * t2 r1, r2 = r2, r1 - q * r2 if r1 > 1: return None if t1 < 0: t1 += n return t1 def xor_bytes(b0, b1): return bytes(x ^ y for (x, y) in zip(b0, b1)) def jacobi_symbol(n, k): """Compute the Jacobi symbol of n modulo k See http://en.wikipedia.org/wiki/Jacobi_symbol For our application k is always prime, so this is the same as the Legendre symbol. """ assert k > 0 and k & 1, "jacobi symbol is only defined for positive odd k" n %= k t = 0 while n != 0: while n & 1 == 0: n >>= 1 r = k & 7 t ^= r == 3 or r == 5 n, k = k, n t ^= n & k & 3 == 3 n = n % k if k == 1: return -1 if t else 1 return 0 def modsqrt(a, p): """Compute the square root of a modulo p when p % 4 = 3. The Tonelli-Shanks algorithm can be used. See https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm Limiting this function to only work for p % 4 = 3 means we don't need to iterate through the loop. The highest n such that p - 1 = 2^n Q with Q odd is n = 1. Therefore Q = (p-1)/2 and sqrt = a^((Q+1)/2) = a^((p+1)/4) secp256k1's is defined over field of size 2**256 - 2**32 - 977, which is 3 mod 4. """ if p % 4 != 3: raise NotImplementedError("modsqrt only implemented for p % 4 = 3") sqrt = pow(a, (p + 1) // 4, p) if pow(sqrt, 2, p) == a % p: return sqrt return None class EllipticCurve: def __init__(self, p, a, b): """Initialize elliptic curve y^2 = x^3 + a*x + b over GF(p).""" self.p = p self.a = a % p self.b = b % p def affine(self, p1): """Convert a Jacobian point tuple p1 to affine form, or None if at infinity. An affine point is represented as the Jacobian (x, y, 1)""" x1, y1, z1 = p1 if z1 == 0: return None inv = modinv(z1, self.p) inv_2 = (inv**2) % self.p inv_3 = (inv_2 * inv) % self.p return ((inv_2 * x1) % self.p, (inv_3 * y1) % self.p, 1) def has_even_y(self, p1): """Whether the point p1 has an even Y coordinate when expressed in affine coordinates.""" return not (p1[2] == 0 or self.affine(p1)[1] & 1) def negate(self, p1): """Negate a Jacobian point tuple p1.""" x1, y1, z1 = p1 return (x1, (self.p - y1) % self.p, z1) def on_curve(self, p1): """Determine whether a Jacobian tuple p is on the curve (and not infinity)""" x1, y1, z1 = p1 z2 = pow(z1, 2, self.p) z4 = pow(z2, 2, self.p) return ( z1 != 0 and ( pow(x1, 3, self.p) + self.a * x1 * z4 + self.b * z2 * z4 - pow(y1, 2, self.p) ) % self.p == 0 ) def is_x_coord(self, x): """Test whether x is a valid X coordinate on the curve.""" x_3 = pow(x, 3, self.p) return jacobi_symbol(x_3 + self.a * x + self.b, self.p) != -1 def lift_x(self, x): """Given an X coordinate on the curve, return a corresponding affine point for which the Y coordinate is even.""" x_3 = pow(x, 3, self.p) v = x_3 + self.a * x + self.b y = modsqrt(v, self.p) if y is None: return None return (x, self.p - y if y & 1 else y, 1) def double(self, p1): """Double a Jacobian tuple p1 See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Doubling """ x1, y1, z1 = p1 if z1 == 0: return (0, 1, 0) y1_2 = (y1**2) % self.p y1_4 = (y1_2**2) % self.p x1_2 = (x1**2) % self.p s = (4 * x1 * y1_2) % self.p m = 3 * x1_2 if self.a: m += self.a * pow(z1, 4, self.p) m = m % self.p x2 = (m**2 - 2 * s) % self.p y2 = (m * (s - x2) - 8 * y1_4) % self.p z2 = (2 * y1 * z1) % self.p return (x2, y2, z2) def add_mixed(self, p1, p2): """Add a Jacobian tuple p1 and an affine tuple p2 See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition (with affine point) """ x1, y1, z1 = p1 x2, y2, z2 = p2 assert z2 == 1 # Adding to the point at infinity is a no-op if z1 == 0: return p2 z1_2 = (z1**2) % self.p z1_3 = (z1_2 * z1) % self.p u2 = (x2 * z1_2) % self.p s2 = (y2 * z1_3) % self.p if x1 == u2: if y1 != s2: # p1 and p2 are inverses. Return the point at infinity. return (0, 1, 0) # p1 == p2. The formulas below fail when the two points are equal. return self.double(p1) h = u2 - x1 r = s2 - y1 h_2 = (h**2) % self.p h_3 = (h_2 * h) % self.p u1_h_2 = (x1 * h_2) % self.p x3 = (r**2 - h_3 - 2 * u1_h_2) % self.p y3 = (r * (u1_h_2 - x3) - y1 * h_3) % self.p z3 = (h * z1) % self.p return (x3, y3, z3) def add(self, p1, p2): """Add two Jacobian tuples p1 and p2 See https://en.wikibooks.org/wiki/Cryptography/Prime_Curve/Jacobian_Coordinates - Point Addition """ x1, y1, z1 = p1 x2, y2, z2 = p2 # Adding the point at infinity is a no-op if z1 == 0: return p2 if z2 == 0: return p1 # Adding an Affine to a Jacobian is more efficient since we save field multiplications and squarings when z = 1 if z1 == 1: return self.add_mixed(p2, p1) if z2 == 1: return self.add_mixed(p1, p2) z1_2 = (z1**2) % self.p z1_3 = (z1_2 * z1) % self.p z2_2 = (z2**2) % self.p z2_3 = (z2_2 * z2) % self.p u1 = (x1 * z2_2) % self.p u2 = (x2 * z1_2) % self.p s1 = (y1 * z2_3) % self.p s2 = (y2 * z1_3) % self.p if u1 == u2: if s1 != s2: # p1 and p2 are inverses. Return the point at infinity. return (0, 1, 0) # p1 == p2. The formulas below fail when the two points are equal. return self.double(p1) h = u2 - u1 r = s2 - s1 h_2 = (h**2) % self.p h_3 = (h_2 * h) % self.p u1_h_2 = (u1 * h_2) % self.p x3 = (r**2 - h_3 - 2 * u1_h_2) % self.p y3 = (r * (u1_h_2 - x3) - s1 * h_3) % self.p z3 = (h * z1 * z2) % self.p return (x3, y3, z3) def mul(self, ps): """Compute a (multi) point multiplication ps is a list of (Jacobian tuple, scalar) pairs. """ r = (0, 1, 0) for i in range(255, -1, -1): r = self.double(r) for p, n in ps: if (n >> i) & 1: r = self.add(r, p) return r SECP256K1_FIELD_SIZE = 2**256 - 2**32 - 977 SECP256K1 = EllipticCurve(SECP256K1_FIELD_SIZE, 0, 7) SECP256K1_G = ( 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8, 1, ) SECP256K1_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 SECP256K1_ORDER_HALF = SECP256K1_ORDER // 2 class ECPubKey: """A secp256k1 public key""" def __init__(self): """Construct an uninitialized public key""" self.valid = False def set(self, data): """Construct a public key from a serialization in compressed or uncompressed format""" if len(data) == 65 and data[0] == 0x04: p = ( int.from_bytes(data[1:33], "big"), int.from_bytes(data[33:65], "big"), 1, ) self.valid = SECP256K1.on_curve(p) if self.valid: self.p = p self.compressed = False elif len(data) == 33 and (data[0] == 0x02 or data[0] == 0x03): x = int.from_bytes(data[1:33], "big") if SECP256K1.is_x_coord(x): p = SECP256K1.lift_x(x) # Make the Y coordinate odd if required (lift_x always produces # a point with an even Y coordinate). if data[0] & 1: p = SECP256K1.negate(p) self.p = p self.valid = True self.compressed = True else: self.valid = False else: self.valid = False @property def is_compressed(self): return self.compressed @property def is_valid(self): return self.valid def get_bytes(self): assert self.valid p = SECP256K1.affine(self.p) if p is None: return None if self.compressed: return bytes([0x02 + (p[1] & 1)]) + p[0].to_bytes(32, "big") else: return bytes([0x04]) + p[0].to_bytes(32, "big") + p[1].to_bytes(32, "big") def verify_ecdsa(self, sig, msg, low_s=True): """Verify a strictly DER-encoded ECDSA signature against this pubkey. See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the ECDSA verifier algorithm""" assert self.valid # Extract r and s from the DER formatted signature. Return false for # any DER encoding errors. if sig[1] + 2 != len(sig): return False if len(sig) < 4: return False if sig[0] != 0x30: return False if sig[2] != 0x02: return False rlen = sig[3] if len(sig) < 6 + rlen: return False if rlen < 1 or rlen > 33: return False if sig[4] >= 0x80: return False if rlen > 1 and (sig[4] == 0) and not (sig[5] & 0x80): return False r = int.from_bytes(sig[4 : 4 + rlen], "big") if sig[4 + rlen] != 0x02: return False slen = sig[5 + rlen] if slen < 1 or slen > 33: return False if len(sig) != 6 + rlen + slen: return False if sig[6 + rlen] >= 0x80: return False if slen > 1 and (sig[6 + rlen] == 0) and not (sig[7 + rlen] & 0x80): return False s = int.from_bytes(sig[6 + rlen : 6 + rlen + slen], "big") # Verify that r and s are within the group order if r < 1 or s < 1 or r >= SECP256K1_ORDER or s >= SECP256K1_ORDER: return False if low_s and s >= SECP256K1_ORDER_HALF: return False z = int.from_bytes(msg, "big") # Run verifier algorithm on r, s w = modinv(s, SECP256K1_ORDER) u1 = z * w % SECP256K1_ORDER u2 = r * w % SECP256K1_ORDER R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, u1), (self.p, u2)])) if R is None or (R[0] % SECP256K1_ORDER) != r: return False return True def generate_privkey(): """Generate a valid random 32-byte private key.""" return random.randrange(1, SECP256K1_ORDER).to_bytes(32, "big") class ECKey: """A secp256k1 private key""" def __init__(self): self.valid = False def set(self, secret, compressed): """Construct a private key object with given 32-byte secret and compressed flag.""" assert len(secret) == 32 secret = int.from_bytes(secret, "big") self.valid = secret > 0 and secret < SECP256K1_ORDER if self.valid: self.secret = secret self.compressed = compressed def generate(self, compressed=True): """Generate a random private key (compressed or uncompressed).""" self.set(generate_privkey(), compressed) def get_bytes(self): """Retrieve the 32-byte representation of this key.""" assert self.valid return self.secret.to_bytes(32, "big") @property def is_valid(self): return self.valid @property def is_compressed(self): return self.compressed def get_pubkey(self): """Compute an ECPubKey object for this secret key.""" assert self.valid ret = ECPubKey() p = SECP256K1.mul([(SECP256K1_G, self.secret)]) ret.p = p ret.valid = True ret.compressed = self.compressed return ret def sign_ecdsa(self, msg, nonce_function=None, extra_data=None, low_s=True): """Construct a DER-encoded ECDSA signature with this key. See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the ECDSA signer algorithm.""" assert self.valid z = int.from_bytes(msg, "big") if nonce_function is None: nonce_function = deterministic_k k = nonce_function(self.secret, z, extra_data=extra_data) R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)])) r = R[0] % SECP256K1_ORDER s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER if low_s and s > SECP256K1_ORDER_HALF: s = SECP256K1_ORDER - s # Represent in DER format. The byte representations of r and s have # length rounded up (255 bits becomes 32 bytes and 256 bits becomes 33 # bytes). rb = r.to_bytes((r.bit_length() + 8) // 8, "big") sb = s.to_bytes((s.bit_length() + 8) // 8, "big") return ( b"\x30" + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb ) def deterministic_k(secret, z, extra_data=None): # RFC6979, optimized for secp256k1 k = b"\x00" * 32 v = b"\x01" * 32 if z > SECP256K1_ORDER: z -= SECP256K1_ORDER z_bytes = z.to_bytes(32, "big") secret_bytes = secret.to_bytes(32, "big") if extra_data is not None: z_bytes += extra_data k = hmac.new(k, v + b"\x00" + secret_bytes + z_bytes, "sha256").digest() v = hmac.new(k, v, "sha256").digest() k = hmac.new(k, v + b"\x01" + secret_bytes + z_bytes, "sha256").digest() v = hmac.new(k, v, "sha256").digest() while True: v = hmac.new(k, v, "sha256").digest() candidate = int.from_bytes(v, "big") if candidate >= 1 and candidate < SECP256K1_ORDER: return candidate k = hmac.new(k, v + b"\x00", "sha256").digest() v = hmac.new(k, v, "sha256").digest() def compute_xonly_pubkey(key): """Compute an x-only (32 byte) public key from a (32 byte) private key. This also returns whether the resulting public key was negated. """ assert len(key) == 32 x = int.from_bytes(key, "big") if x == 0 or x >= SECP256K1_ORDER: return (None, None) P = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, x)])) return (P[0].to_bytes(32, "big"), not SECP256K1.has_even_y(P)) def tweak_add_privkey(key, tweak): """Tweak a private key (after negating it if needed).""" assert len(key) == 32 assert len(tweak) == 32 x = int.from_bytes(key, "big") if x == 0 or x >= SECP256K1_ORDER: return None if not SECP256K1.has_even_y(SECP256K1.mul([(SECP256K1_G, x)])): x = SECP256K1_ORDER - x t = int.from_bytes(tweak, "big") if t >= SECP256K1_ORDER: return None x = (x + t) % SECP256K1_ORDER if x == 0: return None return x.to_bytes(32, "big") def tweak_add_pubkey(key, tweak): """Tweak a public key and return whether the result had to be negated.""" assert len(key) == 32 assert len(tweak) == 32 x_coord = int.from_bytes(key, "big") if x_coord >= SECP256K1_FIELD_SIZE: return None P = SECP256K1.lift_x(x_coord) if P is None: return None t = int.from_bytes(tweak, "big") if t >= SECP256K1_ORDER: return None Q = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, t), (P, 1)])) if Q is None: return None return (Q[0].to_bytes(32, "big"), not SECP256K1.has_even_y(Q)) def verify_schnorr(key, sig, msg): """Verify a Schnorr signature (see BIP 340). - key is a 32-byte xonly pubkey (computed using compute_xonly_pubkey). - sig is a 64-byte Schnorr signature - msg is a 32-byte message """ assert len(key) == 32 assert len(msg) == 32 assert len(sig) == 64 x_coord = int.from_bytes(key, "big") if x_coord == 0 or x_coord >= SECP256K1_FIELD_SIZE: return False P = SECP256K1.lift_x(x_coord) if P is None: return False r = int.from_bytes(sig[0:32], "big") if r >= SECP256K1_FIELD_SIZE: return False s = int.from_bytes(sig[32:64], "big") if s >= SECP256K1_ORDER: return False e = ( int.from_bytes(TaggedHash("BIP0340/challenge", sig[0:32] + key + msg), "big") % SECP256K1_ORDER ) R = SECP256K1.mul([(SECP256K1_G, s), (P, SECP256K1_ORDER - e)]) if not SECP256K1.has_even_y(R): return False if ((r * R[2] * R[2]) % SECP256K1_FIELD_SIZE) != R[0]: return False return True def sign_schnorr(key, msg, aux=None, flip_p=False, flip_r=False): """Create a Schnorr signature (see BIP 340).""" assert len(key) == 32 assert len(msg) == 32 if aux is not None: assert len(aux) == 32 sec = int.from_bytes(key, "big") if sec == 0 or sec >= SECP256K1_ORDER: return None P = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, sec)])) if SECP256K1.has_even_y(P) == flip_p: sec = SECP256K1_ORDER - sec if aux is not None: t = (sec ^ int.from_bytes(TaggedHash("BIP0340/aux", aux), "big")).to_bytes( 32, "big" ) else: t = sec.to_bytes(32, "big") kp = ( int.from_bytes( TaggedHash("BIP0340/nonce", t + P[0].to_bytes(32, "big") + msg), "big" ) % SECP256K1_ORDER ) assert kp != 0 R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, kp)])) k = kp if SECP256K1.has_even_y(R) != flip_r else SECP256K1_ORDER - kp e = ( int.from_bytes( TaggedHash( "BIP0340/challenge", R[0].to_bytes(32, "big") + P[0].to_bytes(32, "big") + msg, ), "big", ) % SECP256K1_ORDER ) return R[0].to_bytes(32, "big") + ((k + e * sec) % SECP256K1_ORDER).to_bytes( 32, "big" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/util/py_ripemd160.py0000664000175000017500000001210514623570367024177 0ustar00singalasingala# Copyright (c) 2021 Pieter Wuille # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Pure Python RIPEMD160 implementation.""" # Message schedule indexes for the left path. ML = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13, ] # Message schedule indexes for the right path. MR = [ 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11, ] # Rotation counts for the left path. RL = [ 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6, ] # Rotation counts for the right path. RR = [ 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11, ] # K constants for the left path. KL = [0, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E] # K constants for the right path. KR = [0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0] def fi(x, y, z, i): """The f1, f2, f3, f4, and f5 functions from the specification.""" if i == 0: return x ^ y ^ z elif i == 1: return (x & y) | (~x & z) elif i == 2: return (x | ~y) ^ z elif i == 3: return (x & z) | (y & ~z) elif i == 4: return x ^ (y | ~z) else: assert False def rol(x, i): """Rotate the bottom 32 bits of x left by i bits.""" return ((x << i) | ((x & 0xFFFFFFFF) >> (32 - i))) & 0xFFFFFFFF def compress(h0, h1, h2, h3, h4, block): """Compress state (h0, h1, h2, h3, h4) with block.""" # Left path variables. al, bl, cl, dl, el = h0, h1, h2, h3, h4 # Right path variables. ar, br, cr, dr, er = h0, h1, h2, h3, h4 # Message variables. x = [int.from_bytes(block[4 * i : 4 * (i + 1)], "little") for i in range(16)] # Iterate over the 80 rounds of the compression. for j in range(80): rnd = j >> 4 # Perform left side of the transformation. al = rol(al + fi(bl, cl, dl, rnd) + x[ML[j]] + KL[rnd], RL[j]) + el al, bl, cl, dl, el = el, al, bl, rol(cl, 10), dl # Perform right side of the transformation. ar = rol(ar + fi(br, cr, dr, 4 - rnd) + x[MR[j]] + KR[rnd], RR[j]) + er ar, br, cr, dr, er = er, ar, br, rol(cr, 10), dr # Compose old state, left transform, and right transform into new state. return h1 + cl + dr, h2 + dl + er, h3 + el + ar, h4 + al + br, h0 + bl + cr def ripemd160(data): """Compute the RIPEMD-160 hash of data.""" # Initialize state. state = (0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0) # Process full 64-byte blocks in the input. for b in range(len(data) >> 6): state = compress(*state, data[64 * b : 64 * (b + 1)]) # Construct final blocks (with padding and size). pad = b"\x80" + b"\x00" * ((119 - len(data)) & 63) fin = data[len(data) & ~63 :] + pad + (8 * len(data)).to_bytes(8, "little") # Process final blocks. for b in range(len(fin) >> 6): state = compress(*state, fin[64 * b : 64 * (b + 1)]) # Produce output. return b"".join((h & 0xFFFFFFFF).to_bytes(4, "little") for h in state) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/util/py_secp256k1.py0000664000175000017500000003137314623570367024123 0ustar00singalasingala""" This is a fallback option if the library can't do ctypes bindings to secp256k1 library. Mimics the micropython bindings and internal representation of data structs in secp256k1. """ from . import key as _key # Flags to pass to context_create. CONTEXT_VERIFY = 0b0100000001 CONTEXT_SIGN = 0b1000000001 CONTEXT_NONE = 0b0000000001 # Flags to pass to ec_pubkey_serialize EC_COMPRESSED = 0b0100000010 EC_UNCOMPRESSED = 0b0000000010 def context_randomize(seed, context=None): pass def _reverse64(b): """Converts (a,b) from big to little endian to be consistent with secp256k1""" x = b[:32] y = b[32:] return x[::-1] + y[::-1] def _pubkey_serialize(pub): """Returns pubkey representation like secp library""" b = pub.get_bytes()[1:] return _reverse64(b) def _pubkey_parse(b): """Returns pubkey class instance""" pub = _key.ECPubKey() pub.set(b"\x04" + _reverse64(b)) return pub def ec_pubkey_create(secret, context=None): if len(secret) != 32: raise ValueError("Private key should be 32 bytes long") pk = _key.ECKey() pk.set(secret, compressed=False) if not pk.is_valid: raise ValueError("Invalid private key") return _pubkey_serialize(pk.get_pubkey()) def ec_pubkey_parse(sec, context=None): if len(sec) != 33 and len(sec) != 65: raise ValueError("Serialized pubkey should be 33 or 65 bytes long") if len(sec) == 33: if sec[0] != 0x02 and sec[0] != 0x03: raise ValueError("Compressed pubkey should start with 0x02 or 0x03") else: if sec[0] != 0x04: raise ValueError("Uncompressed pubkey should start with 0x04") pub = _key.ECPubKey() pub.set(sec) pub.compressed = False if not pub.is_valid: raise ValueError("Failed parsing public key") return _pubkey_serialize(pub) def ec_pubkey_serialize(pubkey, flag=EC_COMPRESSED, context=None): if len(pubkey) != 64: raise ValueError("Pubkey should be 64 bytes long") if flag not in [EC_COMPRESSED, EC_UNCOMPRESSED]: raise ValueError("Invalid flag") pub = _pubkey_parse(pubkey) if not pub.is_valid: raise ValueError("Failed to serialize pubkey") if flag == EC_COMPRESSED: pub.compressed = True return pub.get_bytes() def ecdsa_signature_parse_compact(compact_sig, context=None): if len(compact_sig) != 64: raise ValueError("Compact signature should be 64 bytes long") sig = _reverse64(compact_sig) return sig def ecdsa_signature_parse_der(der, context=None): if der[1] + 2 != len(der): raise ValueError("Failed parsing compact signature") if len(der) < 4: raise ValueError("Failed parsing compact signature") if der[0] != 0x30: raise ValueError("Failed parsing compact signature") if der[2] != 0x02: raise ValueError("Failed parsing compact signature") rlen = der[3] if len(der) < 6 + rlen: raise ValueError("Failed parsing compact signature") if rlen < 1 or rlen > 33: raise ValueError("Failed parsing compact signature") if der[4] >= 0x80: raise ValueError("Failed parsing compact signature") if rlen > 1 and (der[4] == 0) and not (der[5] & 0x80): raise ValueError("Failed parsing compact signature") r = int.from_bytes(der[4 : 4 + rlen], "big") if der[4 + rlen] != 0x02: raise ValueError("Failed parsing compact signature") slen = der[5 + rlen] if slen < 1 or slen > 33: raise ValueError("Failed parsing compact signature") if len(der) != 6 + rlen + slen: raise ValueError("Failed parsing compact signature") if der[6 + rlen] >= 0x80: raise ValueError("Failed parsing compact signature") if slen > 1 and (der[6 + rlen] == 0) and not (der[7 + rlen] & 0x80): raise ValueError("Failed parsing compact signature") s = int.from_bytes(der[6 + rlen : 6 + rlen + slen], "big") # Verify that r and s are within the group order if r < 1 or s < 1 or r >= _key.SECP256K1_ORDER or s >= _key.SECP256K1_ORDER: raise ValueError("Failed parsing compact signature") if s >= _key.SECP256K1_ORDER_HALF: raise ValueError("Failed parsing compact signature") return r.to_bytes(32, "little") + s.to_bytes(32, "little") def ecdsa_signature_serialize_der(sig, context=None): if len(sig) != 64: raise ValueError("Signature should be 64 bytes long") r = int.from_bytes(sig[:32], "little") s = int.from_bytes(sig[32:], "little") rb = r.to_bytes((r.bit_length() + 8) // 8, "big") sb = s.to_bytes((s.bit_length() + 8) // 8, "big") return ( b"\x30" + bytes([4 + len(rb) + len(sb), 2, len(rb)]) + rb + bytes([2, len(sb)]) + sb ) def ecdsa_signature_serialize_compact(sig, context=None): if len(sig) != 64: raise ValueError("Signature should be 64 bytes long") return _reverse64(sig) def ecdsa_signature_normalize(sig, context=None): if len(sig) != 64: raise ValueError("Signature should be 64 bytes long") r = int.from_bytes(sig[:32], "little") s = int.from_bytes(sig[32:], "little") if s >= _key.SECP256K1_ORDER_HALF: s = _key.SECP256K1_ORDER - s return r.to_bytes(32, "little") + s.to_bytes(32, "little") def ecdsa_verify(sig, msg, pub, context=None): if len(sig) != 64: raise ValueError("Signature should be 64 bytes long") if len(msg) != 32: raise ValueError("Message should be 32 bytes long") if len(pub) != 64: raise ValueError("Public key should be 64 bytes long") pubkey = _pubkey_parse(pub) return pubkey.verify_ecdsa(ecdsa_signature_serialize_der(sig), msg) def ecdsa_sign(msg, secret, nonce_function=None, extra_data=None, context=None): if len(msg) != 32: raise ValueError("Message should be 32 bytes long") if len(secret) != 32: raise ValueError("Secret key should be 32 bytes long") pk = _key.ECKey() pk.set(secret, False) sig = pk.sign_ecdsa(msg, nonce_function, extra_data) return ecdsa_signature_parse_der(sig) def ec_seckey_verify(secret, context=None): if len(secret) != 32: raise ValueError("Secret should be 32 bytes long") pk = _key.ECKey() pk.set(secret, compressed=False) return pk.is_valid def ec_privkey_negate(secret, context=None): # negate in place if len(secret) != 32: raise ValueError("Secret should be 32 bytes long") s = int.from_bytes(secret, "big") s2 = _key.SECP256K1_ORDER - s return s2.to_bytes(32, "big") def ec_pubkey_negate(pubkey, context=None): if len(pubkey) != 64: raise ValueError("Pubkey should be a 64-byte structure") sec = ec_pubkey_serialize(pubkey) return ec_pubkey_parse(bytes([0x05 - sec[0]]) + sec[1:]) def ec_privkey_tweak_add(secret, tweak, context=None): res = ec_privkey_add(secret, tweak) for i in range(len(secret)): secret[i] = res[i] def ec_pubkey_tweak_add(pub, tweak, context=None): res = ec_pubkey_add(pub, tweak) for i in range(len(pub)): pub[i] = res[i] def ec_privkey_add(secret, tweak, context=None): if len(secret) != 32 or len(tweak) != 32: raise ValueError("Secret and tweak should both be 32 bytes long") s = int.from_bytes(secret, "big") t = int.from_bytes(tweak, "big") r = (s + t) % _key.SECP256K1_ORDER return r.to_bytes(32, "big") def ec_pubkey_add(pub, tweak, context=None): if len(pub) != 64: raise ValueError("Public key should be 64 bytes long") if len(tweak) != 32: raise ValueError("Tweak should be 32 bytes long") pubkey = _pubkey_parse(pub) pubkey.compressed = True t = int.from_bytes(tweak, "big") Q = _key.SECP256K1.affine( _key.SECP256K1.mul([(_key.SECP256K1_G, t), (pubkey.p, 1)]) ) if Q is None: return None return Q[0].to_bytes(32, "little") + Q[1].to_bytes(32, "little") # def ec_privkey_tweak_mul(secret, tweak, context=None): # if len(secret)!=32 or len(tweak)!=32: # raise ValueError("Secret and tweak should both be 32 bytes long") # s = int.from_bytes(secret, 'big') # t = int.from_bytes(tweak, 'big') # if t > _key.SECP256K1_ORDER or s > _key.SECP256K1_ORDER: # raise ValueError("Failed to tweak the secret") # r = pow(s, t, _key.SECP256K1_ORDER) # res = r.to_bytes(32, 'big') # for i in range(len(secret)): # secret[i] = res[i] # def ec_pubkey_tweak_mul(pub, tweak, context=None): # if len(pub)!=64: # raise ValueError("Public key should be 64 bytes long") # if len(tweak)!=32: # raise ValueError("Tweak should be 32 bytes long") # if _secp.secp256k1_ec_pubkey_tweak_mul(context, pub, tweak) == 0: # raise ValueError("Failed to tweak the public key") # def ec_pubkey_combine(*args, context=None): # pub = bytes(64) # pubkeys = (c_char_p * len(args))(*args) # r = _secp.secp256k1_ec_pubkey_combine(context, pub, pubkeys, len(args)) # if r == 0: # raise ValueError("Failed to negate pubkey") # return pub # schnorrsig def xonly_pubkey_from_pubkey(pubkey, context=None): if len(pubkey) != 64: raise ValueError("Pubkey should be 64 bytes long") sec = ec_pubkey_serialize(pubkey) parity = sec[0] == 0x03 pub = ec_pubkey_parse(b"\x02" + sec[1:33]) return pub, parity def schnorrsig_verify(sig, msg, pubkey, context=None): assert len(sig) == 64 assert len(msg) == 32 assert len(pubkey) == 64 sec = ec_pubkey_serialize(pubkey) return _key.verify_schnorr(sec[1:33], sig, msg) def keypair_create(secret, context=None): pub = ec_pubkey_create(secret) pub2, parity = xonly_pubkey_from_pubkey(pub) keypair = secret + pub return keypair def schnorrsig_sign(msg, keypair, nonce_function=None, extra_data=None, context=None): assert len(msg) == 32 if len(keypair) == 32: keypair = keypair_create(keypair, context=context) assert len(keypair) == 96 return _key.sign_schnorr(keypair[:32], msg, extra_data) # recoverable def ecdsa_sign_recoverable(msg, secret, context=None): sig = ecdsa_sign(msg, secret) pub = ec_pubkey_create(secret) # Search for correct index. Not efficient but I am lazy. # For efficiency use c-bindings to libsecp256k1 for i in range(4): if ecdsa_recover(sig + bytes([i]), msg) == pub: return sig + bytes([i]) raise ValueError("Failed to sign") def ecdsa_recoverable_signature_serialize_compact(sig, context=None): if len(sig) != 65: raise ValueError("Recoverable signature should be 65 bytes long") compact = ecdsa_signature_serialize_compact(sig[:64]) return compact, sig[64] def ecdsa_recoverable_signature_parse_compact(compact_sig, recid, context=None): if len(compact_sig) != 64: raise ValueError("Signature should be 64 bytes long") # TODO: also check r value so recid > 2 makes sense if recid < 0 or recid > 4: raise ValueError("Failed parsing compact signature") return ecdsa_signature_parse_compact(compact_sig) + bytes([recid]) def ecdsa_recoverable_signature_convert(sigin, context=None): if len(sigin) != 65: raise ValueError("Recoverable signature should be 65 bytes long") return sigin[:64] def ecdsa_recover(sig, msghash, context=None): if len(sig) != 65: raise ValueError("Recoverable signature should be 65 bytes long") if len(msghash) != 32: raise ValueError("Message should be 32 bytes long") idx = sig[-1] r = int.from_bytes(sig[:32], "little") s = int.from_bytes(sig[32:64], "little") z = int.from_bytes(msghash, "big") # r = Rx mod N, so R can be 02x, 03x, 02(N+x), 03(N+x) # two latter cases only if N+x < P r_candidates = [ b"\x02" + r.to_bytes(32, "big"), b"\x03" + r.to_bytes(32, "big"), ] if r + _key.SECP256K1_ORDER < _key.SECP256K1_FIELD_SIZE: r2 = r + _key.SECP256K1_ORDER r_candidates = r_candidates + [ b"\x02" + r2.to_bytes(32, "big"), b"\x03" + r2.to_bytes(32, "big"), ] if idx >= len(r_candidates): raise ValueError("Failed to recover public key") R = _key.ECPubKey() R.set(r_candidates[idx]) # s = (z + d * r)/k # (R*s/r - z/r*G) = P rinv = _key.modinv(r, _key.SECP256K1_ORDER) u1 = (s * rinv) % _key.SECP256K1_ORDER u2 = (z * rinv) % _key.SECP256K1_ORDER P1 = _key.SECP256K1.mul([(R.p, u1)]) P2 = _key.SECP256K1.negate(_key.SECP256K1.mul([(_key.SECP256K1_G, u2)])) P = _key.SECP256K1.affine(_key.SECP256K1.add(P1, P2)) result = P[0].to_bytes(32, "little") + P[1].to_bytes(32, "little") # verify signature at the end pubkey = _pubkey_parse(result) if not pubkey.is_valid: raise ValueError("Failed to recover public key") if not ecdsa_verify(sig[:64], msghash, result): raise ValueError("Failed to recover public key") return result ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/embit/util/secp256k1.py0000664000175000017500000000044014623570367023402 0ustar00singalasingalatry: # if it's micropython from micropython import const from secp256k1 import * except: # we are in python try: # try ctypes bindings from .ctypes_secp256k1 import * except: # fallback to python version from .py_secp256k1 import * ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/errors.py0000664000175000017500000001576114506542032021216 0ustar00singalasingala""" Original version: https://github.com/bitcoin-core/HWI Distributed under the MIT License. Errors and Error Codes ********************** HWI has several possible Exceptions with corresponding error codes. :class:`~hwilib.hwwclient.HardwareWalletClient` functions and :mod:`~hwilib.commands` functions will generally raise an exception that is a subclass of :class:`HWWError`. The HWI command line tool will convert these exceptions into a dictionary containing the error message and error code. These look like ``{"error": "", "code": }``. """ from typing import Any, Dict, Iterator, Optional from contextlib import contextmanager # Error codes NO_DEVICE_TYPE = -1 #: Device type was not specified MISSING_ARGUMENTS = -2 #: Arguments are missing DEVICE_CONN_ERROR = -3 #: Error connecting to the device UNKNWON_DEVICE_TYPE = -4 #: Device type is unknown INVALID_TX = -5 #: Transaction is invalid NO_PASSWORD = -6 #: No password provided, but one is needed BAD_ARGUMENT = -7 #: Bad, malformed, or conflicting argument was provided NOT_IMPLEMENTED = -8 #: Function is not implemented UNAVAILABLE_ACTION = -9 #: Function is not available for this device DEVICE_ALREADY_INIT = -10 #: Device is already initialized DEVICE_ALREADY_UNLOCKED = -11 #: Device is already unlocked DEVICE_NOT_READY = -12 #: Device is not ready UNKNOWN_ERROR = -13 #: An unknown error occurred ACTION_CANCELED = -14 #: Action was canceled by the user DEVICE_BUSY = -15 #: Device is busy NEED_TO_BE_ROOT = -16 #: User needs to be root to perform action HELP_TEXT = -17 #: Help text was requested by the user DEVICE_NOT_INITIALIZED = -18 #: Device is not initialized # Exceptions class HWWError(Exception): """ Generic exception type produced by HWI Subclassed by specific Errors to have Exceptions that have specific error codes. Contains a message and error code. """ def __init__(self, msg: str, code: int) -> None: """ Create an exception with the message and error code :param msg: The error message :param code: The error code """ Exception.__init__(self) self.code = code self.msg = msg def get_code(self) -> int: """ Get the error code for this Error :return: The error code """ return self.code def get_msg(self) -> str: """ Get the error message for this Error :return: The error message """ return self.msg def __str__(self) -> str: return self.msg class NoPasswordError(HWWError): """ :class:`HWWError` for :data:`NO_PASSWORD` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, NO_PASSWORD) class UnavailableActionError(HWWError): """ :class:`HWWError` for :data:`UNAVAILABLE_ACTION` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, UNAVAILABLE_ACTION) class DeviceAlreadyInitError(HWWError): """ :class:`HWWError` for :data:`DEVICE_ALREADY_INIT` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, DEVICE_ALREADY_INIT) class DeviceNotReadyError(HWWError): """ :class:`HWWError` for :data:`DEVICE_NOT_READY` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, DEVICE_NOT_READY) class DeviceAlreadyUnlockedError(HWWError): """ :class:`HWWError` for :data:`DEVICE_ALREADY_UNLOCKED` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, DEVICE_ALREADY_UNLOCKED) class UnknownDeviceError(HWWError): """ :class:`HWWError` for :data:`DEVICE_TYPE` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, UNKNWON_DEVICE_TYPE) class NotImplementedError(HWWError): """ :class:`HWWError` for :data:`NOT_IMPLEMENTED` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, NOT_IMPLEMENTED) class PSBTSerializationError(HWWError): """ :class:`HWWError` for :data:`INVALID_TX` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, INVALID_TX) class BadArgumentError(HWWError): """ :class:`HWWError` for :data:`BAD_ARGUMENT` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, BAD_ARGUMENT) class DeviceFailureError(HWWError): """ :class:`HWWError` for :data:`UNKNOWN_ERROR` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, UNKNOWN_ERROR) class ActionCanceledError(HWWError): """ :class:`HWWError` for :data:`ACTION_CANCELED` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, ACTION_CANCELED) class DeviceConnectionError(HWWError): """ :class:`HWWError` for :data:`DEVICE_CONN_ERROR` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, DEVICE_CONN_ERROR) class DeviceBusyError(HWWError): """ :class:`HWWError` for :data:`DEVICE_BUSY` """ def __init__(self, msg: str): """ :param msg: The error message """ HWWError.__init__(self, msg, DEVICE_BUSY) class NeedsRootError(HWWError): def __init__(self, msg: str): HWWError.__init__(self, msg, NEED_TO_BE_ROOT) @contextmanager def handle_errors( msg: Optional[str] = None, result: Optional[Dict[str, Any]] = None, code: int = UNKNOWN_ERROR, debug: bool = False, ) -> Iterator[None]: """ Context manager to catch all Exceptions and HWWErrors to return them as dictionaries containing the error message and code. :param msg: Error message prefix. Attached to the beginning of each error message :param result: The dictionary to put the resulting error in :param code: The default error code to use for Exceptions :param debug: Whether to also print out the traceback for debugging purposes """ if result is None: result = {} if msg is None: msg = "" else: msg = msg + " " try: yield except HWWError as e: result['error'] = msg + e.get_msg() result['code'] = e.get_code() except Exception as e: result['error'] = msg + str(e) result['code'] = code if debug: import traceback traceback.print_exc() return result common_err_msgs = { "enumerate": "Could not open client or get fingerprint information:" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741351045.8513703 ledger_bitcoin-0.4.0/ledger_bitcoin/exception/0000775000175000017500000000000014762564206021327 5ustar00singalasingala././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/exception/__init__.py0000664000175000017500000000144614506542032023432 0ustar00singalasingalafrom .device_exception import DeviceException from .errors import (UnknownDeviceError, DenyError, IncorrectDataError, NotSupportedError, WrongP1P2Error, WrongDataLengthError, InsNotSupportedError, ClaNotSupportedError, WrongResponseLengthError, BadStateError, SignatureFailError) __all__ = [ "DeviceException", "DenyError", "IncorrectDataError", "NotSupportedError", "UnknownDeviceError", "WrongP1P2Error", "WrongDataLengthError", "InsNotSupportedError", "ClaNotSupportedError", "WrongResponseLengthError", "BadStateError", "SignatureFailError" ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/ledger_bitcoin/exception/device_exception.py0000664000175000017500000000256314623570367025225 0ustar00singalasingalaimport enum from typing import Dict, Any, Union from .errors import * class DeviceException(Exception): # pylint: disable=too-few-public-methods exc: Dict[int, Any] = { 0x5515: SecurityStatusNotSatisfiedError, # returned by sdk in recent versions 0x6985: DenyError, 0x6982: SecurityStatusNotSatisfiedError, # used in older app versions 0x6A80: IncorrectDataError, 0x6A82: NotSupportedError, 0x6A86: WrongP1P2Error, 0x6A87: WrongDataLengthError, 0x6B00: SwapError, 0x6D00: InsNotSupportedError, 0x6E00: ClaNotSupportedError, 0xB000: WrongResponseLengthError, 0xB007: BadStateError, 0xB008: SignatureFailError, 0xE000: InterruptedExecution, # not an error } def __new__(cls, error_code: int, ins: Union[int, enum.IntEnum, None] = None, message: str = "" ) -> Any: error_message: str = (f"Error in {ins!r} command" if ins else "Error in command") if error_code in DeviceException.exc: return DeviceException.exc[error_code](hex(error_code), error_message, message) return UnknownDeviceError(hex(error_code), error_message, message) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710772394.0 ledger_bitcoin-0.4.0/ledger_bitcoin/exception/errors.py0000664000175000017500000000126314576050252023211 0ustar00singalasingalaclass UnknownDeviceError(Exception): pass class SecurityStatusNotSatisfiedError(Exception): pass class DenyError(Exception): pass class IncorrectDataError(Exception): pass class NotSupportedError(Exception): pass class WrongP1P2Error(Exception): pass class WrongDataLengthError(Exception): pass class SwapError(Exception): pass class InsNotSupportedError(Exception): pass class ClaNotSupportedError(Exception): pass class WrongResponseLengthError(Exception): pass class BadStateError(Exception): pass class SignatureFailError(Exception): pass # Not really an error class InterruptedExecution(Exception): pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/key.py0000664000175000017500000003717514506542032020475 0ustar00singalasingala# Original version: https://github.com/bitcoin-core/HWI/blob/3fe369d0379212fae1c72729a179d133b0adc872/hwilib/key.py #!/usr/bin/env python3 # Copyright (c) 2020 The HWI developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Key Classes and Utilities ************************* Classes and utilities for working with extended public keys, key origins, and other key related things. """ from . import _base58 as base58 from .common import ( AddressType, Chain, hash256, hash160, ) from .errors import BadArgumentError import binascii import hmac import hashlib import struct from typing import ( Dict, List, Optional, Sequence, Tuple, ) HARDENED_FLAG = 1 << 31 p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) Point = Optional[Tuple[int, int]] def H_(x: int) -> int: """ Shortcut function that "hardens" a number in a BIP44 path. """ return x | HARDENED_FLAG def is_hardened(i: int) -> bool: """ Returns whether an index is hardened """ return i & HARDENED_FLAG != 0 def point_add(p1: Point, p2: Point) -> Point: if (p1 is None): return p2 if (p2 is None): return p1 if (p1[0] == p2[0] and p1[1] != p2[1]): return None if (p1 == p2): lam = (3 * p1[0] * p1[0] * pow(2 * p1[1], p - 2, p)) % p else: lam = ((p2[1] - p1[1]) * pow(p2[0] - p1[0], p - 2, p)) % p x3 = (lam * lam - p1[0] - p2[0]) % p return (x3, (lam * (p1[0] - x3) - p1[1]) % p) def point_mul(p: Point, n: int) -> Point: r = None for i in range(256): if ((n >> i) & 1): r = point_add(r, p) p = point_add(p, p) return r def deserialize_point(b: bytes) -> Point: x = int.from_bytes(b[1:], byteorder="big") y = pow((x * x * x + 7) % p, (p + 1) // 4, p) if (y & 1 != b[0] & 1): y = p - y return (x, y) def bytes_to_point(point_bytes: bytes) -> Point: header = point_bytes[0] if header == 4: x = point_bytes = point_bytes[1:33] y = point_bytes = point_bytes[33:65] return (int(binascii.hexlify(x), 16), int(binascii.hexlify(y), 16)) return deserialize_point(point_bytes) def point_to_bytes(p: Point) -> bytes: if p is None: raise ValueError("Cannot convert None to bytes") return (b'\x03' if p[1] & 1 else b'\x02') + p[0].to_bytes(32, byteorder="big") def int_from_bytes(b: bytes) -> int: return int(binascii.hexlify(b), 16) def lift_x(x: int) -> 'Point': c = (pow(x, 3, p) + 7) % p y = pow(c, (p + 1) // 4, p) assert(c == y * y % p) return (x, p - y if y & 1 else y) def tagged_hash(tag: str, data: bytes) -> bytes: hashtag = hashlib.sha256(tag.encode()).digest() return hashlib.sha256(hashtag + hashtag + data).digest() def taproot_tweak_pubkey(pubkey: bytes, h: bytes) -> Tuple[int, bytes]: t = int_from_bytes(tagged_hash("TapTweak", pubkey + h)) if t >= p: raise ValueError Q = point_add(lift_x(int_from_bytes(pubkey)), point_mul(G, t)) return 0 if Q[1] & 1 == 0 else 1, Q[0].to_bytes(32, byteorder="big") def get_taproot_output_key(derived_key: bytes) -> bytes: assert(len(derived_key) == 33) p = derived_key[1:] _, key = taproot_tweak_pubkey(p, b'') return key # An extended public key (xpub) or private key (xprv). Just a data container for now. # Only handles deserialization of extended keys into component data to be handled by something else class ExtendedKey(object): """ A BIP 32 extended public key. """ MAINNET_PUBLIC = b'\x04\x88\xB2\x1E' MAINNET_PRIVATE = b'\x04\x88\xAD\xE4' TESTNET_PUBLIC = b'\x04\x35\x87\xCF' TESTNET_PRIVATE = b'\x04\x35\x83\x94' def __init__(self, version: bytes, depth: int, parent_fingerprint: bytes, child_num: int, chaincode: bytes, privkey: Optional[bytes], pubkey: bytes) -> None: """ :param version: The version bytes for this xpub :param depth: The depth of this xpub as defined in BIP 32 :param parent_fingerprint: The 4 byte fingerprint of the parent xpub as defined in BIP 32 :param child_num: The number of this xpub as defined in BIP 32 :param chaincode: The chaincode of this xpub as defined in BIP 32 :param privkey: The private key for this xpub if available :param pubkey: The public key for this xpub """ self.version: bytes = version self.is_testnet: bool = version == ExtendedKey.TESTNET_PUBLIC or version == ExtendedKey.TESTNET_PRIVATE self.is_private: bool = version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE self.depth: int = depth self.parent_fingerprint: bytes = parent_fingerprint self.child_num: int = child_num self.chaincode: bytes = chaincode self.pubkey: bytes = pubkey self.privkey: Optional[bytes] = privkey @classmethod def deserialize(cls, xpub: str) -> 'ExtendedKey': """ Create an :class:`~ExtendedKey` from a Base58 check encoded xpub :param xpub: The Base58 check encoded xpub """ data = base58.decode(xpub)[:-4] # Decoded xpub without checksum return cls.from_bytes(data) @classmethod def from_bytes(cls, data: bytes) -> 'ExtendedKey': """ Create an :class:`~ExtendedKey` from a serialized xpub :param xpub: The serialized xpub """ version = data[0:4] if version not in [ExtendedKey.MAINNET_PRIVATE, ExtendedKey.MAINNET_PUBLIC, ExtendedKey.TESTNET_PRIVATE, ExtendedKey.TESTNET_PUBLIC]: raise BadArgumentError(f"Extended key magic of {version.hex()} is invalid") is_private = version == ExtendedKey.MAINNET_PRIVATE or version == ExtendedKey.TESTNET_PRIVATE depth = data[4] parent_fingerprint = data[5:9] child_num = struct.unpack('>I', data[9:13])[0] chaincode = data[13:45] if is_private: privkey = data[46:] pubkey = point_to_bytes(point_mul(G, int.from_bytes(privkey, byteorder="big"))) return cls(version, depth, parent_fingerprint, child_num, chaincode, privkey, pubkey) else: pubkey = data[45:78] return cls(version, depth, parent_fingerprint, child_num, chaincode, None, pubkey) def serialize(self) -> bytes: """ Serialize the ExtendedKey with the serialization format described in BIP 32. Does not create an xpub string, but the bytes serialized here can be Base58 check encoded into one. :return: BIP 32 serialized extended key """ r = self.version + struct.pack('B', self.depth) + self.parent_fingerprint + struct.pack('>I', self.child_num) + self.chaincode if self.is_private: if self.privkey is None: raise ValueError("Somehow we are private but don't have a privkey") r += b"\x00" + self.privkey else: r += self.pubkey return r def to_string(self) -> str: """ Serialize the ExtendedKey as a Base58 check encoded xpub string :return: Base58 check encoded xpub """ data = self.serialize() checksum = hash256(data)[0:4] return base58.encode(data + checksum) def get_printable_dict(self) -> Dict[str, object]: """ Get the attributes of this ExtendedKey as a dictionary that can be printed :return: Dictionary containing ExtendedKey information that can be printed """ d: Dict[str, object] = {} d['testnet'] = self.is_testnet d['private'] = self.is_private d['depth'] = self.depth d['parent_fingerprint'] = binascii.hexlify(self.parent_fingerprint).decode() d['child_num'] = self.child_num d['chaincode'] = binascii.hexlify(self.chaincode).decode() if self.is_private and isinstance(self.privkey, bytes): d['privkey'] = binascii.hexlify(self.privkey).decode() d['pubkey'] = binascii.hexlify(self.pubkey).decode() return d def derive_priv(self, i: int) -> 'ExtendedKey': """ Derive the private key at the given child index. :param i: The child index of the pubkey to derive """ if not self.privkey: raise ValueError("Can only derive a private key from an extended private key") # Data to HMAC if is_hardened(i): data = b'\0' + self.privkey + struct.pack(">L", i) else: data = self.pubkey + struct.pack(">L", i) # Get HMAC of data Ihmac = hmac.new(self.chaincode, data, hashlib.sha512).digest() Il = Ihmac[:32] Ir = Ihmac[32:] # Construct new key material from Il and current private key Il_int = int.from_bytes(Il, byteorder="big") if Il_int > n: return None privkey_int = int.from_bytes(self.privkey, byteorder="big") k_int = (Il_int + privkey_int) % n if (k_int == 0): return None privkey = k_int.to_bytes(32, byteorder="big") pubkey = point_to_bytes(point_mul(G, k_int)) chaincode = Ir fingerprint = hash160(self.pubkey)[0:4] return ExtendedKey(ExtendedKey.TESTNET_PRIVATE if self.is_testnet else ExtendedKey.MAINNET_PRIVATE, self.depth + 1, fingerprint, i, chaincode, privkey, pubkey) def derive_pub(self, i: int) -> 'ExtendedKey': """ Derive the public key at the given child index. :param i: The child index of the pubkey to derive """ if is_hardened(i): raise ValueError("Index cannot be larger than 2^31") # Data to HMAC. Same as CKDpriv() for public child key. data = self.pubkey + struct.pack(">L", i) # Get HMAC of data Ihmac = hmac.new(self.chaincode, data, hashlib.sha512).digest() Il = Ihmac[:32] Ir = Ihmac[32:] # Construct curve point Il*G+K Il_int = int(binascii.hexlify(Il), 16) child_pubkey = point_add(point_mul(G, Il_int), bytes_to_point(self.pubkey)) # Construct and return a new BIP32Key pubkey = point_to_bytes(child_pubkey) chaincode = Ir fingerprint = hash160(self.pubkey)[0:4] return ExtendedKey(ExtendedKey.TESTNET_PUBLIC if self.is_testnet else ExtendedKey.MAINNET_PUBLIC, self.depth + 1, fingerprint, i, chaincode, None, pubkey) def derive_priv_path(self, path: Sequence[int]) -> 'ExtendedKey': """ Derive the private key at the given path :param path: Sequence of integers for the path of the key to derive """ key = self for i in path: key = key.derive_priv(i) return key def derive_pub_path(self, path: Sequence[int]) -> 'ExtendedKey': """ Derive the public key at the given path :param path: Sequence of integers for the path of the pubkey to derive """ key = self for i in path: key = key.derive_pub(i) return key def neutered(self) -> 'ExtendedKey': """ Returns the public key corresponding to this private key. """ if not self.is_private: raise ValueError("It is already a public key") return ExtendedKey(ExtendedKey.TESTNET_PUBLIC if self.is_testnet else ExtendedKey.MAINNET_PUBLIC, self.depth, self.parent_fingerprint, self.child_num, self.chaincode, None, self.pubkey) class KeyOriginInfo(object): """ Object representing the origin of a key. """ def __init__(self, fingerprint: bytes, path: Sequence[int]) -> None: """ :param fingerprint: The 4 byte BIP 32 fingerprint of a parent key from which this key is derived from :param path: The derivation path to reach this key from the key at ``fingerprint`` """ self.fingerprint: bytes = fingerprint self.path: Sequence[int] = path @classmethod def deserialize(cls, s: bytes) -> 'KeyOriginInfo': """ Deserialize a serialized KeyOriginInfo. They will be serialized in the same way that PSBTs serialize derivation paths """ fingerprint = s[0:4] s = s[4:] path = list(struct.unpack("<" + "I" * (len(s) // 4), s)) return cls(fingerprint, path) def serialize(self) -> bytes: """ Serializes the KeyOriginInfo in the same way that derivation paths are stored in PSBTs """ r = self.fingerprint r += struct.pack("<" + "I" * len(self.path), *self.path) return r def _path_string(self) -> str: s = "" for i in self.path: hardened = is_hardened(i) i &= ~HARDENED_FLAG s += "/" + str(i) if hardened: s += "h" return s def to_string(self) -> str: """ Return the KeyOriginInfo as a string in the form ///... This is the same way that KeyOriginInfo is shown in descriptors """ s = binascii.hexlify(self.fingerprint).decode() s += self._path_string() return s @classmethod def from_string(cls, s: str) -> 'KeyOriginInfo': """ Create a KeyOriginInfo from the string :param s: The string to parse """ s = s.lower() entries = s.split("/") fingerprint = binascii.unhexlify(s[0:8]) path: Sequence[int] = [] if len(entries) > 1: path = parse_path(s[9:]) return cls(fingerprint, path) def get_derivation_path(self) -> str: """ Return the string for just the path """ return "m" + self._path_string() def get_full_int_list(self) -> List[int]: """ Return a list of ints representing this KeyOriginInfo. The first int is the fingerprint, followed by the path """ xfp = [struct.unpack(" List[int]: """ Convert BIP32 path string to list of uint32 integers with hardened flags. Several conventions are supported to set the hardened flag: -1, 1', 1h e.g.: "0/1h/1" -> [0, 0x80000001, 1] :param nstr: path string :return: list of integers """ if not nstr: return [] n = nstr.split("/") # m/a/b/c => a/b/c if n[0] == "m": n = n[1:] def str_to_harden(x: str) -> int: if x.startswith("-"): return H_(abs(int(x))) elif x.endswith(("h", "'")): return H_(int(x[:-1])) else: return int(x) try: return [str_to_harden(x) for x in n] except Exception: raise ValueError("Invalid BIP32 path", nstr) def get_bip44_purpose(addrtype: AddressType) -> int: """ Determine the BIP 44 purpose based on the given :class:`~hwilib.common.AddressType`. :param addrtype: The address type """ if addrtype == AddressType.LEGACY: return 44 elif addrtype == AddressType.SH_WIT: return 49 elif addrtype == AddressType.WIT: return 84 elif addrtype == AddressType.TAP: return 86 else: raise ValueError("Unknown address type") def get_bip44_chain(chain: Chain) -> int: """ Determine the BIP 44 coin type based on the Bitcoin chain type. For the Bitcoin mainnet chain, this returns 0. For the other chains, this returns 1. :param chain: The chain """ if chain == Chain.MAIN: return 0 else: return 1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/ledger_bitcoin/merkle.py0000664000175000017500000002027714506542032021157 0ustar00singalasingalafrom typing import List, Iterable, Mapping from .common import write_varint, sha256 NIL = bytes([0] * 32) def floor_lg(n: int) -> int: """Return floor(log_2(n)) for a positive integer `n`""" assert n > 0 r = 0 t = 1 while 2 * t <= n: t = 2 * t r = r + 1 return r def ceil_lg(n: int) -> int: """Return ceiling(log_2(n)) for a positive integer `n`.""" assert n > 0 r = 0 t = 1 while t < n: t = 2 * t r = r + 1 return r def is_power_of_2(n: int) -> bool: """For a positive integer `n`, returns `True` is `n` is a perfect power of 2, `False` otherwise.""" assert n >= 1 return n & (n - 1) == 0 def largest_power_of_2_less_than(n: int) -> int: """For an integer `n` which is at least 2, returns the largest exact power of 2 that is strictly less than `n`.""" assert n > 1 if is_power_of_2(n): return n // 2 else: return 1 << floor_lg(n) def element_hash(element_preimage: bytes) -> bytes: """Computes the hash of an element to be stored in the Merkle tree.""" return sha256(b'\x00' + element_preimage) def combine_hashes(left: bytes, right: bytes) -> bytes: if len(left) != 32 or len(right) != 32: raise ValueError("The elements must be 32-bytes sha256 outputs.") return sha256(b'\x01' + left + right) # root is the only node with parent == None # leaves have left == right == None class Node: def __init__(self, left, right, parent, value: bytes): self.left = left self.right = right self.parent = parent self.value = value def recompute_value(self): assert self.left is not None assert self.right is not None self.value = combine_hashes(self.left.value, self.right.value) def sibling(self): if self.parent is None: raise IndexError("The root does not have a sibling.") if self.parent.left == self: return self.parent.right elif self.parent.right == self: return self.parent.left else: raise IndexError("Invalid state: not a child of his parent.") def make_tree(leaves: List[Node], begin: int, size: int) -> Node: """Given a list of nodes, builds the left-complete Merkle tree on top of it. The nodes in `leaves` are modified by setting their `parent` field appropriately. It returns the root of the newly built tree. """ if size == 0: return [] if size == 1: return leaves[begin] lchild_size = largest_power_of_2_less_than(size) lchild = make_tree(leaves, begin, lchild_size) rchild = make_tree(leaves, begin + lchild_size, size - lchild_size) root = Node(lchild, rchild, None, None) root.recompute_value() lchild.parent = rchild.parent = root return root class MerkleTree: """ Maintains a dynamic vector of values and the Merkle tree built on top of it. The elements of the vector are stored as the leaves of a binary tree. It is possible to add a new element to the vector, or change an existing element; the hashes in the Merkle tree will be recomputed after each operation in O(log n) time, for a vector with n elements. The value of each internal node is the hash of the concatenation of: - a single byte 0x01; - the values of the left child; - the value of the right child. The binary tree has the following properties (assuming the vector contains n leaves): - There are always n - 1 internal nodes; all the internal nodes have exactly two children. - If a subtree has n > 1 leaves, then the left subchild is a complete subtree with p leaves, where p is the largest power of 2 smaller than n. """ def __init__(self, elements: Iterable[bytes] = []): self.leaves = [Node(None, None, None, el) for el in elements] n_elements = len(self.leaves) if n_elements > 0: self.root_node = make_tree(self.leaves, 0, n_elements) self.depth = ceil_lg(n_elements) else: self.root_node = None self.depth = None def __len__(self) -> int: """Return the total number of leaves in the tree.""" return len(self.leaves) @property def root(self) -> bytes: """Return the Merkle root, or None if the tree is empty.""" return NIL if self.root_node is None else self.root_node.value def copy(self): """Return an identical copy of this Merkle tree.""" return MerkleTree([leaf.value for leaf in self.leaves]) def add(self, x: bytes) -> None: """Add an element as new leaf, and recompute the tree accordingly. Cost O(log n).""" if len(x) != 32: raise ValueError("Inserted elements must be exactly 32 bytes long") new_leaf = Node(None, None, None, x) self.leaves.append(new_leaf) if len(self.leaves) == 1: self.root_node = new_leaf self.depth = 0 return # add a new leaf if self.depth == 0: ltree_size = 0 else: # number of leaves of the left subtree of cur_root ltree_size = 1 << (self.depth - 1) cur_root = self.root_node cur_root_size = len(self.leaves) - 1 while not is_power_of_2(cur_root_size): cur_root = cur_root.right cur_root_size -= ltree_size ltree_size /= 2 # node value will be computed later new_node = Node(cur_root, new_leaf, cur_root.parent, None) if cur_root.parent is None: # replacing the root self.depth += 1 self.root_node = new_node else: assert cur_root.parent.right == cur_root cur_root.parent.right = new_node cur_root.parent = new_node new_leaf.parent = new_node self.fix_up(new_node) def set(self, index: int, x: bytes) -> None: """ Set the value of the leaf at position `index` to `x`, recomputing the tree accordingly. If `index` equals the current number of leaves, then it is equivalent to `add(x)`. Cost: Worst case O(log n). """ assert 0 <= index <= len(self.leaves) if not (0 <= index <= len(self.leaves)): raise ValueError( "The index must be at least 0, and at most the current number of leaves.") if len(x) != 32: raise ValueError("Inserted elements must be exactly 32 bytes long.") if index == len(self.leaves): self.add(x) else: self.leaves[index].value = x self.fix_up(self.leaves[index].parent) def fix_up(self, node: Node): while node is not None: node.recompute_value() node = node.parent def get(self, i: int) -> bytes: """Return the value of the leaf with index `i`, where 0 <= i < len(self).""" return self.leaves[i].value def leaf_index(self, x: bytes) -> int: """Return the index of the leaf with hash `x`. Raises `ValueError` if not found.""" idx = 0 while idx < len(self): if self.leaves[idx].value == x: return idx idx += 1 raise ValueError("Leaf not found") def prove_leaf(self, index: int) -> List[bytes]: """Produce the Merkle proof of membership for the leaf with the given index where 0 <= index < len(self).""" node = self.leaves[index] proof = [] while node.parent is not None: sibling = node.sibling() assert sibling is not None proof.append(sibling.value) node = node.parent return proof def get_merkleized_map_commitment(mapping: Mapping[bytes, bytes]) -> bytes: """Returns a serialized Merkleized map commitment, encoded as the concatenation of: - the number of key/value pairs, as a Bitcoin-style varint; - the root of the Merkle tree of the keys - the root of the Merkle tree of the values. """ items_sorted = list(sorted(mapping.items())) keys_hashes = [element_hash(i[0]) for i in items_sorted] values_hashes = [element_hash(i[1]) for i in items_sorted] return write_varint(len(mapping)) + MerkleTree(keys_hashes).root + MerkleTree(values_hashes).root ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733838905.0 ledger_bitcoin-0.4.0/ledger_bitcoin/psbt.py0000664000175000017500000014711014726044071020650 0ustar00singalasingala# Original version: https://github.com/bitcoin-core/HWI/blob/3fe369d0379212fae1c72729a179d133b0adc872/hwilib/key.py # Distributed under the MIT License. # fmt: off """ PSBT Classes and Utilities ************************** """ import base64 import struct from io import BytesIO, BufferedReader from typing import ( Dict, List, Mapping, MutableMapping, Optional, Sequence, Set, Tuple, Union, ) from .key import KeyOriginInfo from .errors import PSBTSerializationError from .tx import ( COutPoint, CTransaction, CTxIn, CTxInWitness, CTxOut, ) from ._serialize import ( deser_compact_size, deser_string, Readable, ser_compact_size, ser_string, ser_uint256, uint256_from_str, ) def DeserializeHDKeypath( f: Readable, key: bytes, hd_keypaths: MutableMapping[bytes, KeyOriginInfo], expected_sizes: Sequence[int], ) -> None: """ :meta private: Deserialize a serialized PSBT public key and keypath key-value pair. :param f: The byte stream to read the value from. :param key: The bytes of the key of the key-value pair. :param hd_keypaths: Dictionary of public key bytes to their :class:`~hwilib.key.KeyOriginInfo`. :param expected_sizes: List of key lengths expected for the keypair being deserialized. """ if len(key) not in expected_sizes: raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey. Length: {}".format(len(key))) pubkey = key[1:] if pubkey in hd_keypaths: raise PSBTSerializationError("Duplicate key, input partial signature for pubkey already provided") hd_keypaths[pubkey] = KeyOriginInfo.deserialize(deser_string(f)) def SerializeHDKeypath(hd_keypaths: Mapping[bytes, KeyOriginInfo], type: bytes) -> bytes: """ :meta private: Serialize a public key to :class:`~hwilib.key.KeyOriginInfo` mapping as a PSBT key-value pair. :param hd_keypaths: The mapping of public key to keypath :param type: The PSBT type bytes to use :returns: The serialized keypaths """ r = b"" for pubkey, path in sorted(hd_keypaths.items()): r += ser_string(type + pubkey) packed = path.serialize() r += ser_string(packed) return r class PartiallySignedInput: """ An object for a PSBT input map. """ PSBT_IN_NON_WITNESS_UTXO = 0x00 PSBT_IN_WITNESS_UTXO = 0x01 PSBT_IN_PARTIAL_SIG = 0x02 PSBT_IN_SIGHASH_TYPE = 0x03 PSBT_IN_REDEEM_SCRIPT = 0x04 PSBT_IN_WITNESS_SCRIPT = 0x05 PSBT_IN_BIP32_DERIVATION = 0x06 PSBT_IN_FINAL_SCRIPTSIG = 0x07 PSBT_IN_FINAL_SCRIPTWITNESS = 0x08 PSBT_IN_PREVIOUS_TXID = 0x0e PSBT_IN_OUTPUT_INDEX = 0x0f PSBT_IN_SEQUENCE = 0x10 PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11 PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12 PSBT_IN_TAP_KEY_SIG = 0x13 PSBT_IN_TAP_SCRIPT_SIG = 0x14 PSBT_IN_TAP_LEAF_SCRIPT = 0x15 PSBT_IN_TAP_BIP32_DERIVATION = 0x16 PSBT_IN_TAP_INTERNAL_KEY = 0x17 PSBT_IN_TAP_MERKLE_ROOT = 0x18 PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1a PSBT_IN_MUSIG2_PUB_NONCE = 0x1b PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1c def __init__(self, version: int) -> None: self.non_witness_utxo: Optional[CTransaction] = None self.witness_utxo: Optional[CTxOut] = None self.partial_sigs: Dict[bytes, bytes] = {} self.sighash: Optional[int] = None self.redeem_script = b"" self.witness_script = b"" self.hd_keypaths: Dict[bytes, KeyOriginInfo] = {} self.final_script_sig = b"" self.final_script_witness = CTxInWitness() self.prev_txid = b"" self.prev_out: Optional[int] = None self.sequence: Optional[int] = None self.time_locktime: Optional[int] = None self.height_locktime: Optional[int] = None self.tap_key_sig = b"" self.tap_script_sigs: Dict[Tuple[bytes, bytes], bytes] = {} self.tap_scripts: Dict[Tuple[bytes, int], Set[bytes]] = {} self.tap_bip32_paths: Dict[bytes, Tuple[Set[bytes], KeyOriginInfo]] = {} self.tap_internal_key = b"" self.tap_merkle_root = b"" self.musig2_participant_pubkeys: Dict[bytes, List[bytes]] = {} self.musig2_pub_nonces: Dict[Tuple[bytes, bytes, Optional[bytes]], bytes] = {} self.musig2_partial_sigs: Dict[Tuple[bytes, bytes, Optional[bytes]], bytes] = {} self.unknown: Dict[bytes, bytes] = {} self.version: int = version def set_null(self) -> None: """ Clear all values in this PSBT input map. """ self.non_witness_utxo = None self.witness_utxo = None self.partial_sigs.clear() self.sighash = None self.redeem_script = b"" self.witness_script = b"" self.hd_keypaths.clear() self.final_script_sig = b"" self.final_script_witness = CTxInWitness() self.tap_key_sig = b"" self.tap_script_sigs.clear() self.tap_scripts.clear() self.tap_bip32_paths.clear() self.tap_internal_key = b"" self.tap_merkle_root = b"" self.prev_txid = b"" self.prev_out = None self.sequence = None self.time_locktime = None self.height_locktime = None self.unknown.clear() def deserialize(self, f: Readable) -> None: """ Deserialize a serialized PSBT input. :param f: A byte stream containing the serialized PSBT input """ key_lookup: Set[bytes] = set() while True: # read the key try: key = deser_string(f) except Exception: break # Check for separator if len(key) == 0: break # First byte of key is the type key_type = deser_compact_size(BytesIO(key)) if key_type == PartiallySignedInput.PSBT_IN_NON_WITNESS_UTXO: if key in key_lookup: raise PSBTSerializationError("Duplicate Key, input non witness utxo already provided") elif len(key) != 1: raise PSBTSerializationError("non witness utxo key is more than one byte type") self.non_witness_utxo = CTransaction() utxo_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore self.non_witness_utxo.deserialize(utxo_bytes) self.non_witness_utxo.rehash() elif key_type == PartiallySignedInput.PSBT_IN_WITNESS_UTXO: if key in key_lookup: raise PSBTSerializationError("Duplicate Key, input witness utxo already provided") elif len(key) != 1: raise PSBTSerializationError("witness utxo key is more than one byte type") self.witness_utxo = CTxOut() tx_out_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore self.witness_utxo.deserialize(tx_out_bytes) elif key_type == PartiallySignedInput.PSBT_IN_PARTIAL_SIG: if len(key) != 34 and len(key) != 66: raise PSBTSerializationError("Size of key was not the expected size for the type partial signature pubkey") pubkey = key[1:] if pubkey in self.partial_sigs: raise PSBTSerializationError("Duplicate key, input partial signature for pubkey already provided") sig = deser_string(f) self.partial_sigs[pubkey] = sig elif key_type == PartiallySignedInput.PSBT_IN_SIGHASH_TYPE: if key in key_lookup: raise PSBTSerializationError("Duplicate key, input sighash type already provided") elif len(key) != 1: raise PSBTSerializationError("sighash key is more than one byte type") sighash_bytes = deser_string(f) self.sighash = struct.unpack(" 65: raise PSBTSerializationError("Input Taproot key path signature is longer than 65 bytes") elif key_type == PartiallySignedInput.PSBT_IN_TAP_SCRIPT_SIG: if key in key_lookup: raise PSBTSerializationError("Duplicate key, input Taproot script signature already provided") elif len(key) != 65: raise PSBTSerializationError("Input Taproot script signature key is not 65 bytes") xonly = key[1:33] script_hash = key[33:65] sig = deser_string(f) if len(sig) < 64: raise PSBTSerializationError("Input Taproot script path signature is shorter than 64 bytes") elif len(sig) > 65: raise PSBTSerializationError("Input Taproot script path signature is longer than 65 bytes") self.tap_script_sigs[(xonly, script_hash)] = sig elif key_type == PartiallySignedInput.PSBT_IN_TAP_LEAF_SCRIPT: if key in key_lookup: raise PSBTSerializationError("Duplicate key, input Taproot leaf script already provided") elif len(key) < 34: raise PSBTSerializationError("Input Taproot leaf script key is not at least 34 bytes") elif (len(key) - 2) % 32 != 0: raise PSBTSerializationError("Input Taproot leaf script key's control block is not valid") script = deser_string(f) if len(script) == 0: raise PSBTSerializationError("Input Taproot leaf script cannot be empty") leaf_script = (script[:-1], int(script[-1])) if leaf_script not in self.tap_scripts: self.tap_scripts[leaf_script] = set() self.tap_scripts[(script[:-1], int(script[-1]))].add(key[1:]) elif key_type == PartiallySignedInput.PSBT_IN_TAP_BIP32_DERIVATION: if key in key_lookup: raise PSBTSerializationError("Duplicate key, input Taproot BIP 32 keypath already provided") elif len(key) != 33: raise PSBTSerializationError("Input Taproot BIP 32 keypath key is not 33 bytes") xonly = key[1:33] value = deser_string(f) vs = BytesIO(value) num_hashes = deser_compact_size(vs) leaf_hashes = set() for i in range(0, num_hashes): leaf_hashes.add(vs.read(32)) self.tap_bip32_paths[xonly] = (leaf_hashes, KeyOriginInfo.deserialize(vs.read())) elif key_type == PartiallySignedInput.PSBT_IN_TAP_INTERNAL_KEY: if key in key_lookup: raise PSBTSerializationError("Duplicate key, input Taproot internal key already provided") elif len(key) != 1: raise PSBTSerializationError("Input Taproot internal key key is more than one byte type") self.tap_internal_key = deser_string(f) if len(self.tap_internal_key) != 32: raise PSBTSerializationError("Input Taproot internal key is not 32 bytes") elif key_type == PartiallySignedInput.PSBT_IN_TAP_MERKLE_ROOT: if key in key_lookup: raise PSBTSerializationError("Duplicate key, input Taproot merkle root already provided") elif len(key) != 1: raise PSBTSerializationError("Input Taproot merkle root key is more than one byte type") self.tap_merkle_root = deser_string(f) if len(self.tap_merkle_root) != 32: raise PSBTSerializationError("Input Taproot merkle root is not 32 bytes") elif key_type == PartiallySignedInput.PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS: if key in key_lookup: raise PSBTSerializationError("Duplicate key, input Musig2 participant pubkeys already provided") elif len(key) != 1 + 33: raise PSBTSerializationError("Input Musig2 aggregate compressed pubkey is not 33 bytes") pubkeys_cat = deser_string(f) if len(pubkeys_cat) == 0: raise PSBTSerializationError("The list of compressed pubkeys for Musig2 cannot be empty") if (len(pubkeys_cat) % 33) != 0: raise PSBTSerializationError("The compressed pubkeys for Musig2 must be exactly 33 bytes long") pubkeys = [] for i in range(0, len(pubkeys_cat), 33): pubkeys.append(pubkeys_cat[i: i + 33]) self.musig2_participant_pubkeys[key[1:]] = pubkeys elif key_type == PartiallySignedInput.PSBT_IN_MUSIG2_PUB_NONCE: if key in key_lookup: raise PSBTSerializationError("Duplicate key, Musig2 public nonce already provided") elif len(key) not in [1 + 33 + 33, 1 + 33 + 33 + 32]: raise PSBTSerializationError("Invalid key length for Musig2 public nonce") providing_pubkey = key[1:1+33] aggregate_pubkey = key[1+33:1+33+33] tapleaf_hash = None if len(key) == 1 + 33 + 33 else key[1+33+33:] public_nonces = deser_string(f) if len(public_nonces) != 66: raise PSBTSerializationError("The length of the public nonces in Musig2 must be exactly 66 bytes") self.musig2_pub_nonces[(providing_pubkey, aggregate_pubkey, tapleaf_hash)] = public_nonces elif key_type == PartiallySignedInput.PSBT_IN_MUSIG2_PARTIAL_SIG: if key in key_lookup: raise PSBTSerializationError("Duplicate key, Musig2 partial signature already provided") elif len(key) not in [1 + 33 + 33, 1 + 33 + 33 + 32]: raise PSBTSerializationError("Invalid key length for Musig2 partial signature") providing_pubkey = key[1:1+33] aggregate_pubkey = key[1+33:1+33+33] tapleaf_hash = None if len(key) == 1 + 33 + 33 else key[1+33+33:] partial_sig = deser_string(f) if len(partial_sig) != 32: raise PSBTSerializationError("The length of the partial signature in Musig2 must be exactly 32 bytes") self.musig2_partial_sigs[(providing_pubkey, aggregate_pubkey, tapleaf_hash)] = partial_sig else: if key in self.unknown: raise PSBTSerializationError("Duplicate key, key for unknown value already provided") unknown_bytes = deser_string(f) self.unknown[key] = unknown_bytes key_lookup.add(key) # Make sure required PSBTv2 fields are present if self.version >= 2: if len(self.prev_txid) == 0: raise PSBTSerializationError("Previous TXID is required in PSBTv2") if self.prev_out is None: raise PSBTSerializationError("Previous output's index is required in PSBTv2") def serialize(self) -> bytes: """ Serialize this PSBT input :returns: The serialized PSBT input """ r = b"" if self.non_witness_utxo: r += ser_string(ser_compact_size(PartiallySignedInput.PSBT_IN_NON_WITNESS_UTXO)) tx = self.non_witness_utxo.serialize_with_witness() r += ser_string(tx) if self.witness_utxo: r += ser_string(ser_compact_size(PartiallySignedInput.PSBT_IN_WITNESS_UTXO)) tx = self.witness_utxo.serialize() r += ser_string(tx) if len(self.final_script_sig) == 0 and self.final_script_witness.is_null(): for pubkey, sig in sorted(self.partial_sigs.items()): r += ser_string(ser_compact_size(PartiallySignedInput.PSBT_IN_PARTIAL_SIG) + pubkey) r += ser_string(sig) if self.sighash is not None: r += ser_string(ser_compact_size(PartiallySignedInput.PSBT_IN_SIGHASH_TYPE)) r += ser_string(struct.pack("= 2: if len(self.prev_txid) != 0: r += ser_string(ser_compact_size(PartiallySignedInput.PSBT_IN_PREVIOUS_TXID)) r += ser_string(self.prev_txid) if self.prev_out is not None: r += ser_string(ser_compact_size(PartiallySignedInput.PSBT_IN_OUTPUT_INDEX)) r += ser_string(struct.pack(" None: self.redeem_script = b"" self.witness_script = b"" self.hd_keypaths: Dict[bytes, KeyOriginInfo] = {} self.amount: Optional[int] = None self.script = b"" self.tap_internal_key = b"" self.tap_tree = b"" self.tap_bip32_paths: Dict[bytes, Tuple[Set[bytes], KeyOriginInfo]] = {} self.musig2_participant_pubkeys: Dict[bytes, List[bytes]] = {} self.unknown: Dict[bytes, bytes] = {} self.version: int = version def set_null(self) -> None: """ Clear this PSBT output map """ self.redeem_script = b"" self.witness_script = b"" self.hd_keypaths.clear() self.tap_internal_key = b"" self.tap_tree = b"" self.tap_bip32_paths.clear() self.amount = None self.script = b"" self.unknown.clear() def deserialize(self, f: Readable) -> None: """ Deserialize a serialized PSBT output map :param f: A byte stream containing the serialized PSBT output """ key_lookup: Set[bytes] = set() while True: # read the key try: key = deser_string(f) except Exception: break # Check for separator if len(key) == 0: break # First byte of key is the type key_type = deser_compact_size(BytesIO(key)) if key_type == PartiallySignedOutput.PSBT_OUT_REDEEM_SCRIPT: if key in key_lookup: raise PSBTSerializationError("Duplicate key, output redeemScript already provided") elif len(key) != 1: raise PSBTSerializationError("Output redeemScript key is more than one byte type") self.redeem_script = deser_string(f) elif key_type == PartiallySignedOutput.PSBT_OUT_WITNESS_SCRIPT: if key in key_lookup: raise PSBTSerializationError("Duplicate key, output witnessScript already provided") elif len(key) != 1: raise PSBTSerializationError("Output witnessScript key is more than one byte type") self.witness_script = deser_string(f) elif key_type == PartiallySignedOutput.PSBT_OUT_BIP32_DERIVATION: DeserializeHDKeypath(f, key, self.hd_keypaths, [34, 66]) elif key_type == PartiallySignedOutput.PSBT_OUT_AMOUNT: if key in key_lookup: raise PSBTSerializationError("Duplicate key, output amount already provided") elif len(key) != 1: raise PSBTSerializationError("Output amount key is more than one byte type") v = deser_string(f) if len(v) != 8: raise PSBTSerializationError("Output amount is not 8 bytes") self.amount = struct.unpack("= 2: if self.amount is None: raise PSBTSerializationError("PSBT_OUTPUT_AMOUNT is required in PSBTv2") if len(self.script) == 0: raise PSBTSerializationError("PSBT_OUTPUT_SCRIPT is required in PSBTv2") def serialize(self) -> bytes: """ Serialize this PSBT output :returns: The serialized PSBT output """ r = b"" if len(self.redeem_script) != 0: r += ser_string(ser_compact_size(PartiallySignedOutput.PSBT_OUT_REDEEM_SCRIPT)) r += ser_string(self.redeem_script) if len(self.witness_script) != 0: r += ser_string(ser_compact_size(PartiallySignedOutput.PSBT_OUT_WITNESS_SCRIPT)) r += ser_string(self.witness_script) r += SerializeHDKeypath(self.hd_keypaths, ser_compact_size(PartiallySignedOutput.PSBT_OUT_BIP32_DERIVATION)) if self.version >= 2: if self.amount is not None: r += ser_string(ser_compact_size(PartiallySignedOutput.PSBT_OUT_AMOUNT)) r += ser_string(struct.pack(" CTxOut: """ Creates a CTxOut for this output :returns: The CTxOut """ assert self.amount is not None assert len(self.script) != 0 return CTxOut(self.amount, self.script) class PSBT(object): """ A class representing a PSBT """ PSBT_GLOBAL_UNSIGNED_TX = 0x00 PSBT_GLOBAL_XPUB = 0x01 PSBT_GLOBAL_TX_VERSION = 0x02 PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03 PSBT_GLOBAL_INPUT_COUNT = 0x04 PSBT_GLOBAL_OUTPUT_COUNT = 0x05 PSBT_GLOBAL_TX_MODIFIABLE = 0x06 PSBT_GLOBAL_VERSION = 0xFB def __init__(self, tx: Optional[CTransaction] = None) -> None: """ :param tx: A Bitcoin transaction that specifies the inputs and outputs to use """ if tx: self.tx = tx else: self.tx = CTransaction() self.inputs: List[PartiallySignedInput] = [] self.outputs: List[PartiallySignedOutput] = [] self.unknown: Dict[bytes, bytes] = {} self.xpub: Dict[bytes, KeyOriginInfo] = {} self.tx_version: Optional[int] = None self.fallback_locktime: Optional[int] = None self.tx_modifiable: Optional[int] = None # Assume version 0 PSBT self.version = 0 self.explicit_version = False def deserialize(self, psbt: str) -> None: """ Deserialize a base 64 encoded PSBT. :param psbt: A base 64 PSBT. """ psbt_bytes = base64.b64decode(psbt.strip()) f = BufferedReader(BytesIO(psbt_bytes)) # type: ignore end = len(psbt_bytes) # Read the magic bytes magic = f.read(5) if magic != b"psbt\xff": raise PSBTSerializationError("invalid magic") key_lookup: Set[bytes] = set() input_count = None output_count = None # Read loop while True: # read the key try: key = deser_string(f) except Exception: break # Check for separator if len(key) == 0: break # First byte of key is the type key_type = deser_compact_size(BytesIO(key)) # Do stuff based on type if key_type == PSBT.PSBT_GLOBAL_UNSIGNED_TX: # Checks for correctness if key in key_lookup: raise PSBTSerializationError("Duplicate key, unsigned tx already provided") elif len(key) > 1: raise PSBTSerializationError("Global unsigned tx key is more than one byte type") # read in value tx_bytes = BufferedReader(BytesIO(deser_string(f))) # type: ignore self.tx.deserialize(tx_bytes) # Make sure that all scriptSigs and scriptWitnesses are empty for txin in self.tx.vin: if len(txin.scriptSig) != 0 or not self.tx.wit.is_null(): raise PSBTSerializationError("Unsigned tx does not have empty scriptSigs and scriptWitnesses") elif key_type == PSBT.PSBT_GLOBAL_XPUB: DeserializeHDKeypath(f, key, self.xpub, [79]) elif key_type == PSBT.PSBT_GLOBAL_TX_VERSION: if key in key_lookup: raise PSBTSerializationError("Duplicate key, global transaction version is already provided") elif len(key) > 1: raise PSBTSerializationError("Global transaction version key is more than one byte type") v = deser_string(f) if len(v) != 4: raise PSBTSerializationError("Global transaction version is not 4 bytes") self.tx_version = struct.unpack(" 1: raise PSBTSerializationError("Global fallback locktime key is more than one byte type") v = deser_string(f) if len(v) != 4: raise PSBTSerializationError("Global fallback locktime is not 4 bytes") self.fallback_locktime = struct.unpack(" 1: raise PSBTSerializationError("Global input count key is more than one byte type") _ = deser_compact_size(f) # Value length, we can ignore this input_count = deser_compact_size(f) elif key_type == PSBT.PSBT_GLOBAL_OUTPUT_COUNT: if key in key_lookup: raise PSBTSerializationError("Duplicate key, global output count is already provided") elif len(key) > 1: raise PSBTSerializationError("Global output count key is more than one byte type") _ = deser_compact_size(f) # Value length, we can ignore this output_count = deser_compact_size(f) elif key_type == PSBT.PSBT_GLOBAL_TX_MODIFIABLE: if key in key_lookup: raise PSBTSerializationError("Duplicate key, global tx modifiable flags is already provided") elif len(key) > 1: raise PSBTSerializationError("Global tx modifiable flags key is more than one byte type") v = deser_string(f) if len(v) != 1: raise PSBTSerializationError("Global tx modifiable flags is not 1 bytes") self.tx_modifiable = struct.unpack(" 1: raise PSBTSerializationError("Global PSBT version key is more than one byte type") v = deser_string(f) if len(v) != 4: raise PSBTSerializationError("Global PSBT version is not 1 bytes") self.version = struct.unpack("= 2: # Tx version, input, and output counts are required if self.tx_version is None: raise PSBTSerializationError("PSBT_GLOBAL_TX_VERSION is required in PSBTv2") if input_count is None: raise PSBTSerializationError("PSBT_GLOBAL_INPUT_COUNT is required in PSBTv2") if output_count is None: raise PSBTSerializationError("PSBT_GLOBAL_OUTPUT_COUNT is required in PSBTv2") # Unsigned tx is disallowed if not self.tx.is_null(): raise PSBTSerializationError("PSBT_GLOBAL_UNSIGNED_TX is not allowed in PSBTv2") # Read input data if input_count is None: input_count = len(self.tx.vin) for i in range(input_count): if f.tell() == end: break psbt_in = PartiallySignedInput(self.version) psbt_in.deserialize(f) self.inputs.append(psbt_in) if self.version >= 2: prev_txid = psbt_in.prev_txid else: prev_txid = ser_uint256(self.tx.vin[i].prevout.hash) if psbt_in.non_witness_utxo: psbt_in.non_witness_utxo.rehash() if psbt_in.non_witness_utxo.hash != prev_txid: raise PSBTSerializationError("Non-witness UTXO does not match outpoint hash") if (len(self.inputs) != input_count): raise PSBTSerializationError("Inputs provided does not match the number of inputs in transaction") # Read output data if output_count is None: output_count = len(self.tx.vout) for i in range(output_count): if f.tell() == end: break output = PartiallySignedOutput(self.version) output.deserialize(f) self.outputs.append(output) if len(self.outputs) != output_count: raise PSBTSerializationError("Outputs provided does not match the number of outputs in transaction") self.cache_unsigned_tx_pieces() def serialize(self) -> str: """ Serialize the PSBT as a base 64 encoded string. :returns: The base 64 encoded string. """ r = b"" # magic bytes r += b"psbt\xff" if self.version == 0: # unsigned tx flag r += ser_string(ser_compact_size(PSBT.PSBT_GLOBAL_UNSIGNED_TX)) # write serialized tx tx = self.tx.serialize_with_witness() r += ser_compact_size(len(tx)) r += tx # write xpubs r += SerializeHDKeypath(self.xpub, ser_compact_size(PSBT.PSBT_GLOBAL_XPUB)) if self.version >= 2: assert self.tx_version is not None r += ser_string(ser_compact_size(PSBT.PSBT_GLOBAL_TX_VERSION)) r += ser_string(struct.pack(" 0 or self.explicit_version: r += ser_string(ser_compact_size(PSBT.PSBT_GLOBAL_VERSION)) r += ser_string(struct.pack(" None: """ If this PSBT is v0, then the global unsigned transaction will be used to fill in the PSBTv2 fields so that all users of the PSBT classes can use the same PSBTv2 interface regardless of PSBT version. Does nothing if the PSBT is already v2. """ # To make things easier, we split up the global transaction # and use the PSBTv2 fields for PSBTv0 if self.tx is not None: self.setup_from_tx(self.tx) def setup_from_tx(self, tx: CTransaction): """ Fills in the PSBTv2 fields for this PSBT given a transaction :param tx: The CTransaction to fill from """ self.tx_version = tx.nVersion self.fallback_locktime = tx.nLockTime for i, txin in enumerate(tx.vin): psbt_in = self.inputs[i] psbt_in.prev_txid = ser_uint256(txin.prevout.hash) psbt_in.prev_out = txin.prevout.n psbt_in.sequence = txin.nSequence for i, txout in enumerate(tx.vout): psbt_out = self.outputs[i] psbt_out.amount = txout.nValue psbt_out.script = txout.scriptPubKey def compute_lock_time(self) -> int: """ Computes the lock time for this transaction :returns: The lock time """ time_lock: Optional[int] = 0 height_lock: Optional[int] = 0 for psbt_in in self.inputs: if psbt_in.time_locktime is not None and psbt_in.height_locktime is None: height_lock = None if time_lock is None: raise PSBTSerializationError("Cannot require both time and height locktimes") elif psbt_in.time_locktime is None and psbt_in.height_locktime is not None: time_lock = None if height_lock is None: raise PSBTSerializationError("Cannot require both time and height locktimes") if psbt_in.time_locktime is not None and time_lock is not None: time_lock = max(time_lock, psbt_in.time_locktime) if psbt_in.height_locktime is not None and height_lock is not None: height_lock = max(height_lock, psbt_in.height_locktime) if height_lock is not None and height_lock > 0: return height_lock if time_lock is not None and time_lock > 0: return time_lock if self.fallback_locktime is not None: return self.fallback_locktime return 0 def get_unsigned_tx(self) -> CTransaction: """ Get the unsigned transaction represented by this PSBT :return: A CTransaction """ if not self.tx.is_null(): return self.tx assert self.tx_version is not None tx = CTransaction() tx.nVersion = self.tx_version self.nLockTime = self.compute_lock_time() for psbt_in in self.inputs: assert psbt_in.prev_txid is not None assert psbt_in.prev_out is not None assert psbt_in.sequence is not None txin = CTxIn(COutPoint(uint256_from_str(psbt_in.prev_txid), psbt_in.prev_out), b"", psbt_in.sequence) tx.vin.append(txin) for psbt_out in self.outputs: assert psbt_out.amount is not None txout = CTxOut(psbt_out.amount, psbt_out.script) tx.vout.append(txout) tx.rehash() return tx def _convert_version(self, version) -> None: self.version = version for psbt_in in self.inputs: psbt_in.version = version for psbt_out in self.outputs: psbt_out.version = version def convert_to_v2(self) -> None: """ Sets this PSBT to version 2 """ if self.version == 0: # make sure fields that PSBT version 0 fields are not present self.setup_from_tx(self.tx) self._convert_version(2) def convert_to_v0(self) -> None: """ Sets this PSBT to version 0 """ if self.version == 2: # strip PSBT version 2 fields self.tx_version = None self.fallback_locktime = None self.tx_modifiable = None self._convert_version(0) self.tx = self.get_unsigned_tx() self.explicit_version = False def normalize_psbt(psbt: Union[PSBT, bytes, str]) -> PSBT: """ Deserializes a psbt given as an argument from a string or a byte array, if necessary. :param psbt: Either an instance of PSBT, or binary-encoded psbt as `bytes`, or a base64-encoded psbt as a `str`. :returns: the deserialized PSBT object. If `psbt` was already a `PSBT`, it is returned directly (without cloning). """ if isinstance(psbt, bytes): psbt = base64.b64encode(psbt).decode() if isinstance(psbt, str): psbt_obj = PSBT() psbt_obj.deserialize(psbt) psbt = psbt_obj return psbt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707236603.0 ledger_bitcoin-0.4.0/ledger_bitcoin/py.typed0000664000175000017500000000000014560456373021017 0ustar00singalasingala././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698934586.0 ledger_bitcoin-0.4.0/ledger_bitcoin/ripemd.py0000664000175000017500000001015614520727472021164 0ustar00singalasingala# Copyright (c) 2021 Pieter Wuille # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. # # Taken from https://github.com/bitcoin/bitcoin/blob/124e75a41ea0f3f0e90b63b0c41813184ddce2ab/test/functional/test_framework/ripemd160.py """ Pure Python RIPEMD160 implementation. WARNING: This implementation is NOT constant-time. Do not use without understanding the implications. """ # Message schedule indexes for the left path. ML = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13 ] # Message schedule indexes for the right path. MR = [ 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11 ] # Rotation counts for the left path. RL = [ 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 ] # Rotation counts for the right path. RR = [ 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 ] # K constants for the left path. KL = [0, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e] # K constants for the right path. KR = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0] def fi(x, y, z, i): """The f1, f2, f3, f4, and f5 functions from the specification.""" if i == 0: return x ^ y ^ z elif i == 1: return (x & y) | (~x & z) elif i == 2: return (x | ~y) ^ z elif i == 3: return (x & z) | (y & ~z) elif i == 4: return x ^ (y | ~z) else: assert False def rol(x, i): """Rotate the bottom 32 bits of x left by i bits.""" return ((x << i) | ((x & 0xffffffff) >> (32 - i))) & 0xffffffff def compress(h0, h1, h2, h3, h4, block): """Compress state (h0, h1, h2, h3, h4) with block.""" # Left path variables. al, bl, cl, dl, el = h0, h1, h2, h3, h4 # Right path variables. ar, br, cr, dr, er = h0, h1, h2, h3, h4 # Message variables. x = [int.from_bytes(block[4*i:4*(i+1)], 'little') for i in range(16)] # Iterate over the 80 rounds of the compression. for j in range(80): rnd = j >> 4 # Perform left side of the transformation. al = rol(al + fi(bl, cl, dl, rnd) + x[ML[j]] + KL[rnd], RL[j]) + el al, bl, cl, dl, el = el, al, bl, rol(cl, 10), dl # Perform right side of the transformation. ar = rol(ar + fi(br, cr, dr, 4 - rnd) + x[MR[j]] + KR[rnd], RR[j]) + er ar, br, cr, dr, er = er, ar, br, rol(cr, 10), dr # Compose old state, left transform, and right transform into new state. return h1 + cl + dr, h2 + dl + er, h3 + el + ar, h4 + al + br, h0 + bl + cr def ripemd160(data): """Compute the RIPEMD-160 hash of data.""" # Initialize state. state = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0) # Process full 64-byte blocks in the input. for b in range(len(data) >> 6): state = compress(*state, data[64*b:64*(b+1)]) # Construct final blocks (with padding and size). pad = b"\x80" + b"\x00" * ((119 - len(data)) & 63) fin = data[len(data) & ~63:] + pad + (8 * len(data)).to_bytes(8, 'little') # Process final blocks. for b in range(len(fin) >> 6): state = compress(*state, fin[64*b:64*(b+1)]) # Produce output. return b"".join((h & 0xffffffff).to_bytes(4, 'little') for h in state) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707236603.0 ledger_bitcoin-0.4.0/ledger_bitcoin/segwit_addr.py0000664000175000017500000001163514560456373022206 0ustar00singalasingala# Copyright (c) 2017, 2020 Pieter Wuille # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """Reference implementation for Bech32/Bech32m and segwit addresses.""" from enum import Enum class Encoding(Enum): """Enumeration type to list the various supported encodings.""" BECH32 = 1 BECH32M = 2 CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" BECH32M_CONST = 0x2bc830a3 def bech32_polymod(values): """Internal function that computes the Bech32 checksum.""" generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] chk = 1 for value in values: top = chk >> 25 chk = (chk & 0x1ffffff) << 5 ^ value for i in range(5): chk ^= generator[i] if ((top >> i) & 1) else 0 return chk def bech32_hrp_expand(hrp): """Expand the HRP into values for checksum computation.""" return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] def bech32_verify_checksum(hrp, data): """Verify a checksum given HRP and converted data characters.""" const = bech32_polymod(bech32_hrp_expand(hrp) + data) if const == 1: return Encoding.BECH32 if const == BECH32M_CONST: return Encoding.BECH32M return None def bech32_create_checksum(hrp, data, spec): """Compute the checksum values given HRP and data.""" values = bech32_hrp_expand(hrp) + data const = BECH32M_CONST if spec == Encoding.BECH32M else 1 polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] def bech32_encode(hrp, data, spec): """Compute a Bech32 string given HRP and data values.""" combined = data + bech32_create_checksum(hrp, data, spec) return hrp + '1' + ''.join([CHARSET[d] for d in combined]) def bech32_decode(bech): """Validate a Bech32/Bech32m string, and determine HRP and data.""" if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (bech.lower() != bech and bech.upper() != bech)): return (None, None, None) bech = bech.lower() pos = bech.rfind('1') if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: return (None, None, None) if not all(x in CHARSET for x in bech[pos+1:]): return (None, None, None) hrp = bech[:pos] data = [CHARSET.find(x) for x in bech[pos+1:]] spec = bech32_verify_checksum(hrp, data) if spec is None: return (None, None, None) return (hrp, data[:-6], spec) def convertbits(data, frombits, tobits, pad=True): """General power-of-2 base conversion.""" acc = 0 bits = 0 ret = [] maxv = (1 << tobits) - 1 max_acc = (1 << (frombits + tobits - 1)) - 1 for value in data: if value < 0 or (value >> frombits): return None acc = ((acc << frombits) | value) & max_acc bits += frombits while bits >= tobits: bits -= tobits ret.append((acc >> bits) & maxv) if pad: if bits: ret.append((acc << (tobits - bits)) & maxv) elif bits >= frombits or ((acc << (tobits - bits)) & maxv): return None return ret def decode(hrp, addr): """Decode a segwit address.""" hrpgot, data, spec = bech32_decode(addr) if hrpgot != hrp: return (None, None) decoded = convertbits(data[1:], 5, 8, False) if decoded is None or len(decoded) < 2 or len(decoded) > 40: return (None, None) if data[0] > 16: return (None, None) if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: return (None, None) if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M: return (None, None) return (data[0], decoded) def encode(hrp, witver, witprog): """Encode a segwit address.""" spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) if decode(hrp, ret) == (None, None): return None return ret././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698934586.0 ledger_bitcoin-0.4.0/ledger_bitcoin/transport.py0000664000175000017500000001670714520727472021750 0ustar00singalasingala# extracted from ledgercomm in order to add the `path` parameter to the constructor. import enum import logging import struct from typing import Union, Tuple, Optional, Literal, cast from ledgercomm.interfaces.tcp_client import TCPClient from ledgercomm.interfaces.hid_device import HID from ledgercomm.log import LOG class TransportType(enum.Enum): """Type of interface available.""" HID = 1 TCP = 2 class Transport: """Transport class to send APDUs. Allow to communicate using HID device such as Nano S/X or through TCP socket with the Speculos emulator. Parameters ---------- interface : str Either "hid" or "tcp" for the underlying communication interface. server : str IP address of the TCP server if interface is "tcp". port : int Port of the TCP server if interface is "tcp". path : Optional[str] The path to use with HID if interface is "hid"; defaults to `None`. hid : Optional[HID] The HID instance to use if interface is "hid"; defaults to `None`. If not None, the instance is already presumed open. debug : bool Whether you want debug logs or not. Attributes ---------- interface : TransportType Either TransportType.HID or TransportType.TCP. com : Union[TCPClient, HID] Communication interface to send/receive APDUs. """ def __init__(self, interface: Literal["hid", "tcp"] = "tcp", server: str = "127.0.0.1", port: int = 9999, path: Optional[str] = None, hid: Optional[HID] = None, debug: bool = False) -> None: """Init constructor of Transport.""" if debug: LOG.setLevel(logging.DEBUG) self.inferface: TransportType try: self.interface = TransportType[interface.upper()] except KeyError as exc: raise Exception(f"Unknown interface '{interface}'!") from exc if self.interface == TransportType.TCP: self.com = TCPClient( server=server, port=port) self.com.open() else: if hid is not None: self.com = hid # we assume the instance is already open, when the `hid` parameter is given else: self.com = HID() self.com.path = path self.com.open() @staticmethod def apdu_header(cla: int, ins: Union[int, enum.IntEnum], p1: int = 0, p2: int = 0, opt: Optional[int] = None, lc: int = 0) -> bytes: """Pack the APDU header as bytes. Parameters ---------- cla : int Instruction class: CLA (1 byte) ins : Union[int, IntEnum] Instruction code: INS (1 byte) p1 : int Instruction parameter: P1 (1 byte). p2 : int Instruction parameter: P2 (1 byte). opt : Optional[int] Optional parameter: Opt (1 byte). lc : int Number of bytes in the payload: Lc (1 byte). Returns ------- bytes APDU header packed with parameters. """ ins = cast(int, ins.value) if isinstance( ins, enum.IntEnum) else cast(int, ins) if opt: return struct.pack("BBBBBB", cla, ins, p1, p2, 1 + lc, # add option to length opt) return struct.pack("BBBBB", cla, ins, p1, p2, lc) def send(self, cla: int, ins: Union[int, enum.IntEnum], p1: int = 0, p2: int = 0, option: Optional[int] = None, cdata: bytes = b"") -> int: """Send structured APDUs through `self.com`. Parameters ---------- cla : int Instruction class: CLA (1 byte) ins : Union[int, IntEnum] Instruction code: INS (1 byte) p1 : int Instruction parameter: P1 (1 byte). p2 : int Instruction parameter: P2 (1 byte). option : Optional[int] Optional parameter: Opt (1 byte). cdata : bytes Command data (variable length). Returns ------- int Total lenght of the APDU sent. """ header: bytes = Transport.apdu_header( cla, ins, p1, p2, option, len(cdata)) return self.com.send(header + cdata) def send_raw(self, apdu: Union[str, bytes]) -> int: """Send raw bytes `apdu` through `self.com`. Parameters ---------- apdu : Union[str, bytes] Hexstring or bytes within APDU to be sent through `self.com`. Returns ------- Optional[int] Total lenght of APDU sent if any. """ if isinstance(apdu, str): apdu = bytes.fromhex(apdu) return self.com.send(apdu) def recv(self) -> Tuple[int, bytes]: """Receive data from `self.com`. Blocking IO. Returns ------- Tuple[int, bytes] A pair (sw, rdata) for the status word (2 bytes represented as int) and the reponse data (variable lenght). """ return self.com.recv() def exchange(self, cla: int, ins: Union[int, enum.IntEnum], p1: int = 0, p2: int = 0, option: Optional[int] = None, cdata: bytes = b"") -> Tuple[int, bytes]: """Send structured APDUs and wait to receive datas from `self.com`. Parameters ---------- cla : int Instruction class: CLA (1 byte) ins : Union[int, IntEnum] Instruction code: INS (1 byte) p1 : int Instruction parameter: P1 (1 byte). p2 : int Instruction parameter: P2 (1 byte). option : Optional[int] Optional parameter: Opt (1 byte). cdata : bytes Command data (variable length). Returns ------- Tuple[int, bytes] A pair (sw, rdata) for the status word (2 bytes represented as int) and the reponse data (bytes of variable lenght). """ header: bytes = Transport.apdu_header( cla, ins, p1, p2, option, len(cdata)) return self.com.exchange(header + cdata) def exchange_raw(self, apdu: Union[str, bytes]) -> Tuple[int, bytes]: """Send raw bytes `apdu` and wait to receive datas from `self.com`. Parameters ---------- apdu : Union[str, bytes] Hexstring or bytes within APDU to send through `self.com`. Returns ------- Tuple[int, bytes] A pair (sw, rdata) for the status word (2 bytes represented as int) and the reponse (bytes of variable lenght). """ if isinstance(apdu, str): apdu = bytes.fromhex(apdu) return self.com.exchange(apdu) def close(self) -> None: """Close `self.com` interface. Returns ------- None """ self.com.close() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698934586.0 ledger_bitcoin-0.4.0/ledger_bitcoin/tx.py0000664000175000017500000002174714520727472020347 0ustar00singalasingala#!/usr/bin/env python3 # Copyright (c) 2010 ArtForz -- public domain half-a-node # Copyright (c) 2012 Jeff Garzik # Copyright (c) 2010-2016 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Bitcoin Object Python Serializations Modified from the test/test_framework/mininode.py file from the Bitcoin repository CTransaction,CTxIn, CTxOut, etc....: data structures that should map to corresponding structures in bitcoin/primitives for transactions only """ import copy import struct from .common import ( hash256, ) from ._script import ( is_opreturn, is_p2sh, is_p2pkh, is_p2pk, is_witness, is_p2wsh, ) from ._serialize import ( deser_uint256, deser_string, deser_string_vector, deser_vector, Readable, ser_uint256, ser_string, ser_string_vector, ser_vector, uint256_from_str, ) from typing import ( List, Optional, Tuple, ) # Objects that map to bitcoind objects, which can be serialized/deserialized MSG_WITNESS_FLAG = 1 << 30 class COutPoint(object): def __init__(self, hash: int = 0, n: int = 0xffffffff): self.hash = hash self.n = n def deserialize(self, f: Readable) -> None: self.hash = deser_uint256(f) self.n = struct.unpack(" bytes: r = b"" r += ser_uint256(self.hash) r += struct.pack(" str: return "COutPoint(hash=%064x n=%i)" % (self.hash, self.n) class CTxIn(object): def __init__( self, outpoint: Optional[COutPoint] = None, scriptSig: bytes = b"", nSequence: int = 0, ): if outpoint is None: self.prevout = COutPoint() else: self.prevout = outpoint self.scriptSig = scriptSig self.nSequence = nSequence def deserialize(self, f: Readable) -> None: self.prevout = COutPoint() self.prevout.deserialize(f) self.scriptSig = deser_string(f) self.nSequence = struct.unpack(" bytes: r = b"" r += self.prevout.serialize() r += ser_string(self.scriptSig) r += struct.pack(" str: return "CTxIn(prevout=%s scriptSig=%s nSequence=%i)" \ % (repr(self.prevout), self.scriptSig.hex(), self.nSequence) class CTxOut(object): def __init__(self, nValue: int = 0, scriptPubKey: bytes = b""): self.nValue = nValue self.scriptPubKey = scriptPubKey def deserialize(self, f: Readable) -> None: self.nValue = struct.unpack(" bytes: r = b"" r += struct.pack(" bool: return is_opreturn(self.scriptPubKey) def is_p2sh(self) -> bool: return is_p2sh(self.scriptPubKey) def is_p2wsh(self) -> bool: return is_p2wsh(self.scriptPubKey) def is_p2pkh(self) -> bool: return is_p2pkh(self.scriptPubKey) def is_p2pk(self) -> bool: return is_p2pk(self.scriptPubKey) def is_witness(self) -> Tuple[bool, int, bytes]: return is_witness(self.scriptPubKey) def __repr__(self) -> str: return "CTxOut(nValue=%i.%08i scriptPubKey=%s)" \ % (self.nValue // 100_000_000, self.nValue % 100_000_000, self.scriptPubKey.hex()) class CScriptWitness(object): def __init__(self) -> None: # stack is a vector of strings self.stack: List[bytes] = [] def __repr__(self) -> str: return "CScriptWitness(%s)" % \ (",".join([x.hex() for x in self.stack])) def is_null(self) -> bool: if self.stack: return False return True class CTxInWitness(object): def __init__(self) -> None: self.scriptWitness = CScriptWitness() def deserialize(self, f: Readable) -> None: self.scriptWitness.stack = deser_string_vector(f) def serialize(self) -> bytes: return ser_string_vector(self.scriptWitness.stack) def __repr__(self) -> str: return repr(self.scriptWitness) def is_null(self) -> bool: return self.scriptWitness.is_null() class CTxWitness(object): def __init__(self) -> None: self.vtxinwit: List[CTxInWitness] = [] def deserialize(self, f: Readable) -> None: for i in range(len(self.vtxinwit)): self.vtxinwit[i].deserialize(f) def serialize(self) -> bytes: r = b"" # This is different than the usual vector serialization -- # we omit the length of the vector, which is required to be # the same length as the transaction's vin vector. for x in self.vtxinwit: r += x.serialize() return r def __repr__(self) -> str: return "CTxWitness(%s)" % \ (';'.join([repr(x) for x in self.vtxinwit])) def is_null(self) -> bool: for x in self.vtxinwit: if not x.is_null(): return False return True class CTransaction(object): def __init__(self, tx: Optional['CTransaction'] = None) -> None: if tx is None: self.nVersion = 1 self.vin: List[CTxIn] = [] self.vout: List[CTxOut] = [] self.wit = CTxWitness() self.nLockTime = 0 self.sha256: Optional[int] = None self.hash: Optional[bytes] = None else: self.nVersion = tx.nVersion self.vin = copy.deepcopy(tx.vin) self.vout = copy.deepcopy(tx.vout) self.nLockTime = tx.nLockTime self.sha256 = tx.sha256 self.hash = tx.hash self.wit = copy.deepcopy(tx.wit) def deserialize(self, f: Readable) -> None: self.nVersion = struct.unpack(" bytes: r = b"" r += struct.pack(" bytes: flags = 0 if not self.wit.is_null(): flags |= 1 r = b"" r += struct.pack(" bytes: return self.serialize_without_witness() # Recalculate the txid (transaction hash without witness) def rehash(self) -> None: self.sha256 = None self.calc_sha256() # We will only cache the serialization without witness in # self.sha256 and self.hash -- those are expected to be the txid. def calc_sha256(self, with_witness: bool = False) -> Optional[int]: if with_witness: # Don't cache the result, just return it return uint256_from_str(hash256(self.serialize_with_witness())) if self.sha256 is None: self.sha256 = uint256_from_str(hash256(self.serialize_without_witness())) self.hash = hash256(self.serialize()) return None def is_null(self) -> bool: return len(self.vin) == 0 and len(self.vout) == 0 def __repr__(self) -> str: return "CTransaction(nVersion=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ % (self.nVersion, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733838905.0 ledger_bitcoin-0.4.0/ledger_bitcoin/wallet.py0000664000175000017500000001204014726044071021161 0ustar00singalasingalaimport re from enum import IntEnum from typing import List, Union from hashlib import sha256 from .common import serialize_str, AddressType, write_varint from .merkle import MerkleTree, element_hash class WalletType(IntEnum): WALLET_POLICY_V1 = 1 WALLET_POLICY_V2 = 2 # should not be instantiated directly class WalletPolicyBase: def __init__(self, name: str, version: WalletType) -> None: self.name = name self.version = version if (version != WalletType.WALLET_POLICY_V1 and version != WalletType.WALLET_POLICY_V2): raise ValueError("Invalid wallet policy version") def serialize(self) -> bytes: return b"".join([ self.version.value.to_bytes(1, byteorder="big"), serialize_str(self.name) ]) @property def id(self) -> bytes: return sha256(self.serialize()).digest() class WalletPolicy(WalletPolicyBase): """ Represents a wallet stored with a wallet policy. For version V2, the wallet is serialized as follows: - 1 byte : wallet version - 1 byte : length of the wallet name (max 64) - (var) : wallet name (ASCII string) - (varint) : length of the descriptor template - 32-bytes : sha256 hash of the descriptor template - (varint) : number of keys (not larger than 252) - 32-bytes : root of the Merkle tree of all the keys information. The specific format of the keys is deferred to subclasses. """ def __init__(self, name: str, descriptor_template: str, keys_info: List[str], version: WalletType = WalletType.WALLET_POLICY_V2): super().__init__(name, version) self.descriptor_template = descriptor_template self.keys_info = keys_info @property def n_keys(self) -> int: return len(self.keys_info) def serialize(self) -> bytes: keys_info_hashes = map( lambda k: element_hash(k.encode()), self.keys_info) descriptor_template_sha256 = sha256( self.descriptor_template.encode()).digest() return b"".join([ super().serialize(), write_varint(len(self.descriptor_template.encode())), self.descriptor_template.encode( ) if self.version == WalletType.WALLET_POLICY_V1 else descriptor_template_sha256, write_varint(len(self.keys_info)), MerkleTree(keys_info_hashes).root ]) def get_descriptor(self, change: Union[bool, None]) -> str: """ Generates a descriptor string based on the wallet's descriptor template and keys. Args: change (bool | None): Indicates whether the descriptor is for a change address. - If None, returns the BIP-389 multipath address for both the receive and change address. - If True, the descriptor is for a change address. - If False, the descriptor is for a non-change address. Returns: str: The generated descriptor. """ desc = self.descriptor_template for i in reversed(range(self.n_keys)): key = self.keys_info[i] desc = desc.replace(f"@{i}", key) # in V1, /** is part of the key; in V2, it's part of the policy map. This handles either if change is not None: desc = desc.replace("/**", f"/{1 if change else 0}/*") else: desc = desc.replace("/**", f"/<0;1>/*") if self.version == WalletType.WALLET_POLICY_V2: # V2, the / syntax is supported. Replace with M if not change, or with N if change if change is not None: desc = re.sub(r"/<(\d+);(\d+)>", "/\\2" if change else "/\\1", desc) return desc class MultisigWallet(WalletPolicy): def __init__(self, name: str, address_type: AddressType, threshold: int, keys_info: List[str], sorted: bool = True, version: WalletType = WalletType.WALLET_POLICY_V2) -> None: n_keys = len(keys_info) if not (1 <= threshold <= n_keys <= 16): raise ValueError("Invalid threshold or number of keys") multisig_op = "sortedmulti" if sorted else "multi" if (address_type == AddressType.LEGACY): policy_prefix = f"sh({multisig_op}(" policy_suffix = f"))" elif address_type == AddressType.WIT: policy_prefix = f"wsh({multisig_op}(" policy_suffix = f"))" elif address_type == AddressType.SH_WIT: policy_prefix = f"sh(wsh({multisig_op}(" policy_suffix = f")))" else: raise ValueError(f"Unexpected address type: {address_type}") key_placeholder_suffix = "/**" if version == WalletType.WALLET_POLICY_V2 else "" descriptor_template = "".join([ policy_prefix, str(threshold) + ",", ",".join("@" + str(l) + key_placeholder_suffix for l in range(n_keys)), policy_suffix ]) super().__init__(name, descriptor_template, keys_info, version) self.threshold = threshold ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741351045.8513703 ledger_bitcoin-0.4.0/ledger_bitcoin.egg-info/0000775000175000017500000000000014762564206021023 5ustar00singalasingala././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741351045.0 ledger_bitcoin-0.4.0/ledger_bitcoin.egg-info/PKG-INFO0000644000175000017500000001350714762564205022123 0ustar00singalasingalaMetadata-Version: 2.2 Name: ledger_bitcoin Version: 0.4.0 Summary: Client for Ledger Nano Bitcoin application Home-page: https://github.com/LedgerHQ/app-bitcoin-new Author: Ledger Author-email: hello@ledger.fr Project-URL: Bug Tracker, https://github.com/LedgerHQ/app-bitcoin-new/issues Classifier: Programming Language :: Python :: 3 Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Requires-Python: >=3.7 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: typing-extensions>=3.7 Requires-Dist: ledgercomm>=1.1.0 Requires-Dist: packaging>=21.3 Provides-Extra: hid Requires-Dist: hidapi>=0.9.0.post3; extra == "hid" # Ledger Bitcoin application client ## Overview Client library for Ledger Bitcoin application. Main repository and documentation: https://github.com/LedgerHQ/app-bitcoin-new ## Install If you just want to communicate through TCP socket (for example with the Speculos emulator), there is no dependency: ```bash $ pip install ledger_bitcoin ``` otherwise, [hidapi](https://github.com/trezor/cython-hidapi) must be installed as an extra dependency: ```bash $ pip install ledger_bitcoin[hid] ``` ## Getting started The main method exported by the library is `createClient`, which queries the hardware wallet for the version of the running app, and then returns the appropriate implementation of the `Client` class. See the documentation of the class and the example below for the supported methods. When running on a legacy version of the app (below version `2.0.0`), only the features that were available on the app are supported. Any unsopported method (e.g.: multisig registration or addresses, taproot addresses) will raise a `NotImplementedError`. ### Running with speculos It is possible to run the app and the library with the [speculos](https://github.com/LedgerHQ/speculos) emulator. ⚠️ Currently, speculos does not correctly emulate the version of the app, always returning a dummy value; in order to use the library, it is necessary to set the `SPECULOS_APPNAME` environment variable before starting speculos, for example with: ``` $ export SPECULOS_APPNAME="Bitcoin Test:2.1.0" ``` Similarly, to test the library behavior on a legacy version of the app, one can set the version to `1.6.5` (the final version of the 1.X series). The expected application name is `Bitcoin` for mainnet, `Bitcoin Test` for testnet. ### Example The following example showcases all the main methods of the `Client`'s interface. If you are not using the context manager syntax when creating the client, remember to call the `stop()` method to release the communication channel. Testing the `sign_psbt` method requires producing a valid PSBT (with any external tool that supports either PSBTv0 or PSBTv2), and provide the corresponding wallet policy; it is skipped by default in the following example. ```python from typing import Optional from ledger_bitcoin import createClient, Chain, MultisigWallet, MultisigWallet, WalletPolicy, AddressType, TransportClient from ledger_bitcoin.psbt import PSBT def main(): # speculos on default host/port # with createClient(TransportClient(), chain=Chain.TEST) as client: # Ledger Nano connected via USB with createClient(chain=Chain.TEST) as client: # ==> Get the master key fingerprint fpr = client.get_master_fingerprint().hex() print(f"Master key fingerprint: {fpr}") # ==> Get and display on screen the first taproot address first_taproot_account_pubkey = client.get_extended_pubkey("m/86'/1'/0'") first_taproot_account_policy = WalletPolicy( "", "tr(@0/**)", [ f"[{fpr}/86'/1'/0']{first_taproot_account_pubkey}" ], ) first_taproot_account_address = client.get_wallet_address( first_taproot_account_policy, None, change=0, address_index=0, display=True # show address on the wallet's screen ) print(f"First taproot account receive address: {first_taproot_account_address}") # ==> Register a multisig wallet named "Cold storage" our_pubkey = client.get_extended_pubkey("m/48'/1'/0'/2'") other_key_info = "[76223a6e/48'/1'/0'/2']tpubDE7NQymr4AFtewpAsWtnreyq9ghkzQBXpCZjWLFVRAvnbf7vya2eMTvT2fPapNqL8SuVvLQdbUbMfWLVDCZKnsEBqp6UK93QEzL8Ck23AwF" multisig_policy = MultisigWallet( name="Cold storage", address_type=AddressType.WIT, threshold=2, keys_info=[ other_key_info, # some other bitcoiner f"[{fpr}/48'/1'/0'/2']{our_pubkey}", # that's us ], ) policy_id, policy_hmac = client.register_wallet(multisig_policy) print(f"Policy hmac: {policy_hmac.hex()}. Store it safely (together with the policy).") assert policy_id == multisig_policy.id # should never fail # ==> Derive and show an address for "Cold storage" multisig_address = client.get_wallet_address(multisig_policy, policy_hmac, change=0, address_index=0, display=True) print(f"Multisig wallet address: {multisig_address}") # ==> Sign a psbt # TODO: set a wallet policy and a valid psbt file in order to test psbt signing psbt_filename: Optional[str] = None signing_policy: Optional[WalletPolicy] = None signing_policy_hmac: Optional[bytes] = None if not psbt_filename or not signing_policy: print("Nothing to sign :(") return raw_psbt_base64 = open(psbt_filename, "r").read() psbt = PSBT() psbt.deserialize(raw_psbt_base64) result = client.sign_psbt(psbt, signing_policy, signing_policy_hmac) print("Returned signatures:") print(result) if __name__ == "__main__": main() ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741351045.0 ledger_bitcoin-0.4.0/ledger_bitcoin.egg-info/SOURCES.txt0000664000175000017500000000460214762564205022710 0ustar00singalasingalaLICENSE README.md pyproject.toml setup.cfg ledger_bitcoin/__init__.py ledger_bitcoin/_base58.py ledger_bitcoin/_script.py ledger_bitcoin/_serialize.py ledger_bitcoin/bip0327.py ledger_bitcoin/client.py ledger_bitcoin/client_base.py ledger_bitcoin/client_command.py ledger_bitcoin/client_legacy.py ledger_bitcoin/command_builder.py ledger_bitcoin/common.py ledger_bitcoin/errors.py ledger_bitcoin/key.py ledger_bitcoin/merkle.py ledger_bitcoin/psbt.py ledger_bitcoin/py.typed ledger_bitcoin/ripemd.py ledger_bitcoin/segwit_addr.py ledger_bitcoin/transport.py ledger_bitcoin/tx.py ledger_bitcoin/wallet.py ledger_bitcoin.egg-info/PKG-INFO ledger_bitcoin.egg-info/SOURCES.txt ledger_bitcoin.egg-info/dependency_links.txt ledger_bitcoin.egg-info/requires.txt ledger_bitcoin.egg-info/top_level.txt ledger_bitcoin/btchip/__init__.py ledger_bitcoin/btchip/bitcoinTransaction.py ledger_bitcoin/btchip/bitcoinVarint.py ledger_bitcoin/btchip/btchip.py ledger_bitcoin/btchip/btchipComm.py ledger_bitcoin/btchip/btchipException.py ledger_bitcoin/btchip/btchipHelpers.py ledger_bitcoin/btchip/btchipUtils.py ledger_bitcoin/btchip/ledgerWrapper.py ledger_bitcoin/embit/__init__.py ledger_bitcoin/embit/base.py ledger_bitcoin/embit/base58.py ledger_bitcoin/embit/bech32.py ledger_bitcoin/embit/bip32.py ledger_bitcoin/embit/compact.py ledger_bitcoin/embit/ec.py ledger_bitcoin/embit/hashes.py ledger_bitcoin/embit/misc.py ledger_bitcoin/embit/networks.py ledger_bitcoin/embit/script.py ledger_bitcoin/embit/descriptor/__init__.py ledger_bitcoin/embit/descriptor/arguments.py ledger_bitcoin/embit/descriptor/base.py ledger_bitcoin/embit/descriptor/checksum.py ledger_bitcoin/embit/descriptor/descriptor.py ledger_bitcoin/embit/descriptor/errors.py ledger_bitcoin/embit/descriptor/miniscript.py ledger_bitcoin/embit/descriptor/taptree.py ledger_bitcoin/embit/util/__init__.py ledger_bitcoin/embit/util/ctypes_secp256k1.py ledger_bitcoin/embit/util/key.py ledger_bitcoin/embit/util/py_ripemd160.py ledger_bitcoin/embit/util/py_secp256k1.py ledger_bitcoin/embit/util/secp256k1.py ledger_bitcoin/exception/__init__.py ledger_bitcoin/exception/device_exception.py ledger_bitcoin/exception/errors.py tests/test_client_legacy.py tests/test_get_extended_pubkey_legacyapp.py tests/test_get_master_fingerprint_legacyapp.py tests/test_get_wallet_address_legacyapp.py tests/test_ripemd160.py tests/test_sign_message_legacyapp.py tests/test_sign_psbt_legacyapp.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741351045.0 ledger_bitcoin-0.4.0/ledger_bitcoin.egg-info/dependency_links.txt0000664000175000017500000000000114762564205025070 0ustar00singalasingala ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741351045.0 ledger_bitcoin-0.4.0/ledger_bitcoin.egg-info/requires.txt0000664000175000017500000000012414762564205023417 0ustar00singalasingalatyping-extensions>=3.7 ledgercomm>=1.1.0 packaging>=21.3 [hid] hidapi>=0.9.0.post3 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741351045.0 ledger_bitcoin-0.4.0/ledger_bitcoin.egg-info/top_level.txt0000664000175000017500000000001714762564205023552 0ustar00singalasingalaledger_bitcoin ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716449527.0 ledger_bitcoin-0.4.0/pyproject.toml0000664000175000017500000000023714623570367017277 0ustar00singalasingala[build-system] requires = [ "ledgercomm>=1.1.0", "setuptools>=42", "typing-extensions>=3.7", "wheel" ] build-backend = "setuptools.build_meta" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741351045.8523705 ledger_bitcoin-0.4.0/setup.cfg0000664000175000017500000000152114762564206016200 0ustar00singalasingala[metadata] name = ledger_bitcoin version = attr: ledger_bitcoin.__version__ author = Ledger author_email = hello@ledger.fr description = Client for Ledger Nano Bitcoin application long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/LedgerHQ/app-bitcoin-new project_urls = Bug Tracker = https://github.com/LedgerHQ/app-bitcoin-new/issues classifiers = Programming Language :: Python :: 3 License :: OSI Approved :: Apache Software License Operating System :: OS Independent [options] packages = find: python_requires = >=3.7 install_requires = typing-extensions>=3.7 ledgercomm>=1.1.0 packaging>=21.3 [options.package_data] * = py.typed [options.extras_require] hid = hidapi>=0.9.0.post3 [options.packages.find] exclude = tests tests.utils [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1741351045.8513703 ledger_bitcoin-0.4.0/tests/0000775000175000017500000000000014762564206015522 5ustar00singalasingala././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698934586.0 ledger_bitcoin-0.4.0/tests/test_client_legacy.py0000664000175000017500000000065714520727472021743 0ustar00singalasingalafrom pathlib import Path from bitcoin_client.ledger_bitcoin import Client from bitcoin_client.ledger_bitcoin.client_legacy import LegacyClient tests_root: Path = Path(__file__).parent def test_client_legacy(client: Client): # tests that the library correctly instatiates the LegacyClient and not the new one, # since the version of the app binary being tested is an old one assert isinstance(client, LegacyClient) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/tests/test_get_extended_pubkey_legacyapp.py0000664000175000017500000000241114506542032025161 0ustar00singalasingalafrom bitcoin_client.ledger_bitcoin import Client def test_get_extended_pubkey_standard_nodisplay(client: Client): testcases = { "m/44'/1'/0'": "tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT", "m/44'/1'/10'": "tpubDCwYjpDhUdPGp21gSpVay2QPJVh6WNySWMXPhbcu1DsxH31dF7mY18oibbu5RxCLBc1Szerjscuc3D5HyvfYqfRvc9mesewnFqGmPjney4d", "m/44'/1'/2'/1/42": "tpubDGF9YgHKv6qh777rcqVhpmDrbNzgophJM9ec7nHiSfrbss7fVBXoqhmZfohmJSvhNakDHAspPHjVVNL657tLbmTXvSeGev2vj5kzjMaeupT", "m/48'/1'/4'/1'/0/7": "tpubDK8WPFx4WJo1R9mEL7Wq325wBiXvkAe8ipgb9Q1QBDTDUD2YeCfutWtzY88NPokZqJyRPKHLGwTNLT7jBG59aC6VH8q47LDGQitPB6tX2d7", "m/49'/1'/1'/1/3": "tpubDGnetmJDCL18TyaaoyRAYbkSE9wbHktSdTS4mfsR6inC8c2r6TjdBt3wkqEQhHYPtXpa46xpxDaCXU2PRNUGVvDzAHPG6hHRavYbwAGfnFr", "m/84'/1'/2'/0/10": "tpubDG9YpSUwScWJBBSrhnAT47NcT4NZGLcY18cpkaiWHnkUCi19EtCh8Heeox268NaFF6o56nVeSXuTyK6jpzTvV1h68Kr3edA8AZp27MiLUNt", "m/86'/1'/4'/1/12": "tpubDHTZ815MvTaRmo6Qg1rnU6TEU4ZkWyA56jA1UgpmMcBGomnSsyo34EZLoctzZY9MTJ6j7bhccceUeXZZLxZj5vgkVMYfcZ7DNPsyRdFpS3f", } for path, pubkey in testcases.items(): assert pubkey == client.get_extended_pubkey( path=path, display=False ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/tests/test_get_master_fingerprint_legacyapp.py0000664000175000017500000000040314506542032025703 0ustar00singalasingalafrom bitcoin_client.ledger_bitcoin import Client from .conftest import SpeculosGlobals def test_get_master_fingerprint(client: Client, speculos_globals: SpeculosGlobals): assert client.get_master_fingerprint() == speculos_globals.master_key_fingerprint ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698934586.0 ledger_bitcoin-0.4.0/tests/test_get_wallet_address_legacyapp.py0000664000175000017500000000343714520727472025021 0ustar00singalasingalafrom bitcoin_client.ledger_bitcoin import Client, WalletPolicy def test_get_wallet_address_singlesig_legacy(client: Client): # legacy address (P2PKH) wallet = WalletPolicy( name="", descriptor_template="pkh(@0/**)", keys_info=[ f"[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT", ], ) assert client.get_wallet_address(wallet, None, 0, 0, False) == "mz5vLWdM1wHVGSmXUkhKVvZbJ2g4epMXSm" assert client.get_wallet_address(wallet, None, 1, 15, False) == "myFCUBRCKFjV7292HnZtiHqMzzHrApobpT" def test_get_wallet_address_singlesig_wit(client: Client): # bech32 address (P2WPKH) wallet = WalletPolicy( name="", descriptor_template="wpkh(@0/**)", keys_info=[ f"[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P", ], ) assert client.get_wallet_address(wallet, None, 0, 0, False) == "tb1qzdr7s2sr0dwmkwx033r4nujzk86u0cy6fmzfjk" assert client.get_wallet_address(wallet, None, 1, 15, False) == "tb1qlrvzyx8jcjfj2xuy69du9trtxnsvjuped7e289" def test_get_wallet_address_singlesig_sh_wit(client: Client): # wrapped segwit addresses (P2SH-P2WPKH) wallet = WalletPolicy( name="", descriptor_template="sh(wpkh(@0/**))", keys_info=[ f"[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3", ], ) assert client.get_wallet_address(wallet, None, 0, 0, False) == "2MyHkbusvLomaarGYMqyq7q9pSBYJRwWcsw" assert client.get_wallet_address(wallet, None, 1, 15, False) == "2NAbM4FSeBQG4o85kbXw2YNfKypcnEZS9MR" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1698934586.0 ledger_bitcoin-0.4.0/tests/test_ripemd160.py0000664000175000017500000000203214520727472020635 0ustar00singalasingalafrom bitcoin_client.ledger_bitcoin.ripemd import ripemd160 def test_ripemd160(): """RIPEMD-160 test vectors.""" # See https://homes.esat.kuleuven.be/~bosselae/ripemd160.html for msg, hexout in [ (b"", "9c1185a5c5e9fc54612808977ee8f548b2258d31"), (b"a", "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe"), (b"abc", "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc"), (b"message digest", "5d0689ef49d2fae572b881b123a85ffa21595f36"), (b"abcdefghijklmnopqrstuvwxyz", "f71c27109c692c1b56bbdceb5b9d2865b3708dbc"), ( b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "12a053384a9c0c88e405a06c27dcf49ada62eb2b", ), ( b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", "b0e20b6e3116640286ed3a87a5713079b21f5189", ), (b"1234567890" * 8, "9b752e45573d4b39f4dbd3323cab82bf63326bfb"), (b"a" * 1000000, "52783243c1697bdbe16d37f97f68f08325dc1528"), ]: assert ripemd160(msg).hex() == hexout ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1696252954.0 ledger_bitcoin-0.4.0/tests/test_sign_message_legacyapp.py0000664000175000017500000000067714506542032023623 0ustar00singalasingalafrom bitcoin_client.ledger_bitcoin import Client from test_utils import has_automation @has_automation("automations/sign_message.json") def test_sign_message(client: Client): msg = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks." path = "m/44'/1'/0'/0/0" result = client.sign_message(msg, path) assert result == "IOR4YRVlmJGMx+H7PgQvHzWAF0HAgrUggQeRdnoWKpypfaAberpvF+XbOCM5Cd/ljogNyU3w2OIL8eYCyZ6Ru2k=" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728985602.0 ledger_bitcoin-0.4.0/tests/test_sign_psbt_legacyapp.py0000664000175000017500000001260014703435002023131 0ustar00singalasingalafrom pathlib import Path from bitcoin_client.ledger_bitcoin import Client, WalletPolicy, PartialSignature from bitcoin_client.ledger_bitcoin.psbt import PSBT from test_utils import has_automation tests_root: Path = Path(__file__).parent CURRENCY_TICKER = "TEST" def open_psbt_from_file(filename: str) -> PSBT: raw_psbt_base64 = open(filename, "r").read() psbt = PSBT() psbt.deserialize(raw_psbt_base64) return psbt @has_automation("automations/sign_with_wallet_accept.json") def test_sign_psbt_singlesig_pkh_1to1(client: Client): # PSBT for a legacy 1-input 1-output spend (no change address) psbt = open_psbt_from_file(f"{tests_root}/psbt/singlesig/pkh-1to1.psbt") wallet = WalletPolicy( "", "pkh(@0/**)", [ "[f5acc2fd/44'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT" ], ) # expected sigs: # #0: # "pubkey" : "02ee8608207e21028426f69e76447d7e3d5e077049f5e683c3136c2314762a4718", # "signature" : "3045022100e55b3ca788721aae8def2eadff710e524ffe8c9dec1764fdaa89584f9726e196022012a30fbcf9e1a24df31a1010356b794ab8de438b4250684757ed5772402540f401" result = client.sign_psbt(psbt, wallet, None) assert result == [( 0, PartialSignature( pubkey=bytes.fromhex("02ee8608207e21028426f69e76447d7e3d5e077049f5e683c3136c2314762a4718"), signature=bytes.fromhex( "3045022100e55b3ca788721aae8def2eadff710e524ffe8c9dec1764fdaa89584f9726e196022012a30fbcf9e1a24df31a1010356b794ab8de438b4250684757ed5772402540f401" ) ) )] @has_automation("automations/sign_with_wallet_accept.json") def test_sign_psbt_singlesig_sh_wpkh_1to2(client: Client): # PSBT for a wrapped segwit 1-input 2-output spend (1 change address) psbt = open_psbt_from_file(f"{tests_root}/psbt/singlesig/sh-wpkh-1to2.psbt") wallet = WalletPolicy( "", "sh(wpkh(@0/**))", [ "[f5acc2fd/49'/1'/0']tpubDC871vGLAiKPcwAw22EjhKVLk5L98UGXBEcGR8gpcigLQVDDfgcYW24QBEyTHTSFEjgJgbaHU8CdRi9vmG4cPm1kPLmZhJEP17FMBdNheh3" ], ) # expected sigs: # #0: # "pubkey" : "024ba3b77d933de9fa3f9583348c40f3caaf2effad5b6e244ece8abbfcc7244f67", # "signature" : "30440220720722b08489c2a50d10edea8e21880086c8e8f22889a16815e306daeea4665b02203fcf453fa490b76cf4f929714065fc90a519b7b97ab18914f9451b5a4b45241201" result = client.sign_psbt(psbt, wallet, None) assert len(result) == 1 assert result == [( 0, PartialSignature( pubkey=bytes.fromhex("024ba3b77d933de9fa3f9583348c40f3caaf2effad5b6e244ece8abbfcc7244f67"), signature=bytes.fromhex( "30440220720722b08489c2a50d10edea8e21880086c8e8f22889a16815e306daeea4665b02203fcf453fa490b76cf4f929714065fc90a519b7b97ab18914f9451b5a4b45241201" ) ) )] @has_automation("automations/sign_with_wallet_accept.json") def test_sign_psbt_singlesig_wpkh_1to2(client: Client): # PSBT for a legacy 1-input 2-output spend (1 change address) psbt = open_psbt_from_file(f"{tests_root}/psbt/singlesig/wpkh-1to2.psbt") wallet = WalletPolicy( "", "wpkh(@0/**)", [ "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P" ], ) result = client.sign_psbt(psbt, wallet, None) # expected sigs # #0: # "pubkey" : "03ee2c3d98eb1f93c0a1aa8e5a4009b70eb7b44ead15f1666f136b012ad58d3068", # "signature" : "3045022100ab44f34dd7e87c9054591297a101e8500a0641d1d591878d0d23cf8096fa79e802205d12d1062d925e27b57bdcf994ecf332ad0a8e67b8fe407bab2101255da632aa01" assert result == [( 0, PartialSignature( pubkey=bytes.fromhex("03ee2c3d98eb1f93c0a1aa8e5a4009b70eb7b44ead15f1666f136b012ad58d3068"), signature=bytes.fromhex( "3045022100ab44f34dd7e87c9054591297a101e8500a0641d1d591878d0d23cf8096fa79e802205d12d1062d925e27b57bdcf994ecf332ad0a8e67b8fe407bab2101255da632aa01" ) ) )] @has_automation("automations/sign_with_wallet_accept.json") def test_sign_psbt_singlesig_wpkh_2to2(client: Client): # PSBT for a legacy 2-input 2-output spend (1 change address) psbt = open_psbt_from_file(f"{tests_root}/psbt/singlesig/wpkh-2to2.psbt") wallet = WalletPolicy( "", "wpkh(@0/**)", [ "[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P" ], ) result = client.sign_psbt(psbt, wallet, None) assert result == [( 0, PartialSignature( pubkey=bytes.fromhex("03455ee7cedc97b0ba435b80066fc92c963a34c600317981d135330c4ee43ac7a3"), signature=bytes.fromhex( "304402206b3e877655f08c6e7b1b74d6d893a82cdf799f68a5ae7cecae63a71b0339e5ce022019b94aa3fb6635956e109f3d89c996b1bfbbaf3c619134b5a302badfaf52180e01" ) ) ), ( 1, PartialSignature( pubkey=bytes.fromhex("0271b5b779ad870838587797bcf6f0c7aec5abe76a709d724f48d2e26cf874f0a0"), signature=bytes.fromhex( "3045022100e2e98e4f8c70274f10145c89a5d86e216d0376bdf9f42f829e4315ea67d79d210220743589fd4f55e540540a976a5af58acd610fa5e188a5096dfe7d36baf3afb94001" ) ) )]