diff --git a/.pyrightconfig.json b/.pyrightconfig.json new file mode 100644 index 0000000..d4c038a --- /dev/null +++ b/.pyrightconfig.json @@ -0,0 +1,26 @@ +{ + "include": [ + "bitcoinutils" + ], + "exclude": [ + "**/node_modules", + "**/__pycache__", + "**/.*" + ], + "ignore": [], + "defineConstant": { + "DEBUG": true + }, + "stubPath": "./stubs", + "typeCheckingMode": "basic", + "useLibraryCodeForTypes": false, + "reportMissingImports": "warning", + "reportMissingTypeStubs": "warning", + "reportUnknownMemberType": "warning", + "reportUnknownVariableType": "warning", + "reportUnknownArgumentType": "warning", + "reportPrivateUsage": "warning", + "reportGeneralTypeIssues": "warning", + "pythonVersion": "3.7", + "pythonPlatform": "All" + } \ No newline at end of file diff --git a/bitcoinutils/proxy.py b/bitcoinutils/proxy.py index e4a7e69..b4f0d28 100644 --- a/bitcoinutils/proxy.py +++ b/bitcoinutils/proxy.py @@ -9,20 +9,61 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. -from typing import Optional -from bitcoinrpc.authproxy import AuthServiceProxy # type: ignore +from __future__ import annotations +from typing import Optional, Any, Dict, List, Union, Tuple, Callable, TypeVar, cast, overload + +# For type checking, we need to handle the imported library +from bitcoinrpc.authproxy import AuthServiceProxy from bitcoinutils.setup import get_network from bitcoinutils.constants import NETWORK_DEFAULT_PORTS +T = TypeVar('T') +JSONDict = Dict[str, Any] +JSONList = List[Any] +JSONValue = Union[str, int, float, bool, None, JSONDict, JSONList] + + +class RPCError(Exception): + """Exception raised for errors when interfacing with the Bitcoin node. + + Attributes: + message -- explanation of the error + code -- error code returned by the node + """ + + def __init__(self, message: str, code: Optional[int] = None): + self.message = message + self.code = code + super().__init__(f"RPC Error ({code}): {message}" if code else message) + + class NodeProxy: - """Simple Bitcoin node proxy that can call all of Bitcoin's JSON-RPC functionality. + """Bitcoin node proxy that can call all of Bitcoin's JSON-RPC functionality. + + This class provides a convenient interface to interact with a Bitcoin node using + the JSON-RPC protocol. It supports all methods available in Bitcoin Core and + handles authentication, connection management, and error handling. Attributes ---------- proxy : object - a bitcoinrpc AuthServiceProxy object + an instance of bitcoinrpc.authproxy.AuthServiceProxy + + Methods + ------- + call(method, *params) + Calls any RPC method with provided parameters + get_blockchain_info() + Returns information about the blockchain + get_network_info() + Returns information about the network + get_wallet_info() + Returns information about the wallet + get_new_address(label="", address_type=None) + Generates a new address + ... (other methods) """ def __init__( @@ -31,39 +72,768 @@ def __init__( rpcpassword: str, host: Optional[str] = None, port: Optional[int] = None, + timeout: int = 30, + use_https: bool = False, ) -> None: - """Connects to node using credentials given + """Connects to a Bitcoin node using provided credentials. Parameters ---------- rpcuser : str - as defined in bitcoin.conf + RPC username as defined in bitcoin.conf rpcpassword : str - as defined in bitcoin.conf + RPC password as defined in bitcoin.conf host : str, optional - host where the Bitcoin node resides; defaults to 127.0.0.1 + Host where the Bitcoin node resides; defaults to 127.0.0.1 port : int, optional - port to connect to; uses default ports according to network + Port to connect to; uses default ports according to network + timeout : int, optional + Timeout for RPC calls in seconds; defaults to 30 + use_https : bool, optional + Whether to use HTTPS for the connection; defaults to False Raises ------ ValueError - if rpcuser and/or rpcpassword are not specified + If rpcuser and/or rpcpassword are not specified """ - if not rpcuser or not rpcpassword: raise ValueError("rpcuser or rpcpassword is missing") if not host: host = "127.0.0.1" - if not port: port = NETWORK_DEFAULT_PORTS[get_network()] - self.proxy = AuthServiceProxy( - "http://{}:{}@{}:{}".format(rpcuser, rpcpassword, host, port) - ) + protocol = "https" if use_https else "http" + service_url = f"{protocol}://{rpcuser}:{rpcpassword}@{host}:{port}" + + self.proxy = AuthServiceProxy(service_url, timeout=timeout) + + def __call__(self, method: str, *params: Any) -> Any: + """Directly call any Bitcoin Core RPC method. + + This is a convenience method that allows calling RPC methods directly + through the NodeProxy instance. + + Parameters + ---------- + method : str + The RPC method name + *params : Any + Parameters to pass to the RPC method + + Returns + ------- + Any + The result of the RPC call + + Raises + ------ + RPCError + If the RPC call fails + """ + return self.call(method, *params) + + def call(self, method: str, *params: Any) -> Any: + """Call any Bitcoin Core RPC method. + + Parameters + ---------- + method : str + The RPC method name + *params : Any + Parameters to pass to the RPC method + + Returns + ------- + Any + The result of the RPC call + + Raises + ------ + RPCError + If the RPC call fails + """ + try: + rpc_method = getattr(self.proxy, method) + return rpc_method(*params) + except Exception as e: + # Extract error code if available + error_code = None + if hasattr(e, 'error') and 'code' in e.error: + error_code = e.error['code'] + + raise RPCError(str(e), error_code) + + # Convenience methods for common RPC calls + # Blockchain methods + def get_blockchain_info(self) -> JSONDict: + """Get information about the blockchain. + + Returns + ------- + dict + Information about the blockchain including the current + blockchain height, difficulty, chain (main/test/regtest), etc. + """ + result = self.call('getblockchaininfo') + return cast(JSONDict, result) + + def get_block_count(self) -> int: + """Get the current block height of the blockchain. + + Returns + ------- + int + The current block height + """ + result = self.call('getblockcount') + return cast(int, result) + + def get_block_hash(self, height: int) -> str: + """Get the block hash for a specific height. + + Parameters + ---------- + height : int + The block height + + Returns + ------- + str + The block hash + """ + result = self.call('getblockhash', height) + return cast(str, result) + + def get_block(self, block_hash: str, verbosity: int = 1) -> Union[str, JSONDict]: + """Get block data for a specific block hash. + + Parameters + ---------- + block_hash : str + The block hash + verbosity : int, optional + 0 for hex-encoded data, 1 for a JSON object, 2 for JSON object with transaction data + + Returns + ------- + Union[str, dict] + Block data as hex string (verbosity=0) or JSON object (verbosity>0) + """ + result = self.call('getblock', block_hash, verbosity) + if verbosity == 0: + return cast(str, result) + return cast(JSONDict, result) + + def get_difficulty(self) -> float: + """Get the current network difficulty. + + Returns + ------- + float + The current difficulty + """ + result = self.call('getdifficulty') + return cast(float, result) + + def get_chain_tips(self) -> List[JSONDict]: + """Get information about chain tips. + + Returns + ------- + List[dict] + List of chain tips + """ + result = self.call('getchaintips') + return cast(List[JSONDict], result) + + # Wallet methods + def get_balance(self, dummy: str = "*", minconf: int = 0, include_watchonly: bool = False) -> float: + """Get the total balance of the wallet. + + Parameters + ---------- + dummy : str, optional + Remains for backward compatibility (must be "*" for selection of all wallets) + minconf : int, optional + Minimum number of confirmations + include_watchonly : bool, optional + Whether to include watch-only addresses + + Returns + ------- + float + The wallet balance in BTC + """ + result = self.call('getbalance', dummy, minconf, include_watchonly) + return cast(float, result) + + def get_wallet_info(self) -> JSONDict: + """Get information about the wallet. + + Returns + ------- + dict + Information about the wallet + """ + result = self.call('getwalletinfo') + return cast(JSONDict, result) + + def get_new_address(self, label: str = "", address_type: Optional[str] = None) -> str: + """Generate a new address. + + Parameters + ---------- + label : str, optional + A label for the address + address_type : str, optional + The address type (legacy, p2sh-segwit, bech32, or null for default) + + Returns + ------- + str + The new address + """ + if address_type: + result = self.call('getnewaddress', label, address_type) + else: + result = self.call('getnewaddress', label) + return cast(str, result) + + def get_raw_change_address(self, address_type: Optional[str] = None) -> str: + """Generate a new address for receiving change. + + Parameters + ---------- + address_type : str, optional + The address type (legacy, p2sh-segwit, bech32, or null for default) + + Returns + ------- + str + The new change address + """ + if address_type: + result = self.call('getrawchangeaddress', address_type) + else: + result = self.call('getrawchangeaddress') + return cast(str, result) + + def list_unspent( + self, + minconf: int = 1, + maxconf: int = 9999999, + addresses: Optional[List[str]] = None, + include_unsafe: bool = True, + query_options: Optional[JSONDict] = None + ) -> List[JSONDict]: + """Get a list of unspent transaction outputs. + + Parameters + ---------- + minconf : int, optional + Minimum number of confirmations + maxconf : int, optional + Maximum number of confirmations + addresses : List[str], optional + Filter by addresses + include_unsafe : bool, optional + Include outputs that are not safe to spend + query_options : dict, optional + Additional query options + + Returns + ------- + List[dict] + List of unspent transaction outputs + """ + if addresses is None: + addresses = [] + + if query_options: + result = self.call('listunspent', minconf, maxconf, addresses, include_unsafe, query_options) + else: + result = self.call('listunspent', minconf, maxconf, addresses, include_unsafe) + return cast(List[JSONDict], result) + + def list_transactions( + self, + label: str = "*", + count: int = 10, + skip: int = 0, + include_watchonly: bool = False + ) -> List[JSONDict]: + """Get a list of wallet transactions. + + Parameters + ---------- + label : str, optional + Label to filter transactions + count : int, optional + Number of transactions to return + skip : int, optional + Number of transactions to skip + include_watchonly : bool, optional + Whether to include watch-only addresses + + Returns + ------- + List[dict] + List of wallet transactions + """ + result = self.call('listtransactions', label, count, skip, include_watchonly) + return cast(List[JSONDict], result) + + def get_transaction(self, txid: str, include_watchonly: bool = False) -> JSONDict: + """Get detailed information about a transaction. + + Parameters + ---------- + txid : str + The transaction ID + include_watchonly : bool, optional + Whether to include watch-only addresses + + Returns + ------- + dict + Detailed information about the transaction + """ + result = self.call('gettransaction', txid, include_watchonly) + return cast(JSONDict, result) + + # Network methods + def get_network_info(self) -> JSONDict: + """Get information about the network. + + Returns + ------- + dict + Information about the network + """ + result = self.call('getnetworkinfo') + return cast(JSONDict, result) + + def get_peer_info(self) -> List[JSONDict]: + """Get information about connected peers. + + Returns + ------- + List[dict] + List of connected peers + """ + result = self.call('getpeerinfo') + return cast(List[JSONDict], result) + + def get_node_addresses(self, count: int = 1) -> List[JSONDict]: + """Get known addresses for network nodes. + + Parameters + ---------- + count : int, optional + The number of addresses to return + + Returns + ------- + List[dict] + List of node addresses + """ + result = self.call('getnodeaddresses', count) + return cast(List[JSONDict], result) + + def get_net_totals(self) -> JSONDict: + """Get network traffic statistics. + + Returns + ------- + dict + Network traffic statistics + """ + result = self.call('getnettotals') + return cast(JSONDict, result) + + # Transaction methods + def create_raw_transaction( + self, + inputs: List[JSONDict], + outputs: Union[JSONDict, List[JSONDict]], + locktime: int = 0, + replaceable: bool = False + ) -> str: + """Create a raw transaction without signing it. + + Parameters + ---------- + inputs : List[dict] + List of transaction inputs + outputs : Union[dict, List[dict]] + Dictionary with addresses as keys and amounts as values, or a list of outputs + locktime : int, optional + Transaction locktime + replaceable : bool, optional + Whether the transaction is replaceable (BIP125) + + Returns + ------- + str + The hex-encoded raw transaction + """ + result = self.call('createrawtransaction', inputs, outputs, locktime, replaceable) + return cast(str, result) + + def sign_raw_transaction_with_wallet( + self, + hex_string: str, + prev_txs: Optional[List[JSONDict]] = None, + sighash_type: str = "ALL" + ) -> JSONDict: + """Sign a raw transaction with the keys in the wallet. + + Parameters + ---------- + hex_string : str + The hex-encoded raw transaction + prev_txs : List[dict], optional + Previous transactions being spent + sighash_type : str, optional + Signature hash type + + Returns + ------- + dict + The signed transaction + """ + if prev_txs: + result = self.call('signrawtransactionwithwallet', hex_string, prev_txs, sighash_type) + else: + result = self.call('signrawtransactionwithwallet', hex_string) + return cast(JSONDict, result) + + def send_raw_transaction(self, hex_string: str, max_fee_rate: Optional[float] = None) -> str: + """Submit a raw transaction to the network. + + Parameters + ---------- + hex_string : str + The hex-encoded raw transaction + max_fee_rate : float, optional + Reject transactions with a fee rate higher than this (in BTC/kB) + + Returns + ------- + str + The transaction hash + """ + if max_fee_rate is not None: + result = self.call('sendrawtransaction', hex_string, max_fee_rate) + else: + result = self.call('sendrawtransaction', hex_string) + return cast(str, result) + + def decode_raw_transaction(self, hex_string: str, is_witness: bool = True) -> JSONDict: + """Decode a raw transaction. + + Parameters + ---------- + hex_string : str + The hex-encoded raw transaction + is_witness : bool, optional + Whether the transaction is in witness format + + Returns + ------- + dict + The decoded transaction + """ + result = self.call('decoderawtransaction', hex_string, is_witness) + return cast(JSONDict, result) + + def get_raw_transaction(self, txid: str, verbose: bool = False, blockhash: Optional[str] = None) -> Union[str, JSONDict]: + """Get a raw transaction. + + Parameters + ---------- + txid : str + The transaction ID + verbose : bool, optional + Whether to return detailed information + blockhash : str, optional + The block hash in which to look for the transaction + + Returns + ------- + Union[str, dict] + The raw transaction as hex string or a JSON object if verbose=True + """ + if blockhash: + result = self.call('getrawtransaction', txid, verbose, blockhash) + else: + result = self.call('getrawtransaction', txid, verbose) + if verbose: + return cast(JSONDict, result) + return cast(str, result) + + def estimate_smart_fee(self, conf_target: int, estimate_mode: str = "CONSERVATIVE") -> JSONDict: + """Estimate the fee for a transaction. + + Parameters + ---------- + conf_target : int + Confirmation target in blocks + estimate_mode : str, optional + Fee estimate mode (UNSET, ECONOMICAL, CONSERVATIVE) + + Returns + ------- + dict + Estimated fee information + """ + result = self.call('estimatesmartfee', conf_target, estimate_mode) + return cast(JSONDict, result) - def get_proxy(self) -> "NodeProxy": - """Returns bitcoinrpc AuthServiceProxy object""" - return self.proxy + # Utility methods + def validate_address(self, address: str) -> JSONDict: + """Validate a Bitcoin address. + + Parameters + ---------- + address : str + The address to validate + + Returns + ------- + dict + Information about the address + """ + result = self.call('validateaddress', address) + return cast(JSONDict, result) + + def get_mempool_info(self) -> JSONDict: + """Get information about the memory pool. + + Returns + ------- + dict + Information about the memory pool + """ + result = self.call('getmempoolinfo') + return cast(JSONDict, result) + + def get_mempool_entry(self, txid: str) -> JSONDict: + """Get mempool data for a transaction. + + Parameters + ---------- + txid : str + The transaction ID + + Returns + ------- + dict + The mempool entry + """ + result = self.call('getmempoolentry', txid) + return cast(JSONDict, result) + + def get_mempool_ancestors(self, txid: str, verbose: bool = False) -> Union[List[str], JSONDict]: + """Get mempool ancestors for a transaction. + + Parameters + ---------- + txid : str + The transaction ID + verbose : bool, optional + Whether to return detailed information + + Returns + ------- + Union[List[str], dict] + List of ancestor transaction IDs or detailed information + """ + result = self.call('getmempoolancestors', txid, verbose) + if verbose: + return cast(JSONDict, result) + return cast(List[str], result) + + def get_mempool_descendants(self, txid: str, verbose: bool = False) -> Union[List[str], JSONDict]: + """Get mempool descendants for a transaction. + + Parameters + ---------- + txid : str + The transaction ID + verbose : bool, optional + Whether to return detailed information + + Returns + ------- + Union[List[str], dict] + List of descendant transaction IDs or detailed information + """ + result = self.call('getmempooldescendants', txid, verbose) + if verbose: + return cast(JSONDict, result) + return cast(List[str], result) + + # Mining methods + def get_mining_info(self) -> JSONDict: + """Get mining information. + + Returns + ------- + dict + Mining information + """ + result = self.call('getmininginfo') + return cast(JSONDict, result) + + def get_block_template(self, template_request: Optional[JSONDict] = None) -> JSONDict: + """Get block template for miners. + + Parameters + ---------- + template_request : dict, optional + Template request parameters + + Returns + ------- + dict + Block template information + """ + if template_request: + result = self.call('getblocktemplate', template_request) + else: + result = self.call('getblocktemplate') + return cast(JSONDict, result) + + def generate_to_address(self, nblocks: int, address: str, max_tries: int = 1000000) -> List[str]: + """Generate blocks to a specific address. + + Parameters + ---------- + nblocks : int + Number of blocks to generate + address : str + The address to send the newly generated bitcoin to + max_tries : int, optional + Maximum number of tries + + Returns + ------- + List[str] + List of block hashes + """ + result = self.call('generatetoaddress', nblocks, address, max_tries) + return cast(List[str], result) + + # Wallet management methods + def create_wallet( + self, + wallet_name: str, + disable_private_keys: bool = False, + blank: bool = False, + passphrase: str = "", + avoid_reuse: bool = False, + descriptors: Optional[bool] = None, + load_on_startup: bool = False + ) -> JSONDict: + """Create a new wallet. + + Parameters + ---------- + wallet_name : str + The name of the new wallet + disable_private_keys : bool, optional + Whether to disable private keys + blank : bool, optional + Whether to create a blank wallet + passphrase : str, optional + The wallet passphrase + avoid_reuse : bool, optional + Whether to avoid address reuse + descriptors : bool, optional + Whether to create a descriptor wallet + load_on_startup : bool, optional + Whether to load the wallet on startup + + Returns + ------- + dict + Information about the created wallet + """ + args = [wallet_name, disable_private_keys, blank, passphrase, avoid_reuse] + + # Handle optional parameters for newer Bitcoin Core versions + if descriptors is not None: + args.append(descriptors) + if load_on_startup is not None: + args.append(load_on_startup) + + result = self.call('createwallet', *args) + return cast(JSONDict, result) + + def list_wallets(self) -> List[str]: + """List available wallets. + + Returns + ------- + List[str] + List of wallet names + """ + result = self.call('listwallets') + return cast(List[str], result) + + def load_wallet(self, filename: str, load_on_startup: Optional[bool] = None) -> JSONDict: + """Load a wallet. + + Parameters + ---------- + filename : str + The wallet filename + load_on_startup : bool, optional + Whether to load the wallet on startup + + Returns + ------- + dict + Information about the loaded wallet + """ + if load_on_startup is not None: + result = self.call('loadwallet', filename, load_on_startup) + else: + result = self.call('loadwallet', filename) + return cast(JSONDict, result) + + def unload_wallet(self, wallet_name: str = "") -> JSONDict: + """Unload a wallet. + + Parameters + ---------- + wallet_name : str, optional + The wallet name to unload + + Returns + ------- + dict + Result of the unload operation + """ + if wallet_name: + result = self.call('unloadwallet', wallet_name) + else: + result = self.call('unloadwallet') + return cast(JSONDict, result) + + # For backwards compatibility + def get_proxy(self) -> Any: + """Returns the AuthServiceProxy object. + + This method is maintained for backwards compatibility. + + Returns + ------- + AuthServiceProxy + The AuthServiceProxy object + """ + return self.proxy \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 5db5b9b..09de363 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,10 +1,10 @@ .. Bitcoin Utilities documentation master file, created by - sphinx-quickstart on Fri Nov 2 15:36:39 2018. + sphinx-quickstart on Fri Nov 2 15:36:39 2018. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to Bitcoin Utilities's documentation! -============================================= +============================================ Contents: @@ -15,12 +15,18 @@ Contents: usage/transactions usage/script usage/proxy - - + usage/addresses + usage/segwit + usage/hdwallet + usage/bech32 + usage/block + usage/schnorr + usage/constants + usage/utils + Indices and tables ================== * :ref:`genindex` * :ref:`modindex` -* :ref:`search` - +* :ref:`search` \ No newline at end of file diff --git a/docs/usage/addresses.rst b/docs/usage/addresses.rst new file mode 100644 index 0000000..c925e89 --- /dev/null +++ b/docs/usage/addresses.rst @@ -0,0 +1,356 @@ +Address Object +============== + +The Address object in the Python Bitcoin Utils library provides a unified interface for working with different Bitcoin address formats. It supports creation, validation, conversion, and management of various address types including legacy addresses, SegWit addresses, and Taproot addresses. + +Address Types Supported +---------------------- + +The library supports the following address types: + +1. **P2PKH (Pay to Public Key Hash)**: Legacy Bitcoin addresses +2. **P2SH (Pay to Script Hash)**: Script hash addresses +3. **P2WPKH (Pay to Witness Public Key Hash)**: Native SegWit v0 addresses for single signatures +4. **P2WSH (Pay to Witness Script Hash)**: Native SegWit v0 addresses for scripts +5. **P2TR (Pay to Taproot)**: Taproot addresses (SegWit v1) +6. **P2SH-P2WPKH**: Nested SegWit addresses for single signatures +7. **P2SH-P2WSH**: Nested SegWit addresses for scripts + +Class Hierarchy +-------------- + +The Address hierarchy is organized as follows: + +.. code-block:: + + Address (base class) + ├── P2pkhAddress + ├── P2shAddress + └── SegwitAddress (base class) + ├── P2wpkhAddress + ├── P2wshAddress + └── P2trAddress + +Creating Address Objects +---------------------- + +From a Public Key +^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey, PublicKey + + setup('testnet') + + # Generate a private key and get its public key + private_key = PrivateKey() + public_key = private_key.get_public_key() + + # Create different address types from the public key + p2pkh_address = public_key.get_address() + p2wpkh_address = public_key.get_segwit_address() + p2sh_p2wpkh_address = public_key.get_p2sh_p2wpkh_address() + p2tr_address = public_key.get_taproot_address() + + print(f"P2PKH address: {p2pkh_address.to_string()}") + print(f"P2WPKH address: {p2wpkh_address.to_string()}") + print(f"P2SH-P2WPKH address: {p2sh_p2wpkh_address.to_string()}") + print(f"P2TR address: {p2tr_address.to_string()}") + +From a Script +^^^^^^^^^^^ + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PublicKey + from bitcoinutils.script import Script + + setup('testnet') + + # Create public keys + pub1 = PublicKey("pub_key_1_hex") + pub2 = PublicKey("pub_key_2_hex") + + # Create a 2-of-2 multisig script + multisig_script = Script([2, pub1.to_hex(), pub2.to_hex(), 2, 'OP_CHECKMULTISIG']) + + # Create different address types from the script + p2sh_address = multisig_script.get_p2sh_address() + p2wsh_address = multisig_script.get_segwit_address() + p2sh_p2wsh_address = multisig_script.get_p2sh_p2wsh_address() + + print(f"P2SH address: {p2sh_address.to_string()}") + print(f"P2WSH address: {p2wsh_address.to_string()}") + print(f"P2SH-P2WSH address: {p2sh_p2wsh_address.to_string()}") + +From an Address String +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import Address, P2pkhAddress, P2shAddress + from bitcoinutils.keys import P2wpkhAddress, P2wshAddress, P2trAddress + + setup('testnet') + + # Create address objects from string representations + p2pkh = P2pkhAddress('mzF2sbdxcMqKFLoakdBcvZpUXMjgiXGZW1') + p2sh = P2shAddress('2N6Vk58WRh7gQYrRUBZAJAxXC7TKPPpKmDD') + p2wpkh = P2wpkhAddress('tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx') + p2wsh = P2wshAddress('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7') + p2tr = P2trAddress('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6') + +From a Hash160 +^^^^^^^^^^^^ + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import P2pkhAddress, P2shAddress + + setup('testnet') + + # Create address objects from hash160 + p2pkh = P2pkhAddress(hash160='751e76e8199196d454941c45d1b3a323f1433bd6') + p2sh = P2shAddress(hash160='8f55563b9a19f321c211e9b9f38cdf686ea07845') + +Base Address Class Methods +------------------------ + +The base `Address` class provides several methods: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import P2pkhAddress + + setup('testnet') + + # Create an address + addr = P2pkhAddress('mzF2sbdxcMqKFLoakdBcvZpUXMjgiXGZW1') + + # Convert to string + addr_str = addr.to_string() + + # Get hash160 + hash160 = addr.to_hash160() + + # Get script pubkey + script_pubkey = addr.to_script_pub_key() + + # Get address type + addr_type = addr.get_type() + +SegWit Address Base Class Methods +------------------------------- + +The `SegwitAddress` base class provides additional methods specific to SegWit addresses: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import P2wpkhAddress, P2trAddress + + setup('testnet') + + # Create a segwit address + segwit_addr = P2wpkhAddress('tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx') + + # Get witness program + witness_program = segwit_addr.to_witness_program() + + # Create a taproot address + taproot_addr = P2trAddress('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6') + + # Check if y-coordinate is odd (P2TR addresses only) + is_odd = taproot_addr.is_odd() + +Address Creation Methods in Other Classes +--------------------------------------- + +The library also provides convenient methods to create address objects from other objects: + +From Public Key +^^^^^^^^^^^^^ + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey + + setup('testnet') + + # Generate private key + private_key = PrivateKey() + public_key = private_key.get_public_key() + + # Create different address types + p2pkh_addr = public_key.get_address() + p2wpkh_addr = public_key.get_segwit_address() + p2tr_addr = public_key.get_taproot_address() + +From Script +^^^^^^^^^ + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.script import Script + + setup('testnet') + + # Create a script + script = Script(['OP_1', 'public_key_1', 'public_key_2', 'OP_2', 'OP_CHECKMULTISIG']) + + # Get different address types + p2sh_addr = script.get_p2sh_address() + p2wsh_addr = script.get_segwit_address() + + # Taproot addresses with script trees + # Define some scripts + script1 = Script(['pubkey1', 'OP_CHECKSIG']) + script2 = Script(['pubkey2', 'OP_CHECKSIG']) + + # Create a taproot address with these scripts + p2tr_addr = public_key.get_taproot_address([script1, script2]) + +Creating an Address from Scratch +------------------------------ + +While typically addresses are derived from keys or scripts, you can also create an address object directly: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import P2pkhAddress, P2shAddress + from bitcoinutils.keys import P2wpkhAddress, P2wshAddress, P2trAddress + + setup('testnet') + + # Legacy addresses + p2pkh = P2pkhAddress(hash160='751e76e8199196d454941c45d1b3a323f1433bd6') + p2sh = P2shAddress(hash160='8f55563b9a19f321c211e9b9f38cdf686ea07845') + + # SegWit addresses + p2wpkh = P2wpkhAddress(witness_program='751e76e8199196d454941c45d1b3a323f1433bd6') + p2wsh = P2wshAddress(script=some_script) + p2tr = P2trAddress(witness_program='cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115') + +Script PubKey Generation +---------------------- + +Each address type can generate its corresponding scriptPubKey: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import P2pkhAddress, P2shAddress, P2wpkhAddress, P2trAddress + + setup('testnet') + + # Create different address types + p2pkh = P2pkhAddress('mzF2sbdxcMqKFLoakdBcvZpUXMjgiXGZW1') + p2sh = P2shAddress('2N6Vk58WRh7gQYrRUBZAJAxXC7TKPPpKmDD') + p2wpkh = P2wpkhAddress('tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx') + p2tr = P2trAddress('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6') + + # Get scriptPubKey for each address type + p2pkh_script = p2pkh.to_script_pub_key() # OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG + p2sh_script = p2sh.to_script_pub_key() # OP_HASH160 OP_EQUAL + p2wpkh_script = p2wpkh.to_script_pub_key() # OP_0 + p2tr_script = p2tr.to_script_pub_key() # OP_1 + + print(f"P2PKH scriptPubKey: {p2pkh_script.to_string()}") + print(f"P2SH scriptPubKey: {p2sh_script.to_string()}") + print(f"P2WPKH scriptPubKey: {p2wpkh_script.to_string()}") + print(f"P2TR scriptPubKey: {p2tr_script.to_string()}") + +Converting Between Address Types +----------------------------- + +While there's no direct "convert" method, you can convert between address types using the intermediate objects: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey, P2pkhAddress, P2wpkhAddress + + setup('testnet') + + # Start with a P2PKH address + p2pkh_addr = P2pkhAddress('mzF2sbdxcMqKFLoakdBcvZpUXMjgiXGZW1') + + # To convert, first we'd need the underlying public key + # In a real application, you'd have the private key + private_key = PrivateKey('your_private_key_wif') + public_key = private_key.get_public_key() + + # Now create different address types + new_p2pkh_addr = public_key.get_address() + p2wpkh_addr = public_key.get_segwit_address() + p2tr_addr = public_key.get_taproot_address() + +Address Validation +---------------- + +The library provides automatic validation when creating address objects: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import P2pkhAddress + + setup('testnet') + + # This will validate the address + try: + addr = P2pkhAddress('mzF2sbdxcMqKFLoakdBcvZpUXMjgiXGZW1') + # Address is valid + print(f"Address {addr.to_string()} is valid") + except ValueError: + # Address is invalid + print("Invalid address provided") + +Network-specific Addresses +------------------------ + +The library supports both mainnet and testnet addresses: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey + + # For mainnet + setup('mainnet') + + priv = PrivateKey() + pub = priv.get_public_key() + + # Mainnet addresses + mainnet_p2pkh = pub.get_address() + mainnet_p2wpkh = pub.get_segwit_address() + mainnet_p2tr = pub.get_taproot_address() + + print(f"Mainnet P2PKH: {mainnet_p2pkh.to_string()}") # Starts with '1' + print(f"Mainnet P2WPKH: {mainnet_p2wpkh.to_string()}") # Starts with 'bc1q' + print(f"Mainnet P2TR: {mainnet_p2tr.to_string()}") # Starts with 'bc1p' + + # For testnet + setup('testnet') + + priv = PrivateKey() + pub = priv.get_public_key() + + # Testnet addresses + testnet_p2pkh = pub.get_address() + testnet_p2wpkh = pub.get_segwit_address() + testnet_p2tr = pub.get_taproot_address() + + print(f"Testnet P2PKH: {testnet_p2pkh.to_string()}") # Starts with 'm' or 'n' + print(f"Testnet P2WPKH: {testnet_p2wpkh.to_string()}") # Starts with 'tb1q' + print(f"Testnet P2TR: {testnet_p2tr.to_string()}") # Starts with 'tb1p' \ No newline at end of file diff --git a/docs/usage/bech32.rst b/docs/usage/bech32.rst new file mode 100644 index 0000000..308e29e --- /dev/null +++ b/docs/usage/bech32.rst @@ -0,0 +1,237 @@ +Bech32 Module +============ + +The `bech32` module provides functions for encoding and decoding Bech32 and Bech32m addresses as specified in BIP173 and BIP350. This address format is used for native SegWit addresses in Bitcoin. + +Overview +-------- + +Bech32 is an address format that includes error detection and is case insensitive. It is used for native SegWit addresses and has the following components: + +- Human-readable part (HRP): e.g., "bc" for Bitcoin mainnet, "tb" for testnet +- Separator: Always "1" +- Data part: Encoded data that includes the witness version and witness program + +Bech32m is an improved version of Bech32 (described in BIP350) that is used for SegWit version 1 and higher (e.g., Taproot addresses). + +Basic Usage +---------- + +The module provides functions for encoding and decoding Bech32 addresses: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.bech32 import encode, decode + + # Setup the network + setup('mainnet') + + # Encode a witness program + hrp = "bc" # Human-readable part for Bitcoin mainnet + witness_version = 0 # SegWit version 0 + witness_program = [0, 14, 20, 15, ...] # Witness program as list of integers + + address = encode(hrp, witness_version, witness_program) + print(f"Bech32 address: {address}") + + # Decode a Bech32 address + decoded_version, decoded_program = decode(hrp, address) + print(f"Witness version: {decoded_version}") + print(f"Witness program: {decoded_program}") + +Encoding Addresses +---------------- + +To encode a witness program into a Bech32 address: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.bech32 import encode + + # Setup the network + setup('testnet') + + # For SegWit v0 address on testnet + hrp = "tb" + witness_version = 0 + witness_program = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] + + address = encode(hrp, witness_version, witness_program) + print(f"SegWit v0 address: {address}") + + # For SegWit v1 address (Taproot) on testnet + witness_version = 1 + witness_program = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31] + + taproot_address = encode(hrp, witness_version, witness_program) + print(f"Taproot address: {taproot_address}") + +Decoding Addresses +---------------- + +To decode a Bech32 or Bech32m address: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.bech32 import decode + + # Setup the network + setup('mainnet') + + # Decode a SegWit v0 address + segwit_address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" + hrp = "bc" + + witness_version, witness_program = decode(hrp, segwit_address) + + if witness_version is not None: + print(f"Witness version: {witness_version}") + print(f"Witness program: {witness_program}") + else: + print("Invalid address") + + # Decode a Taproot address + taproot_address = "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vq5zuyut" + + witness_version, witness_program = decode(hrp, taproot_address) + + if witness_version is not None: + print(f"Witness version: {witness_version}") + print(f"Witness program: {witness_program}") + else: + print("Invalid address") + +Converting Between Data Formats +---------------------------- + +The module also provides utility functions for converting between different data formats: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.bech32 import convertbits + from bitcoinutils.utils import h_to_b, b_to_h + + # Setup the network + setup('testnet') + + # Convert a hex string to a list of 5-bit integers (for Bech32 encoding) + hex_string = "751e76e8199196d454941c45d1b3a323f1433bd6" + byte_data = h_to_b(hex_string) + + # Convert from 8-bit bytes to 5-bit integers for Bech32 + five_bit_data = convertbits(list(byte_data), 8, 5) + + # Convert back to 8-bit bytes + eight_bit_data = convertbits(five_bit_data, 5, 8, False) + + # Convert back to hex + recovered_hex = b_to_h(bytes(eight_bit_data)) + + print(f"Original hex: {hex_string}") + print(f"Recovered hex: {recovered_hex}") + +Working with SegWit Addresses +--------------------------- + +The primary use case for Bech32 is encoding and decoding SegWit addresses: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey + from bitcoinutils.bech32 import encode, decode + + # Setup the network + setup('testnet') + + # Create a private key and derive public key + private_key = PrivateKey() + public_key = private_key.get_public_key() + + # Get a SegWit v0 address + segwit_address = public_key.get_segwit_address() + print(f"SegWit v0 address: {segwit_address.to_string()}") + + # Get a Taproot (SegWit v1) address + taproot_address = public_key.get_taproot_address() + print(f"Taproot address: {taproot_address.to_string()}") + + # Decode the SegWit v0 address + witness_version, witness_program = decode("tb", segwit_address.to_string()) + print(f"SegWit v0 witness version: {witness_version}") + print(f"SegWit v0 witness program: {witness_program}") + + # Decode the Taproot address + witness_version, witness_program = decode("tb", taproot_address.to_string()) + print(f"Taproot witness version: {witness_version}") + print(f"Taproot witness program: {witness_program}") + +Error Detection +------------- + +Bech32 includes error detection and can help identify common mistakes: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.bech32 import decode + + # Setup the network + setup('mainnet') + + # Valid address + valid_address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" + + # Address with a typo (v8f3t4 -> v8f3t5) + typo_address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5" + + # Address with incorrect case (mixed case not allowed in Bech32) + case_error_address = "bc1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4" + + # Check valid address + result = decode("bc", valid_address) + print(f"Valid address check: {result[0] is not None}") + + # Check address with typo + result = decode("bc", typo_address) + print(f"Typo address check: {result[0] is not None}") + + # Check address with case error + result = decode("bc", case_error_address) + print(f"Case error address check: {result[0] is not None}") + +Bech32 vs Bech32m +--------------- + +The module automatically handles both Bech32 (for witness version 0) and Bech32m (for witness version 1+): + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.bech32 import encode, decode + + # Setup the network + setup('testnet') + + # SegWit v0 uses Bech32 + v0_program = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] + v0_address = encode("tb", 0, v0_program) + print(f"SegWit v0 address: {v0_address}") + + # SegWit v1 uses Bech32m + v1_program = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31] + v1_address = encode("tb", 1, v1_program) + print(f"SegWit v1 address: {v1_address}") + + # Decode both + v0_result = decode("tb", v0_address) + v1_result = decode("tb", v1_address) + + print(f"SegWit v0 decode: version={v0_result[0]}, program length={len(v0_result[1])}") + print(f"SegWit v1 decode: version={v1_result[0]}, program length={len(v1_result[1])}") \ No newline at end of file diff --git a/docs/usage/block.rst b/docs/usage/block.rst new file mode 100644 index 0000000..bb853ea --- /dev/null +++ b/docs/usage/block.rst @@ -0,0 +1,318 @@ +Block Module +=========== + +The `block` module provides functionality for working with Bitcoin blocks. It allows parsing, creating, and analyzing Bitcoin blocks, including their headers and transactions. + +Overview +-------- + +A Bitcoin block consists of a block header and a list of transactions. The block header contains metadata about the block, such as the version, previous block hash, merkle root, timestamp, difficulty target, and nonce. The transactions are the actual data stored in the blockchain. + +This module allows you to: + +- Parse blocks from raw hex data +- Access block header fields +- Extract and work with transactions in a block +- Parse SegWit blocks (both v0 and v1) + +Basic Usage +---------- + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.block import Block + + # Setup the network + setup('testnet') + + # Parse a block from hex data + block_hex = "0100000001..." # Hex representation of a block + block = Block.from_hex(block_hex) + + # Get block information + print(f"Block hash: {block.get_hash()}") + print(f"Previous block hash: {block.get_prev_block_hash()}") + print(f"Merkle root: {block.get_merkle_root()}") + print(f"Block version: {block.get_version()}") + print(f"Block timestamp: {block.get_timestamp()}") + print(f"Block difficulty target: {block.get_bits()}") + print(f"Block nonce: {block.get_nonce()}") + + # Get transactions + txs = block.get_transactions() + print(f"Number of transactions: {len(txs)}") + + # Print the first transaction (coinbase) + if txs: + print(f"Coinbase transaction: {txs[0].serialize()}") + +Parsing Blocks +----------- + +The Block class provides methods to parse blocks from different sources: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.block import Block + from bitcoinutils.utils import h_to_b + + # Setup the network + setup('testnet') + + # From a hex string + block_hex = "0100000001..." # Hex representation of a block + block = Block.from_hex(block_hex) + + # From bytes + block_bytes = h_to_b(block_hex) + block = Block.from_bytes(block_bytes) + + # From a file + with open('block.dat', 'rb') as f: + block_data = f.read() + block = Block.from_bytes(block_data) + +Block Header +---------- + +The block header contains metadata about the block. You can access these fields using getter methods: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.block import Block + + # Setup the network + setup('testnet') + + # Parse a block + block_hex = "0100000001..." # Hex representation of a block + block = Block.from_hex(block_hex) + + # Access header fields + version = block.get_version() + prev_block_hash = block.get_prev_block_hash() + merkle_root = block.get_merkle_root() + timestamp = block.get_timestamp() + bits = block.get_bits() + nonce = block.get_nonce() + + # Print header information + print(f"Block version: {version}") + print(f"Previous block hash: {prev_block_hash}") + print(f"Merkle root: {merkle_root}") + print(f"Timestamp: {timestamp}") + print(f"Bits: {bits}") + print(f"Nonce: {nonce}") + + # Get the block hash (double SHA-256 of the header) + block_hash = block.get_hash() + print(f"Block hash: {block_hash}") + +Working with Transactions +----------------------- + +The Block class allows you to access the transactions in the block: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.block import Block + + # Setup the network + setup('testnet') + + # Parse a block + block_hex = "0100000001..." # Hex representation of a block + block = Block.from_hex(block_hex) + + # Get all transactions + txs = block.get_transactions() + + # Process transactions + for i, tx in enumerate(txs): + print(f"Transaction {i}:") + print(f" TXID: {tx.get_txid()}") + print(f" Version: {tx.get_version()}") + print(f" Locktime: {tx.get_locktime()}") + print(f" Number of inputs: {len(tx.get_inputs())}") + print(f" Number of outputs: {len(tx.get_outputs())}") + + # Get the coinbase transaction (first transaction in a block) + coinbase_tx = txs[0] + print(f"Coinbase transaction ID: {coinbase_tx.get_txid()}") + + # Check if the merkle root is valid + calculated_merkle_root = block.calculate_merkle_root() + stored_merkle_root = block.get_merkle_root() + print(f"Merkle root valid: {calculated_merkle_root == stored_merkle_root}") + +SegWit Blocks +----------- + +SegWit blocks have a special structure with a witness commitment in the coinbase transaction. The Block class can handle these: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.block import Block + + # Setup the network + setup('testnet') + + # Parse a SegWit block + segwit_block_hex = "0000002001..." # Hex representation of a SegWit block + block = Block.from_hex(segwit_block_hex) + + # Check if it's a SegWit block (version >= 0x20000000) + is_segwit = (block.get_version() & 0xE0000000) == 0x20000000 + print(f"Is SegWit block: {is_segwit}") + + # Get transactions with witness data + txs = block.get_transactions() + + # Check for witness data in transactions + for i, tx in enumerate(txs): + has_witness = any(txin.witness for txin in tx.get_inputs()) + print(f"Transaction {i} has witness data: {has_witness}") + +Block Validation +------------- + +You can perform some basic validation on a block: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.block import Block + import time + + # Setup the network + setup('testnet') + + # Parse a block + block_hex = "0100000001..." # Hex representation of a block + block = Block.from_hex(block_hex) + + # Validate block hash + prev_block_hash = "000000000000000000023..." # Known previous block hash + if block.get_prev_block_hash() != prev_block_hash: + print("Invalid previous block hash") + + # Check timestamp (must be less than 2 hours in the future) + current_time = int(time.time()) + if block.get_timestamp() > current_time + 7200: + print("Block timestamp too far in the future") + + # Validate merkle root + calculated_merkle_root = block.calculate_merkle_root() + if calculated_merkle_root != block.get_merkle_root(): + print("Invalid merkle root") + + # Verify coinbase transaction + txs = block.get_transactions() + if not txs or not txs[0].is_coinbase(): + print("Missing or invalid coinbase transaction") + +Creating a Block +------------- + +While not commonly used outside of mining, you can also create a block: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.block import Block + from bitcoinutils.transactions import Transaction + import time + + # Setup the network + setup('testnet') + + # Create a coinbase transaction + coinbase_tx = Transaction.create_coinbase("03...") + + # Add other transactions + txs = [coinbase_tx, tx1, tx2, ...] + + # Create block header + version = 1 + prev_block_hash = "000000000000000000023..." + merkle_root = "..." # Calculate from transactions + timestamp = int(time.time()) + bits = 0x1d00ffff # Difficulty target + nonce = 0 # Starting nonce for mining + + # Create the block + block = Block(version, prev_block_hash, merkle_root, timestamp, bits, nonce, txs) + + # Serialize the block + block_hex = block.serialize() + print(f"Block hex: {block_hex}") + +Practical Applications +------------------- + +Some practical applications of the block module include: + +1. **Block Explorer Functionality**: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.block import Block + from bitcoinutils.proxy import NodeProxy + + # Setup the network + setup('testnet') + + # Connect to a node + proxy = NodeProxy('user', 'password') + + # Get the latest block hash + latest_hash = proxy.get_best_block_hash() + + # Get the block data + block_data = proxy.get_block(latest_hash, verbose=False) + + # Parse the block + block = Block.from_hex(block_data) + + # Display block information + print(f"Block hash: {block.get_hash()}") + print(f"Block time: {block.get_timestamp()}") + print(f"Transactions: {len(block.get_transactions())}") + +2. **Transaction Verification**: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.block import Block + from bitcoinutils.utils import merkle_root + + # Setup the network + setup('testnet') + + # Parse a block + block_hex = "0100000001..." # Hex representation of a block + block = Block.from_hex(block_hex) + + # Get transaction IDs + tx_ids = [tx.get_txid() for tx in block.get_transactions()] + + # Verify a specific transaction is in the block + tx_id_to_verify = "1234..." + if tx_id_to_verify in tx_ids: + print(f"Transaction {tx_id_to_verify} is in the block") + + # Get the transaction + tx_index = tx_ids.index(tx_id_to_verify) + tx = block.get_transactions()[tx_index] + + # Process the transaction + print(f"Transaction details:") + print(f" Inputs: {len(tx.get_inputs())}") + print(f" Outputs: {len(tx.get_outputs())}") \ No newline at end of file diff --git a/docs/usage/constants.rst b/docs/usage/constants.rst new file mode 100644 index 0000000..37f9eeb --- /dev/null +++ b/docs/usage/constants.rst @@ -0,0 +1,206 @@ +Constants Module +=============== + +The `constants` module provides constants used throughout the Python Bitcoin Utils library. These constants include network-specific values, address prefixes, signature hash types, and other Bitcoin-related constants. + +Overview +-------- + +The constants in this module are organized into categories: + +- Network-related constants +- Address type constants +- Signature hash constants +- Script constants +- Other Bitcoin-specific constants + +Network Constants +--------------- + +These constants define network-specific values for mainnet, testnet, and regtest: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.constants import NETWORK_P2PKH_PREFIXES, NETWORK_P2SH_PREFIXES, NETWORK_SEGWIT_PREFIXES, NETWORK_WIF_PREFIXES, NETWORK_DEFAULT_PORTS + + # Set up the network + setup('mainnet') + + # Display network constants + print(f"P2PKH Prefix (mainnet): {NETWORK_P2PKH_PREFIXES['mainnet'].hex()}") + print(f"P2SH Prefix (mainnet): {NETWORK_P2SH_PREFIXES['mainnet'].hex()}") + print(f"SegWit Prefix (mainnet): {NETWORK_SEGWIT_PREFIXES['mainnet']}") + print(f"WIF Prefix (mainnet): {NETWORK_WIF_PREFIXES['mainnet'].hex()}") + print(f"Default Port (mainnet): {NETWORK_DEFAULT_PORTS['mainnet']}") + + # Change network + setup('testnet') + + # Display network constants for testnet + print(f"P2PKH Prefix (testnet): {NETWORK_P2PKH_PREFIXES['testnet'].hex()}") + print(f"P2SH Prefix (testnet): {NETWORK_P2SH_PREFIXES['testnet'].hex()}") + print(f"SegWit Prefix (testnet): {NETWORK_SEGWIT_PREFIXES['testnet']}") + print(f"WIF Prefix (testnet): {NETWORK_WIF_PREFIXES['testnet'].hex()}") + print(f"Default Port (testnet): {NETWORK_DEFAULT_PORTS['testnet']}") + +Network Prefixes +-------------- + +The network prefixes are used for address encoding: + +- `NETWORK_P2PKH_PREFIXES`: Prefixes for Pay-to-Public-Key-Hash (P2PKH) addresses + - Mainnet: 0x00 (addresses start with '1') + - Testnet: 0x6f (addresses start with 'm' or 'n') + +- `NETWORK_P2SH_PREFIXES`: Prefixes for Pay-to-Script-Hash (P2SH) addresses + - Mainnet: 0x05 (addresses start with '3') + - Testnet: 0xc4 (addresses start with '2') + +- `NETWORK_SEGWIT_PREFIXES`: Prefixes for SegWit addresses (Bech32) + - Mainnet: "bc" + - Testnet: "tb" + +- `NETWORK_WIF_PREFIXES`: Prefixes for Wallet Import Format (WIF) private keys + - Mainnet: 0x80 + - Testnet: 0xef + +Address Types +----------- + +The library defines constants for different address types: + +.. code-block:: python + + from bitcoinutils.constants import P2PKH_ADDRESS, P2SH_ADDRESS, P2WPKH_ADDRESS_V0, P2WSH_ADDRESS_V0, P2TR_ADDRESS_V1 + + print(f"P2PKH Address Type: {P2PKH_ADDRESS}") + print(f"P2SH Address Type: {P2SH_ADDRESS}") + print(f"P2WPKH Address Type (SegWit v0): {P2WPKH_ADDRESS_V0}") + print(f"P2WSH Address Type (SegWit v0): {P2WSH_ADDRESS_V0}") + print(f"P2TR Address Type (SegWit v1): {P2TR_ADDRESS_V1}") + +Signature Hash Constants +--------------------- + +These constants define signature hash types used in transaction signing: + +.. code-block:: python + + from bitcoinutils.constants import SIGHASH_ALL, SIGHASH_NONE, SIGHASH_SINGLE, SIGHASH_ANYONECANPAY, TAPROOT_SIGHASH_ALL + + # Legacy and SegWit v0 signature hash types + print(f"SIGHASH_ALL: {SIGHASH_ALL}") + print(f"SIGHASH_NONE: {SIGHASH_NONE}") + print(f"SIGHASH_SINGLE: {SIGHASH_SINGLE}") + print(f"SIGHASH_ANYONECANPAY: {SIGHASH_ANYONECANPAY}") + + # Combinations + print(f"SIGHASH_ALL | SIGHASH_ANYONECANPAY: {SIGHASH_ALL | SIGHASH_ANYONECANPAY}") + print(f"SIGHASH_NONE | SIGHASH_ANYONECANPAY: {SIGHASH_NONE | SIGHASH_ANYONECANPAY}") + print(f"SIGHASH_SINGLE | SIGHASH_ANYONECANPAY: {SIGHASH_SINGLE | SIGHASH_ANYONECANPAY}") + + # Taproot signature hash type + print(f"TAPROOT_SIGHASH_ALL: {TAPROOT_SIGHASH_ALL}") + +Using Constants in Code +--------------------- + +Here are some examples of how constants are used in the library: + +1. **Network Selection**: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.constants import NETWORK_P2PKH_PREFIXES + + # Set up the network + setup('testnet') + + # Get the network prefix for the current network + network = get_network() + prefix = NETWORK_P2PKH_PREFIXES[network] + print(f"Current network: {network}") + print(f"P2PKH prefix: {prefix.hex()}") + +2. **Address Type Identification**: + +.. code-block:: python + + from bitcoinutils.keys import P2pkhAddress, P2shAddress, P2wpkhAddress, P2wshAddress, P2trAddress + + # Create addresses + p2pkh = P2pkhAddress('mnc4ZZCFRvbNxTRMhf2gEgKUfMi3XSy7L6') + p2sh = P2shAddress('2N6Vk58WRh7gQYrRUBZAJAxXC7TKPPpKmDD') + p2wpkh = P2wpkhAddress('tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx') + p2wsh = P2wshAddress('tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7') + p2tr = P2trAddress('tb1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqc8gma6') + + # Check address types + print(f"P2PKH type: {p2pkh.get_type() == P2PKH_ADDRESS}") + print(f"P2SH type: {p2sh.get_type() == P2SH_ADDRESS}") + print(f"P2WPKH type: {p2wpkh.get_type() == P2WPKH_ADDRESS_V0}") + print(f"P2WSH type: {p2wsh.get_type() == P2WSH_ADDRESS_V0}") + print(f"P2TR type: {p2tr.get_type() == P2TR_ADDRESS_V1}") + +3. **Signature Hash Types**: + +.. code-block:: python + + from bitcoinutils.transactions import Transaction, TxInput, TxOutput + from bitcoinutils.keys import PrivateKey + from bitcoinutils.constants import SIGHASH_ALL, SIGHASH_SINGLE, SIGHASH_ANYONECANPAY + + # Create a transaction + txin = TxInput('previous_tx_id', 0) + txout = TxOutput(0.001, recipient_script_pub_key) + tx = Transaction([txin], [txout]) + + # Sign with different sighash types + private_key = PrivateKey('private_key_wif') + + # Sign with SIGHASH_ALL (default) + sig_all = private_key.sign_input(tx, 0, script_pub_key) + + # Sign with SIGHASH_SINGLE + sig_single = private_key.sign_input(tx, 0, script_pub_key, sighash=SIGHASH_SINGLE) + + # Sign with SIGHASH_ALL | SIGHASH_ANYONECANPAY + sig_all_anyone = private_key.sign_input(tx, 0, script_pub_key, sighash=SIGHASH_ALL | SIGHASH_ANYONECANPAY) + +Default Values +----------- + +The library also defines some default values: + +.. code-block:: python + + from bitcoinutils.constants import DEFAULT_TX_LOCKTIME, DEFAULT_TX_VERSION, DEFAULT_TX_IN_SEQUENCE + + print(f"Default Transaction Locktime: {DEFAULT_TX_LOCKTIME}") + print(f"Default Transaction Version: {DEFAULT_TX_VERSION}") + print(f"Default Transaction Input Sequence: {DEFAULT_TX_IN_SEQUENCE}") + +Extending and Customizing +----------------------- + +If you need to work with networks not defined in the constants module (e.g., a private Bitcoin network), you can extend the constants in your application: + +.. code-block:: python + + from bitcoinutils.constants import NETWORK_P2PKH_PREFIXES, NETWORK_P2SH_PREFIXES, NETWORK_SEGWIT_PREFIXES, NETWORK_WIF_PREFIXES, NETWORK_DEFAULT_PORTS + import bitcoinutils.setup + + # Add custom network + NETWORK_P2PKH_PREFIXES['mynet'] = bytes.fromhex('6f') # Same as testnet + NETWORK_P2SH_PREFIXES['mynet'] = bytes.fromhex('c4') # Same as testnet + NETWORK_SEGWIT_PREFIXES['mynet'] = 'my' # Custom prefix + NETWORK_WIF_PREFIXES['mynet'] = bytes.fromhex('ef') # Same as testnet + NETWORK_DEFAULT_PORTS['mynet'] = 18333 # Custom port + + # Patch the network list + bitcoinutils.setup.NETWORKS.append('mynet') + + # Set up the custom network + bitcoinutils.setup.setup('mynet') \ No newline at end of file diff --git a/docs/usage/hdwallet.rst b/docs/usage/hdwallet.rst new file mode 100644 index 0000000..29a9b87 --- /dev/null +++ b/docs/usage/hdwallet.rst @@ -0,0 +1,181 @@ +HD Wallet +======== + +The `hdwallet` module implements Hierarchical Deterministic (HD) wallets according to BIP32. HD wallets allow for the generation of a tree of keys from a single seed, which is particularly useful for wallet applications. + +Overview +-------- + +HD wallets work by deriving a hierarchy of keys from a single master key. This master key is derived from a seed, which can be represented as a mnemonic phrase (as specified in BIP39) or as a seed directly. + +Basic Usage +---------- + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.hdwallet import HDWallet + + # Setup the network + setup('testnet') + + # Create an HD wallet from a seed + seed = "000102030405060708090a0b0c0d0e0f" + hdwallet = HDWallet.from_seed(seed) + + # Derive a child key + child = hdwallet.derive_path("m/44'/0'/0'/0/0") + + # Get the private and public keys + private_key = child.get_private_key() + public_key = child.get_public_key() + + print(f"BIP32 extended private key: {child.to_extended_private_key()}") + print(f"BIP32 extended public key: {child.to_extended_public_key()}") + print(f"Private key (WIF): {private_key.to_wif()}") + print(f"Public key (hex): {public_key.to_hex()}") + print(f"Address: {public_key.get_address().to_string()}") + +Creating an HD Wallet +-------------------- + +You can create an HD wallet from a seed, a mnemonic phrase, or an extended key: + +.. code-block:: python + + # From a seed (hex string) + seed = "000102030405060708090a0b0c0d0e0f" + hdwallet_from_seed = HDWallet.from_seed(seed) + + # From an extended private key + xpriv = "tprv8ZgxMBicQKsPeWHBt7a68nPnvgTnuDhUgDWC8wZCgA8GahrQ3f3uWpq7wE7Uc1dLBnCe1hhCZ886K6ND37memNCdCUNrfumuKHDYDAEqoia" + hdwallet_from_xpriv = HDWallet.from_extended_key(xpriv) + + # From an extended public key (watch-only wallet) + xpub = "tpubD6NzVbkrYhZ4XpMCLWrG6bG7E9FeX6qehxQ6RaMBhB49XyCMQUi9NwD9HBrichm9jnN9hEhAsR7Ks3H7DZYJu2BcTKKCPQH7VsaBAusVSSS" + hdwallet_from_xpub = HDWallet.from_extended_key(xpub) + +Deriving Child Keys +----------------- + +There are two ways to derive child keys: + +1. Using the `derive_child` method to derive a single child key +2. Using the `derive_path` method to derive a key using a BIP32 path + +.. code-block:: python + + # Using derive_child + # Derive a child key at index 0 (non-hardened) + child_0 = hdwallet.derive_child(0) + + # Derive a hardened child key at index 0 + child_0_hardened = hdwallet.derive_child(0, hardened=True) + + # Using derive_path + # Derive using a BIP32 path + # m / purpose' / coin_type' / account' / change / address_index + # Note: In paths, ' or h denotes hardened derivation + bip44_path = "m/44'/0'/0'/0/0" # BIP44 path for the first address + child = hdwallet.derive_path(bip44_path) + +Extended Keys +----------- + +Extended keys are serialized representations of HD wallet keys that contain both the key and the chain code. They are used to export and import HD wallets. + +.. code-block:: python + + # Get the extended private key + xpriv = hdwallet.to_extended_private_key() + print(f"Extended private key: {xpriv}") + + # Get the extended public key + xpub = hdwallet.to_extended_public_key() + print(f"Extended public key: {xpub}") + + # Import from an extended key + imported_wallet = HDWallet.from_extended_key(xpriv) + +BIP44 Standard Paths +------------------ + +BIP44 defines a standard path structure for HD wallets: + +`m / purpose' / coin_type' / account' / change / address_index` + +- `purpose` is always 44' for BIP44 +- `coin_type` is the type of cryptocurrency (0' for Bitcoin, 1' for Bitcoin testnet) +- `account` is the account number, starting from 0' +- `change` is 0 for external addresses (receiving) and 1 for internal addresses (change) +- `address_index` is the address number, starting from 0 + +.. code-block:: python + + # Derive the first receiving address for the first account + receiving_address_0 = hdwallet.derive_path("m/44'/0'/0'/0/0") + + # Derive the first change address for the first account + change_address_0 = hdwallet.derive_path("m/44'/0'/0'/1/0") + +Working with Keys and Addresses +----------------------------- + +After deriving a child key, you can get the associated private key, public key, and addresses: + +.. code-block:: python + + # Derive a child key + child = hdwallet.derive_path("m/44'/0'/0'/0/0") + + # Get the private key + private_key = child.get_private_key() + print(f"Private key (WIF): {private_key.to_wif()}") + + # Get the public key + public_key = child.get_public_key() + print(f"Public key (hex): {public_key.to_hex()}") + + # Get different address types + p2pkh_address = public_key.get_address() + p2wpkh_address = public_key.get_segwit_address() + p2tr_address = public_key.get_taproot_address() + + print(f"P2PKH address: {p2pkh_address.to_string()}") + print(f"P2WPKH address: {p2wpkh_address.to_string()}") + print(f"P2TR address: {p2tr_address.to_string()}") + +Creating a Watch-Only Wallet +-------------------------- + +You can create a watch-only wallet from an extended public key. This is useful for monitoring addresses without having access to the private keys: + +.. code-block:: python + + # Create a wallet + seed = "000102030405060708090a0b0c0d0e0f" + hdwallet = HDWallet.from_seed(seed) + + # Get the extended public key for the account + account = hdwallet.derive_path("m/44'/0'/0'") + xpub = account.to_extended_public_key() + + # Create a watch-only wallet from the xpuba + watch_only = HDWallet.from_extended_key(xpub) + + # Derive receiving addresses + address_0 = watch_only.derive_path("0/0").get_public_key().get_address() + address_1 = watch_only.derive_path("0/1").get_public_key().get_address() + + print(f"Address 0: {address_0.to_string()}") + print(f"Address 1: {address_1.to_string()}") + +Security Considerations +--------------------- + +When working with HD wallets, keep the following security considerations in mind: + +1. **Master Key Security**: The master key (seed or mnemonic) can derive all keys in the wallet. Keep it secure. +2. **Extended Private Keys**: Extended private keys contain the chain code and can derive all child private keys. Treat them as sensitive as the master key. +3. **Extended Public Keys**: While extended public keys can only derive public keys, they can leak privacy information if combined with any child private key. +4. **Hardened Derivation**: Use hardened derivation (') for the first levels of your HD wallet to prevent potential security issues. \ No newline at end of file diff --git a/docs/usage/keys.rst b/docs/usage/keys.rst index 9ef86b1..bb7a9cb 100644 --- a/docs/usage/keys.rst +++ b/docs/usage/keys.rst @@ -1,5 +1,268 @@ -Keys and Addresses module -------------------------- +Keys and Addresses +================ -.. automodule:: keys - :members: +The ``keys`` module provides classes and methods for working with Bitcoin keys and addresses. It includes functionality for creating, managing, and using private keys, public keys, and various address types. + +Overview +-------- + +This module implements the following classes: + +- ``PrivateKey``: For managing ECDSA private keys +- ``PublicKey``: For managing ECDSA public keys +- ``Address``: Base class for Bitcoin addresses +- ``P2pkhAddress``: Pay-to-Public-Key-Hash (P2PKH) addresses +- ``P2shAddress``: Pay-to-Script-Hash (P2SH) addresses +- ``SegwitAddress``: Base class for Segregated Witness addresses +- ``P2wpkhAddress``: Pay-to-Witness-Public-Key-Hash (P2WPKH) addresses +- ``P2wshAddress``: Pay-to-Witness-Script-Hash (P2WSH) addresses +- ``P2trAddress``: Pay-to-Taproot (P2TR) addresses + +Basic Usage +---------- + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey, PublicKey, P2pkhAddress + + # Always remember to setup the network + setup('testnet') + + # Generate a new private key + priv = PrivateKey() + print(f"Private key (WIF): {priv.to_wif()}") + + # Get the corresponding public key + pub = priv.get_public_key() + print(f"Public key: {pub.to_hex()}") + + # Get the corresponding P2PKH address + addr = pub.get_address() + print(f"Address: {addr.to_string()}") + + # Create a specific private key from WIF + priv2 = PrivateKey.from_wif("cTALNpTpRbbxTCJ2A5Vq88UxT44w1PE2cYqiB3n4hRvzyCev1Wwo") + print(f"Address from WIF: {priv2.get_public_key().get_address().to_string()}") + +Working with Private Keys +------------------------ + +The ``PrivateKey`` class provides methods for creating and managing private keys: + +.. code-block:: python + + # Generate a random private key + priv = PrivateKey() + + # Create from WIF + priv = PrivateKey.from_wif("cVwfreZB3i8hCGcnZ8JeXAE3PQCgqpd2Yx1HscAUxN6XUfBFDGH3") + + # Create from raw bytes + priv = PrivateKey.from_bytes(b'...') + + # Create from specific number (deterministic) + priv = PrivateKey(secret_exponent=123456789) + + # Export to WIF (Wallet Import Format) + wif = priv.to_wif(compressed=True) # compressed by default + + # Sign a message + signature = priv.sign_message("Hello, Bitcoin!") + + # Sign a transaction input + from bitcoinutils.transactions import Transaction + signature = priv.sign_input(tx, txin_index, script) + + # Get the corresponding public key + pub = priv.get_public_key() + +Working with Public Keys +---------------------- + +The ``PublicKey`` class provides methods for creating and managing public keys: + +.. code-block:: python + + # Get a public key from a private key + priv = PrivateKey() + pub = priv.get_public_key() + + # Create from hex (SEC format) + pub = PublicKey.from_hex("02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc") + + # Recover a public key from a message and signature + pub = PublicKey.from_message_signature(message, signature) + + # Export to hex (SEC format) + hex_compressed = pub.to_hex(compressed=True) + hex_uncompressed = pub.to_hex(compressed=False) + + # For taproot (x-only pubkeys) + x_only_hex = pub.to_x_only_hex() + + # Verify a message signature + is_valid = pub.verify(signature, message) + + # Convert to hash160 (used in address creation) + hash160 = pub.to_hash160(compressed=True) + + # Get different address types + p2pkh_addr = pub.get_address(compressed=True) + p2wpkh_addr = pub.get_segwit_address() + p2tr_addr = pub.get_taproot_address() + +Working with Addresses +-------------------- + +The module provides several address types, each with specific methods: + +.. code-block:: python + + # Create a P2PKH address from a public key + pub = PrivateKey().get_public_key() + p2pkh = pub.get_address() + print(f"P2PKH address: {p2pkh.to_string()}") + + # Create from an existing address string + p2pkh = P2pkhAddress.from_address("mzx5YhAH9kNHtcN481u6WkjeHjYtVeKVh2") + + # Create from hash160 + p2pkh = P2pkhAddress.from_hash160("751e76e8199196d454941c45d1b3a323f1433bd6") + + # Get scriptPubKey for use in transactions + script = p2pkh.to_script_pub_key() + + # P2SH address from a redeem script + from bitcoinutils.script import Script + redeem_script = Script(['OP_2', pub1.to_hex(), pub2.to_hex(), 'OP_2', 'OP_CHECKMULTISIG']) + p2sh = P2shAddress.from_script(redeem_script) + print(f"P2SH address: {p2sh.to_string()}") + +SegWit Addresses +-------------- + +SegWit addresses are special address types that use Segregated Witness: + +.. code-block:: python + + # P2WPKH address (SegWit version 0) + pub = PrivateKey().get_public_key() + p2wpkh = pub.get_segwit_address() + print(f"P2WPKH address: {p2wpkh.to_string()}") + + # P2WSH address (SegWit version 0) + witness_script = Script(['OP_2', pub1.to_hex(), pub2.to_hex(), 'OP_2', 'OP_CHECKMULTISIG']) + p2wsh = P2wshAddress.from_script(witness_script) + print(f"P2WSH address: {p2wsh.to_string()}") + + # P2TR address (SegWit version 1 - Taproot) + p2tr = pub.get_taproot_address() + print(f"P2TR address: {p2tr.to_string()}") + + # P2TR with script path spending + taproot_script = Script([...]) # Script path + p2tr = pub.get_taproot_address(scripts=taproot_script) + +Message Signing and Verification +------------------------------ + +Bitcoin provides a standard way to sign and verify messages: + +.. code-block:: python + + # Sign a message with a private key + priv = PrivateKey.from_wif("cVwfreZB3i8hCGcnZ8JeXAE3PQCgqpd2Yx1HscAUxN6XUfBFDGH3") + signature = priv.sign_message("Hello, Bitcoin!") + + # Verify a message with a public key + pub = priv.get_public_key() + is_valid = pub.verify(signature, "Hello, Bitcoin!") + + # Verify a message with an address (static method) + address = "mzx5YhAH9kNHtcN481u6WkjeHjYtVeKVh2" + is_valid = PublicKey.verify_message(address, signature, "Hello, Bitcoin!") + +Working with Taproot +------------------ + +Taproot is a Bitcoin upgrade that enhances privacy, efficiency, and smart contract capabilities: + +.. code-block:: python + + # Create a private key + priv = PrivateKey() + pub = priv.get_public_key() + + # Get a basic P2TR address (key-path only) + p2tr = pub.get_taproot_address() + + # Create a P2TR address with script paths + from bitcoinutils.script import Script + script_a = Script(['OP_1']) + script_b = Script(['OP_0']) + # A simple script tree with two scripts + scripts = [[script_a, script_b]] + p2tr_with_scripts = pub.get_taproot_address(scripts=scripts) + + # Tweak the public key for Taproot + pubkey_tweaked, is_odd = pub.to_taproot_hex(scripts=scripts) + + # Sign a Taproot input for key-path spending + sig = priv.sign_taproot_input(tx, 0, script_pubkeys, amounts) + + # Sign a Taproot input for script-path spending + sig = priv.sign_taproot_input(tx, 0, script_pubkeys, amounts, + script_path=True, tapleaf_script=script_a, + tweak=False) + +Advanced Features +-------------- + +1. **Custom Network Configuration**: + + You can use keys and addresses on different Bitcoin networks: + + .. code-block:: python + + from bitcoinutils.setup import setup + + # Use testnet + setup('testnet') + priv = PrivateKey() + addr = priv.get_public_key().get_address() + print(f"Testnet address: {addr.to_string()}") + + # Use mainnet + setup('mainnet') + priv = PrivateKey() + addr = priv.get_public_key().get_address() + print(f"Mainnet address: {addr.to_string()}") + +2. **Transaction Signing**: + + Private keys can sign different types of transactions: + + .. code-block:: python + + # Sign a regular P2PKH input + sig = priv.sign_input(tx, txin_index, script_pubkey) + + # Sign a SegWit input + sig = priv.sign_segwit_input(tx, txin_index, script_pubkey, amount) + + # Sign a Taproot input + sig = priv.sign_taproot_input(tx, txin_index, scripts, amounts) + +3. **Key Utilities**: + + The module provides various utility methods: + + .. code-block:: python + + # Check if y-coordinate is even + is_even = pub.is_y_even() + + # Get raw bytes representation + key_bytes = priv.to_bytes() + pubkey_bytes = pub.to_bytes() \ No newline at end of file diff --git a/docs/usage/proxy.rst b/docs/usage/proxy.rst index 32d477f..c49b374 100644 --- a/docs/usage/proxy.rst +++ b/docs/usage/proxy.rst @@ -1,6 +1,269 @@ -Proxy module ------------- +Proxy Module +=========== -.. automodule:: proxy - :members: +The `proxy` module allows interaction with a Bitcoin node through RPC calls. It provides a convenient way to query blockchain information, submit transactions, and perform wallet operations. +NodeProxy Class +------------- + +The main class in the proxy module is `NodeProxy`, which provides a wrapper around Bitcoin Core's JSON-RPC interface. + +Basic Usage +---------- + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.proxy import NodeProxy + + # Setup the network + setup('testnet') + + # Connect to a Bitcoin node + proxy = NodeProxy('username', 'password', host='127.0.0.1', port=18332) + + # Get blockchain information + info = proxy.get_blockchain_info() + print(f"Current blockchain height: {info['blocks']}") + + # Get the balance of the wallet + balance = proxy.get_balance() + print(f"Wallet balance: {balance}") + + # Send a raw transaction + tx_hex = "0100000001..." + tx_id = proxy.send_raw_transaction(tx_hex) + print(f"Transaction submitted with ID: {tx_id}") + +Connecting to a Bitcoin Node +-------------------------- + +You can connect to a Bitcoin node by creating a `NodeProxy` instance: + +.. code-block:: python + + from bitcoinutils.proxy import NodeProxy + + # Connect to a local Bitcoin Core node + proxy = NodeProxy( + rpcuser='your_rpc_username', + rpcpassword='your_rpc_password', + host='127.0.0.1', # Default is localhost + port=18332, # 18332 for testnet, 8332 for mainnet + use_https=False # Whether to use HTTPS for the connection + ) + + # Test the connection + try: + network_info = proxy.get_network_info() + print(f"Connected to Bitcoin Core version: {network_info['version']}") + except Exception as e: + print(f"Connection failed: {e}") + +Common RPC Methods +---------------- + +The `NodeProxy` class provides methods that correspond to Bitcoin Core's RPC commands. Here are some of the most commonly used methods: + +Blockchain Information +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + # Get blockchain info + blockchain_info = proxy.get_blockchain_info() + print(f"Chain: {blockchain_info['chain']}") + print(f"Blocks: {blockchain_info['blocks']}") + print(f"Headers: {blockchain_info['headers']}") + + # Get block hash at a specific height + block_hash = proxy.get_block_hash(height=123456) + + # Get block information + block = proxy.get_block(block_hash) + + # Get raw transaction + tx = proxy.get_raw_transaction("transaction_id", verbose=True) + +Wallet Operations +^^^^^^^^^^^^^^^ + +.. code-block:: python + + # Get wallet balance + balance = proxy.get_balance() + + # Get unspent transaction outputs (UTXOs) + utxos = proxy.list_unspent() + + # Create a new address + new_address = proxy.get_new_address() + + # Send bitcoins to an address + txid = proxy.send_to_address("tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx", 0.001) + +Transaction Operations +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + # Create a raw transaction + tx_inputs = [{"txid": "previous_txid", "vout": 0}] + tx_outputs = {"tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx": 0.001} + raw_tx = proxy.create_raw_transaction(tx_inputs, tx_outputs) + + # Sign a raw transaction + signed_tx = proxy.sign_raw_transaction(raw_tx) + + # Send a raw transaction + tx_id = proxy.send_raw_transaction(signed_tx["hex"]) + + # Get transaction info + tx_info = proxy.get_transaction(tx_id) + +Network Information +^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + # Get network information + net_info = proxy.get_network_info() + print(f"Version: {net_info['version']}") + print(f"Subversion: {net_info['subversion']}") + print(f"Connections: {net_info['connections']}") + + # Get network statistics + net_stats = proxy.get_network_stats() + + # Get peer information + peer_info = proxy.get_peer_info() + +Error Handling +----------- + +It's important to handle errors that might occur during RPC calls: + +.. code-block:: python + + try: + # Attempt to get information about a non-existent transaction + tx_info = proxy.get_transaction("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") + except Exception as e: + print(f"Error: {e}") + +Custom RPC Methods +--------------- + +You can call any RPC method that your Bitcoin Core node supports, even if it's not explicitly defined in the NodeProxy class: + +.. code-block:: python + + # Call a custom RPC method + result = proxy.call('estimatesmartfee', 6) # Estimate fee for confirmation within 6 blocks + + # Or use the direct __call__ implementation + result = proxy('estimatesmartfee', 6) + +Working with Testnet +------------------ + +To work with the testnet network: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.proxy import NodeProxy + + # Setup the network for testnet + setup('testnet') + + # Connect to a testnet node + proxy = NodeProxy('username', 'password', port=18332) # Note the testnet port + + # Get some testnet coins from a faucet + faucet_address = proxy.get_new_address() + print(f"Request testnet coins to be sent to: {faucet_address}") + +Security Considerations +-------------------- + +When using the proxy module, keep these security considerations in mind: + +1. **RPC Credentials**: Always protect your RPC username and password. Don't hardcode them in your scripts. + +2. **Network Access**: By default, Bitcoin Core only accepts RPC connections from localhost. If you're connecting from another machine, ensure you've properly configured Bitcoin Core's `rpcallowip` setting. + +3. **HTTPS**: For remote connections, consider using HTTPS by setting `use_https=True`. + +4. **Transaction Validation**: Always validate transactions before broadcasting them to the network. + +Example: Creating and Sending a Transaction +---------------------------------------- + +Here's a complete example of creating and sending a transaction using the proxy module: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.proxy import NodeProxy + from bitcoinutils.keys import PrivateKey, P2pkhAddress + from bitcoinutils.transactions import Transaction, TxInput, TxOutput + + # Setup network + setup('testnet') + + # Connect to node + proxy = NodeProxy('username', 'password', port=18332) + + # Get unspent outputs + unspent = proxy.list_unspent() + + if len(unspent) > 0: + # Get the first unspent output + utxo = unspent[0] + + # Create a transaction input + txin = TxInput(utxo['txid'], utxo['vout']) + + # Create a recipient address + recipient_addr = P2pkhAddress('mzF2sbdxcMqKFLoakdBcvZpUXMjgiXGZW1') + + # Calculate amount (subtract fee) + amount = utxo['amount'] - 0.0001 # Subtract fee + + # Create a transaction output + txout = TxOutput(amount, recipient_addr.to_script_pub_key()) + + # Create transaction + tx = Transaction([txin], [txout]) + + # Get private key for the unspent output + priv_key = PrivateKey('your_private_key_wif') + + # Sign the input + sig = priv_key.sign_input(tx, 0, P2pkhAddress(utxo['address']).to_script_pub_key()) + txin.script_sig = sig + + # Serialize the transaction + signed_tx_hex = tx.serialize() + + # Send the transaction + txid = proxy.send_raw_transaction(signed_tx_hex) + print(f"Transaction sent! TXID: {txid}") + else: + print("No unspent outputs available.") + +Troubleshooting +------------- + +If you encounter issues with the proxy module: + +1. **Connection Refused**: Make sure your Bitcoin Core node is running and accepting RPC connections. + +2. **Authentication Failed**: Verify your RPC username and password are correct. + +3. **Method Not Found**: Ensure the RPC method you're trying to call is supported by your Bitcoin Core version. + +4. **Transaction Rejected**: If your transaction is rejected, check for issues like insufficient funds, invalid inputs, or non-standard scripts. + +5. **RPC Timeout**: For operations that may take a long time, increase the timeout period when instantiating NodeProxy: `NodeProxy(rpcuser, rpcpassword, timeout=60)`. \ No newline at end of file diff --git a/docs/usage/schnorr.rst b/docs/usage/schnorr.rst new file mode 100644 index 0000000..dcbdb01 --- /dev/null +++ b/docs/usage/schnorr.rst @@ -0,0 +1,235 @@ +Schnorr Module +============= + +The `schnorr` module provides functionality for creating and verifying Schnorr signatures according to BIP340. Schnorr signatures are used in Taproot (SegWit v1) to provide more efficient, secure, and privacy-enhancing signature validation. + +Overview +-------- + +Schnorr signatures offer several advantages over ECDSA signatures: + +- **Linearity**: Schnorr signatures can be combined, enabling more efficient multisignature schemes +- **Simplicity**: The verification algorithm is simpler and more intuitive +- **Provable security**: Schnorr signatures have stronger security proofs than ECDSA +- **Smaller size**: No signature malleability means no need for extra data in the signature + +This module implements the BIP340 specification for Schnorr signatures, which is used in Taproot transactions. + +Basic Usage +---------- + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.schnorr import sign, verify + from bitcoinutils.keys import PrivateKey + + # Setup the network + setup('testnet') + + # Create a private key + private_key = PrivateKey() + public_key = private_key.get_public_key() + + # Message to sign + message = "Hello, Bitcoin!" + message_bytes = message.encode('utf-8') + + # Sign the message using Schnorr + signature = sign(private_key, message_bytes) + print(f"Schnorr signature: {signature.hex()}") + + # Verify the signature + is_valid = verify(public_key, message_bytes, signature) + print(f"Signature valid: {is_valid}") + +Signing with Schnorr +------------------ + +To create a Schnorr signature: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.schnorr import sign + from bitcoinutils.keys import PrivateKey + import hashlib + + # Setup the network + setup('testnet') + + # Create a key + private_key = PrivateKey() + + # Option 1: Sign a message directly + message = "Hello, Bitcoin!" + message_bytes = message.encode('utf-8') + signature = sign(private_key, message_bytes) + + # Option 2: Sign a message digest + digest = hashlib.sha256(message_bytes).digest() + signature_from_digest = sign(private_key, digest) + + print(f"Schnorr signature: {signature.hex()}") + +Verifying Schnorr Signatures +-------------------------- + +To verify a Schnorr signature: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.schnorr import verify + from bitcoinutils.keys import PrivateKey, PublicKey + import hashlib + + # Setup the network + setup('testnet') + + # Create a key pair for testing + private_key = PrivateKey() + public_key = private_key.get_public_key() + + # Sign a message + message = "Hello, Bitcoin!" + message_bytes = message.encode('utf-8') + signature = sign(private_key, message_bytes) + + # Verify the signature + is_valid = verify(public_key, message_bytes, signature) + print(f"Signature valid: {is_valid}") + + # Verify using a message digest + digest = hashlib.sha256(message_bytes).digest() + is_valid_digest = verify(public_key, digest, signature) + print(f"Signature valid (using digest): {is_valid_digest}") + + # Verify an invalid signature + modified_signature = bytearray(signature) + modified_signature[0] ^= 1 # Flip a bit + is_invalid = verify(public_key, message_bytes, bytes(modified_signature)) + print(f"Modified signature valid: {is_invalid}") # Should be False + +Working with Taproot +------------------ + +Schnorr signatures are primarily used in Taproot (SegWit v1) transactions. Here's how to use them with Taproot: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey + from bitcoinutils.transactions import Transaction, TxInput, TxOutput + + # Setup the network + setup('testnet') + + # Create a transaction that spends from a Taproot output + txin = TxInput('taproot_tx_id', 0) + txout = TxOutput(0.001, recipient_script_pub_key) + tx = Transaction([txin], [txout]) + + # Sign the Taproot input (this uses Schnorr signatures internally) + private_key = PrivateKey('your_private_key_wif') + signature = private_key.sign_taproot_input(tx, 0, [{'value': 0.001, 'scriptPubKey': prev_script_pub_key}]) + + # Set the witness data + txin.witness = [signature] # Key path spending - just the signature + + # Get the signed transaction + signed_tx_hex = tx.serialize() + print(f"Signed Taproot transaction: {signed_tx_hex}") + +Batch Verification +--------------- + +One advantage of Schnorr signatures is that they can be efficiently batch verified. This is not directly implemented in the library, but here's a conceptual example: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.schnorr import verify + + # Setup the network + setup('testnet') + + # Multiple signature-message-publickey tuples to verify + verifications = [ + (signature1, message1, public_key1), + (signature2, message2, public_key2), + (signature3, message3, public_key3), + ] + + # Verify all signatures + all_valid = all(verify(pk, msg, sig) for sig, msg, pk in verifications) + print(f"All signatures valid: {all_valid}") + +Schnorr vs ECDSA +-------------- + +Here's a comparison between Schnorr and ECDSA signatures in the library: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey + from bitcoinutils.schnorr import sign as schnorr_sign + import time + + # Setup the network + setup('testnet') + + # Create a key pair + private_key = PrivateKey() + public_key = private_key.get_public_key() + + # Message to sign + message = "Hello, Bitcoin!" + message_bytes = message.encode('utf-8') + + # Sign with ECDSA + start_time = time.time() + ecdsa_signature = private_key.sign_message(message) + ecdsa_time = time.time() - start_time + print(f"ECDSA signature: {ecdsa_signature}") + print(f"ECDSA signature time: {ecdsa_time:.6f} seconds") + + # Sign with Schnorr + start_time = time.time() + schnorr_signature = schnorr_sign(private_key, message_bytes) + schnorr_time = time.time() - start_time + print(f"Schnorr signature: {schnorr_signature.hex()}") + print(f"Schnorr signature time: {schnorr_time:.6f} seconds") + + # Compare sizes + print(f"ECDSA signature size: {len(ecdsa_signature)} bytes") + print(f"Schnorr signature size: {len(schnorr_signature)} bytes") + +Technical Details +-------------- + +The Schnorr signature implementation follows BIP340 and has these key characteristics: + +1. **Deterministic Nonce Generation**: Uses a deterministic nonce to prevent catastrophic key leaks from poor randomness. + +2. **Tagged Hashes**: Uses tagged hashes to ensure domain separation, preventing attacks that try to exploit signature schemes. + +3. **x-only Public Keys**: Uses only the x-coordinate of public keys to save space. + +4. **Single Verification Equation**: Has a simple, efficient verification equation. + +5. **No Signature Malleability**: Prevents signature malleability issues that exist in ECDSA. + +Security Considerations +-------------------- + +When using Schnorr signatures, keep in mind these security considerations: + +1. **Key Management**: Protect private keys as they can derive all signatures. + +2. **Nonce Reuse**: The library prevents nonce reuse, but custom implementations must ensure that a nonce is never reused with the same key for different messages. + +3. **Implementation Security**: The library follows the BIP340 reference implementation for security. + +4. **Batch Verification**: Be aware that batch verification can be faster but might mask individual signature failures. \ No newline at end of file diff --git a/docs/usage/script.rst b/docs/usage/script.rst index 575839e..53f30e6 100644 --- a/docs/usage/script.rst +++ b/docs/usage/script.rst @@ -1,6 +1,349 @@ -Script module -------------- +Script +====== -.. automodule:: script - :members: +The ``script`` module provides functionality for working with Bitcoin scripts, which are used to specify the conditions under which bitcoins can be spent. +Overview +-------- + +Bitcoin scripts are a stack-based programming language used to encode spending conditions in Bitcoin transactions. The script module implements a class for creating, manipulating, and converting Bitcoin scripts. + +The main class is: + +- ``Script``: Represents a Bitcoin script as a list of operations and data + +Basic Usage +---------- + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.script import Script + + # Always remember to setup the network + setup('testnet') + + # Create a P2PKH scriptPubKey (locking script) + scriptPubKey = Script(['OP_DUP', 'OP_HASH160', + '751e76e8199196d454941c45d1b3a323f1433bd6', + 'OP_EQUALVERIFY', 'OP_CHECKSIG']) + + # Convert script to bytes + script_bytes = scriptPubKey.to_bytes() + + # Convert script to hex + script_hex = scriptPubKey.to_hex() + + print(f"Script (hex): {script_hex}") + +Creating Scripts +-------------- + +The ``Script`` class provides various ways to create Bitcoin scripts: + +.. code-block:: python + + # Standard P2PKH script + p2pkh_script = Script(['OP_DUP', 'OP_HASH160', + 'hash160_of_public_key', + 'OP_EQUALVERIFY', 'OP_CHECKSIG']) + + # P2SH script + p2sh_script = Script(['OP_HASH160', 'hash160_of_redeem_script', 'OP_EQUAL']) + + # Multi-signature script (2-of-3) + multisig_script = Script(['OP_2', + 'public_key1_hex', + 'public_key2_hex', + 'public_key3_hex', + 'OP_3', 'OP_CHECKMULTISIG']) + + # P2WPKH script + p2wpkh_script = Script(['OP_0', 'hash160_of_public_key']) + + # P2WSH script + p2wsh_script = Script(['OP_0', 'sha256_of_witness_script']) + + # P2TR script + p2tr_script = Script(['OP_1', 'tweaked_public_key_x_only']) + + # Timelock script + timelock_script = Script(['blocknumber_or_timestamp', 'OP_CHECKLOCKTIMEVERIFY', + 'OP_DROP', 'OP_DUP', 'OP_HASH160', + 'hash160_of_public_key', + 'OP_EQUALVERIFY', 'OP_CHECKSIG']) + +Types of Script Elements +---------------------- + +Bitcoin scripts can contain different types of elements: + +1. **OP codes**: These are the operation codes that perform stack manipulations or cryptographic operations + + .. code-block:: python + + # OP codes are represented as strings + script = Script(['OP_DUP', 'OP_HASH160', 'OP_EQUALVERIFY', 'OP_CHECKSIG']) + +2. **Data**: These can be hex-encoded strings representing binary data + + .. code-block:: python + + # Data is represented as hex strings + script = Script(['OP_RETURN', 'deadbeef']) # OP_RETURN followed by data + +3. **Integers**: These are converted to the appropriate minimal representation + + .. code-block:: python + + # Integers from 0 to 16 can use OP_0 to OP_16 + script = Script(['OP_2', 'OP_3', 'OP_ADD']) # Adds 2 and 3 + + # Larger integers are pushed as data + script = Script([42, 'OP_DROP']) # Pushes 42 to stack, then drops it + +Script Conversions +---------------- + +The ``Script`` class provides methods to convert scripts between different formats: + +.. code-block:: python + + # Convert to bytes + script_bytes = script.to_bytes() + + # Convert to hex + script_hex = script.to_hex() + + # Get the original script array + script_array = script.get_script() + + # Parse from raw hex (useful for parsing scripts from transactions) + raw_script_hex = "76a914751e76e8199196d454941c45d1b3a323f1433bd688ac" + parsed_script = Script.from_raw(raw_script_hex) + + # Create copies of scripts + script_copy = Script.copy(script) + +P2SH and P2WSH Conversions +------------------------ + +Scripts can be converted to P2SH and P2WSH formats for use in transactions: + +.. code-block:: python + + # Convert a redeem script to a P2SH script + redeem_script = Script(['OP_2', 'pubkey1', 'pubkey2', 'pubkey3', 'OP_3', 'OP_CHECKMULTISIG']) + p2sh_script = redeem_script.to_p2sh_script_pub_key() + + # Convert a witness script to a P2WSH script + witness_script = Script(['OP_2', 'pubkey1', 'pubkey2', 'pubkey3', 'OP_3', 'OP_CHECKMULTISIG']) + p2wsh_script = witness_script.to_p2wsh_script_pub_key() + +Address Generation from Scripts +---------------------------- + +Scripts can be used to generate Bitcoin addresses: + +.. code-block:: python + + from bitcoinutils.keys import P2shAddress, P2wshAddress + + # Create P2SH address from redeem script + redeem_script = Script(['OP_2', 'pubkey1', 'pubkey2', 'pubkey3', 'OP_3', 'OP_CHECKMULTISIG']) + p2sh_address = P2shAddress.from_script(redeem_script) + + # Create P2WSH address from witness script + witness_script = Script(['OP_2', 'pubkey1', 'pubkey2', 'pubkey3', 'OP_3', 'OP_CHECKMULTISIG']) + p2wsh_address = P2wshAddress.from_script(witness_script) + +Common Script Templates +-------------------- + +Here are some common script templates used in Bitcoin: + +1. **Pay-to-Public-Key-Hash (P2PKH)**: + + .. code-block:: python + + # scriptPubKey (locking script) + p2pkh_scriptPubKey = Script(['OP_DUP', 'OP_HASH160', 'hash160_of_public_key', + 'OP_EQUALVERIFY', 'OP_CHECKSIG']) + + # scriptSig (unlocking script) + p2pkh_scriptSig = Script(['signature', 'public_key']) + +2. **Pay-to-Script-Hash (P2SH)**: + + .. code-block:: python + + # scriptPubKey (locking script) + p2sh_scriptPubKey = Script(['OP_HASH160', 'hash160_of_redeem_script', 'OP_EQUAL']) + + # scriptSig (unlocking script) for a 2-of-3 multisig redeem script + p2sh_scriptSig = Script(['OP_0', 'signature1', 'signature2', + 'serialized_redeem_script']) + + # Where serialized_redeem_script is the hex of: + redeem_script = Script(['OP_2', 'pubkey1', 'pubkey2', 'pubkey3', + 'OP_3', 'OP_CHECKMULTISIG']) + +3. **Pay-to-Witness-Public-Key-Hash (P2WPKH)**: + + .. code-block:: python + + # scriptPubKey (locking script) + p2wpkh_scriptPubKey = Script(['OP_0', 'hash160_of_public_key']) + + # witness stack (not scriptSig): + # [signature, public_key] + +4. **Pay-to-Witness-Script-Hash (P2WSH)**: + + .. code-block:: python + + # scriptPubKey (locking script) + p2wsh_scriptPubKey = Script(['OP_0', 'sha256_of_witness_script']) + + # witness stack for a 2-of-3 multisig witness script: + # [OP_0, signature1, signature2, serialized_witness_script] + + # Where serialized_witness_script is the hex of: + witness_script = Script(['OP_2', 'pubkey1', 'pubkey2', 'pubkey3', + 'OP_3', 'OP_CHECKMULTISIG']) + +5. **Pay-to-Taproot (P2TR)**: + + .. code-block:: python + + # scriptPubKey (locking script) + p2tr_scriptPubKey = Script(['OP_1', 'tweaked_public_key_x_only']) + + # Key path witness: + # [signature] + + # Script path witness (for script A in a script tree): + # [signature, script_A, control_block] + +Working with OP_CODES +------------------- + +The Script module provides access to all standard Bitcoin script OP_CODES. These operation codes are the building blocks of Bitcoin scripts: + +.. code-block:: python + + # Accessing OP_CODES + from bitcoinutils.script import Script + + # Constants + script = Script(['OP_0', 'OP_1', 'OP_2', 'OP_16']) + + # Flow control + script = Script(['OP_IF', 'OP_1', 'OP_ELSE', 'OP_0', 'OP_ENDIF']) + + # Stack operations + script = Script(['OP_DUP', 'OP_DROP', 'OP_SWAP', 'OP_ROT']) + + # Bitwise logic + script = Script(['OP_EQUAL', 'OP_EQUALVERIFY']) + + # Arithmetic + script = Script(['OP_1ADD', 'OP_1SUB', 'OP_ADD', 'OP_SUB']) + + # Crypto + script = Script(['OP_RIPEMD160', 'OP_SHA256', 'OP_HASH160', 'OP_CHECKSIG']) + + # Locktime + script = Script(['OP_CHECKLOCKTIMEVERIFY', 'OP_CHECKSEQUENCEVERIFY']) + +Advanced Script Examples +--------------------- + +Here are some more advanced script examples: + +1. **Timelock Script** - This script can only be spent after a certain block height: + + .. code-block:: python + + # Can only be spent after block 650000 + timelock_script = Script([ + '00009e9c', # 650000 in little-endian hex + 'OP_CHECKLOCKTIMEVERIFY', + 'OP_DROP', + 'OP_DUP', + 'OP_HASH160', + 'hash160_of_public_key', + 'OP_EQUALVERIFY', + 'OP_CHECKSIG' + ]) + +2. **Hash Preimage** - This script can be spent by revealing a preimage to a hash: + + .. code-block:: python + + # Locking script + hash_lock_script = Script([ + 'OP_SHA256', + 'hash_of_secret', + 'OP_EQUAL' + ]) + + # Unlocking script (spend by revealing the secret) + hash_unlock_script = Script(['secret_value']) + +3. **Multi-signature with Timelock** - This script combines multisig with a timelock: + + .. code-block:: python + + # 2-of-3 multisig with a timelock (can't spend until block 650000) + multisig_timelock_script = Script([ + '00009e9c', # 650000 in little-endian hex + 'OP_CHECKLOCKTIMEVERIFY', + 'OP_DROP', + 'OP_2', + 'pubkey1', + 'pubkey2', + 'pubkey3', + 'OP_3', + 'OP_CHECKMULTISIG' + ]) + +4. **Relative Timelock** - This script can only be spent after a certain number of blocks since the UTXO was mined: + + .. code-block:: python + + # Can only be spent 144 blocks (approximately 1 day) after the UTXO was mined + relative_timelock_script = Script([ + 'OP_DUP', + 'OP_HASH160', + 'hash160_of_public_key', + 'OP_EQUALVERIFY', + 'OP_CHECKSIG', + '9001', # 144 in little-endian hex with the most significant bit of the first byte set to 0 + 'OP_CHECKSEQUENCEVERIFY', + 'OP_DROP' + ]) + +Security Considerations +--------------------- + +When working with Bitcoin scripts, keep these security considerations in mind: + +1. **Script Size Limits** - Bitcoin nodes have size limits for scripts. A standard redeem script can't exceed 520 bytes, and the combined size of all stack items can't exceed 10,000 bytes. + +2. **Standard Scripts** - For a transaction to be relayed by most nodes, it must use standard script templates. Non-standard scripts may not be relayed by the network. + +3. **OP_RETURN Data** - When storing data in the blockchain using OP_RETURN, the data is limited to 80 bytes. + +4. **Stack Size** - Bitcoin script has a stack size limit of 1,000 items. + +5. **Signature Verification** - CHECKSIG and CHECKMULTISIG operations are expensive. There's a limit on the number of signature checks per transaction. + +6. **Taproot Considerations** - When using Taproot scripts, ensure you're constructing the script tree correctly for the intended spending paths. + +Conclusion +--------- + +The Script module is a powerful tool for creating and manipulating Bitcoin scripts. It provides a simple, Python-based interface to Bitcoin's scripting language, allowing developers to create and use a wide variety of spending conditions in their Bitcoin applications. + +For more advanced use cases, the module can be combined with the transactions and keys modules to create and sign complex Bitcoin transactions with custom scripts. \ No newline at end of file diff --git a/docs/usage/segwit.rst b/docs/usage/segwit.rst new file mode 100644 index 0000000..98ffca5 --- /dev/null +++ b/docs/usage/segwit.rst @@ -0,0 +1,392 @@ +SegWit Functionality +================== + +SegWit (Segregated Witness) is a Bitcoin protocol upgrade that separates transaction signatures from transaction data, resulting in several benefits such as increased transaction capacity and fixing transaction malleability. + +The Python Bitcoin Utils library provides comprehensive support for SegWit, including both version 0 (P2WPKH, P2WSH) and version 1 (Taproot/P2TR). + +SegWit Versions +-------------- + +The library supports different versions of SegWit: + +* **SegWit v0**: Original SegWit implementation (P2WPKH and P2WSH) +* **SegWit v1**: Taproot update (P2TR) + +Address Types +------------ + +Native SegWit Addresses +^^^^^^^^^^^^^^^^^^^^^^^ + +P2WPKH (Pay to Witness Public Key Hash) +"""""""""""""""""""""""""""""""""""""""" + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey + + setup('testnet') + priv = PrivateKey() + pub = priv.get_public_key() + segwit_addr = pub.get_segwit_address() + print(f"P2WPKH address: {segwit_addr.to_string()}") + +P2WSH (Pay to Witness Script Hash) +"""""""""""""""""""""""""""""""""" + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PublicKey + from bitcoinutils.script import Script + + setup('testnet') + pub1 = PublicKey("public_key_1_hex") + pub2 = PublicKey("public_key_2_hex") + + # Create a 2-of-2 multisig redeem script + redeem_script = Script([2, pub1.to_hex(), pub2.to_hex(), 2, 'OP_CHECKMULTISIG']) + witness_script_addr = redeem_script.get_segwit_address() + print(f"P2WSH address: {witness_script_addr.to_string()}") + +Nested SegWit Addresses +^^^^^^^^^^^^^^^^^^^^^^^ + +P2SH-P2WPKH +""""""""""" + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey + + setup('testnet') + priv = PrivateKey() + pub = priv.get_public_key() + p2sh_p2wpkh_addr = pub.get_p2sh_p2wpkh_address() + print(f"P2SH-P2WPKH address: {p2sh_p2wpkh_addr.to_string()}") + +P2SH-P2WSH +"""""""""" + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PublicKey + from bitcoinutils.script import Script + + setup('testnet') + pub1 = PublicKey("public_key_1_hex") + pub2 = PublicKey("public_key_2_hex") + + # Create a 2-of-2 multisig redeem script + redeem_script = Script([2, pub1.to_hex(), pub2.to_hex(), 2, 'OP_CHECKMULTISIG']) + p2sh_p2wsh_addr = redeem_script.get_p2sh_p2wsh_address() + print(f"P2SH-P2WSH address: {p2sh_p2wsh_addr.to_string()}") + +Taproot Addresses (SegWit v1) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey + + setup('testnet') + priv = PrivateKey() + pub = priv.get_public_key() + taproot_addr = pub.get_taproot_address() + print(f"P2TR address: {taproot_addr.to_string()}") + +Creating SegWit Transactions +--------------------------- + +Sending to a P2WPKH Address +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey, P2wpkhAddress + from bitcoinutils.transactions import Transaction, TxInput, TxOutput + + setup('testnet') + + # Create a P2WPKH address to send to + recipient_addr = P2wpkhAddress('tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx') + + # Create transaction input (from a previous transaction) + txin = TxInput('previous_tx_id', 0) + + # Create transaction output + txout = TxOutput(0.001, recipient_addr.to_script_pub_key()) + + # Create transaction + tx = Transaction([txin], [txout]) + + # Sign the transaction + priv_key = PrivateKey('private_key_wif') + sig = priv_key.sign_input(tx, 0, prev_script_pub_key) + txin.script_sig = sig + + print(f"Signed transaction: {tx.serialize()}") + +Spending from a P2WPKH Address +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey, P2pkhAddress + from bitcoinutils.transactions import Transaction, TxInput, TxOutput + from bitcoinutils.script import Script + + setup('testnet') + + # Create a transaction input (from a P2WPKH UTXO) + txin = TxInput('previous_tx_id', 0) + + # Create a P2PKH address to send to + recipient_addr = P2pkhAddress('recipient_address') + + # Create transaction output + txout = TxOutput(0.0009, recipient_addr.to_script_pub_key()) + + # Create transaction + tx = Transaction([txin], [txout]) + + # For SegWit inputs, use sign_segwit_input instead of sign_input + priv_key = PrivateKey('private_key_wif') + pub_key = priv_key.get_public_key() + script_code = Script(['OP_DUP', 'OP_HASH160', pub_key.to_hash160(), 'OP_EQUALVERIFY', 'OP_CHECKSIG']) + + # Sign the segwit input + signature = priv_key.sign_segwit_input(tx, 0, script_code, 0.001) + + # Set witness data for the input + txin.witness = [signature, pub_key.to_hex()] + + print(f"Signed transaction: {tx.serialize()}") + +Taproot Transactions +------------------- + +Key Path Spending +^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey, P2trAddress + from bitcoinutils.transactions import Transaction, TxInput, TxOutput + + setup('testnet') + + # Create transaction input from a P2TR UTXO + txin = TxInput('previous_tx_id', 0) + + # Create a transaction output + recipient_addr = P2trAddress('recipient_taproot_address') + txout = TxOutput(0.0009, recipient_addr.to_script_pub_key()) + + # Create transaction + tx = Transaction([txin], [txout]) + + # Sign the taproot input using key path + priv_key = PrivateKey('private_key_wif') + signature = priv_key.sign_taproot_input( + tx, 0, + [{'value': 0.001, 'scriptPubKey': prev_script_pub_key}] + ) + + # Set witness data for the input + txin.witness = [signature] + + print(f"Signed transaction: {tx.serialize()}") + +Script Path Spending +^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey, PublicKey + from bitcoinutils.transactions import Transaction, TxInput, TxOutput + from bitcoinutils.script import Script + + setup('testnet') + + # Create transaction input from a P2TR UTXO + txin = TxInput('previous_tx_id', 0) + + # Create a transaction output + recipient_addr = P2pkhAddress('recipient_address') + txout = TxOutput(0.0009, recipient_addr.to_script_pub_key()) + + # Create transaction + tx = Transaction([txin], [txout]) + + # For script path spending, you need the taproot script + tapscript = Script(['pub_key', 'OP_CHECKSIG']) + + # Sign the taproot input using script path + priv_key = PrivateKey('private_key_wif') + signature = priv_key.sign_taproot_input( + tx, 0, + [{'value': 0.001, 'scriptPubKey': prev_script_pub_key}], + script_path=True, + tapleaf_script=tapscript + ) + + # Control block computation and witness setup would be handled internally + # Set witness data for the input + # Note: This is a simplified example. Actual witness data would include the + # control block and the script. + + print(f"Signed transaction: {tx.serialize()}") + +SegWit Transaction Digest +------------------------ + +The library uses different digest algorithms for signing SegWit transactions: + +SegWit v0 Digest Algorithm +^^^^^^^^^^^^^^^^^^^^^^^^^ + +For SegWit v0, the `get_transaction_segwit_digest` method implements the BIP143 specification. + +Taproot (SegWit v1) Digest Algorithm +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For Taproot (SegWit v1), the `get_transaction_taproot_digest` method implements the BIP341 specification. + +Witness Structure +--------------- + +In SegWit transactions, the witness data is stored separately from the transaction inputs: + +P2WPKH Witness +^^^^^^^^^^^^^ + +.. code-block:: + + [signature, public_key] + +P2WSH Witness +^^^^^^^^^^^^ + +.. code-block:: + + [sig1, sig2, ..., sigN, redeem_script] + +P2TR Key Path Witness +^^^^^^^^^^^^^^^^^^^ + +.. code-block:: + + [signature] + +P2TR Script Path Witness +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: + + [sig1, sig2, ..., script, control_block] + +Automatic Handling of Witness Data +-------------------------------- + +The library automatically provides the correct witness format for different types of inputs: + +* For non-witness inputs in SegWit transactions, the library adds a '00' byte as required by the protocol +* For P2WPKH inputs, it creates a witness with signature and public key +* For P2WSH inputs, it creates a witness with signatures and the witness script +* For P2TR inputs, it creates a witness with one signature for key path spending, or signature, script and control block for script path spending + +Mixed Input Transactions +---------------------- + +When creating transactions with both SegWit and non-SegWit inputs: + +1. Each input needs its own specific signing method +2. For non-SegWit inputs, use `sign_input` +3. For SegWit v0 inputs, use `sign_segwit_input` +4. For Taproot inputs, use `sign_taproot_input` +5. Ensure witness data is correctly set for each input + +.. code-block:: python + + # Example of a mixed input transaction + from bitcoinutils.setup import setup + from bitcoinutils.keys import PrivateKey + from bitcoinutils.transactions import Transaction, TxInput, TxOutput + from bitcoinutils.script import Script + + setup('testnet') + + # Create transaction inputs + # Non-SegWit input + txin1 = TxInput('legacy_tx_id', 0) + # SegWit v0 input + txin2 = TxInput('segwit_v0_tx_id', 0) + # Taproot input + txin3 = TxInput('taproot_tx_id', 0) + + # Create transaction output + recipient_addr = P2pkhAddress('recipient_address') + txout = TxOutput(0.0027, recipient_addr.to_script_pub_key()) + + # Create transaction + tx = Transaction([txin1, txin2, txin3], [txout]) + + # Sign each input with the appropriate method + # Legacy input + priv_key1 = PrivateKey('legacy_priv_key_wif') + sig1 = priv_key1.sign_input(tx, 0, legacy_script_pub_key) + txin1.script_sig = sig1 + + # SegWit v0 input + priv_key2 = PrivateKey('segwit_v0_priv_key_wif') + pub_key2 = priv_key2.get_public_key() + script_code2 = Script(['OP_DUP', 'OP_HASH160', pub_key2.to_hash160(), 'OP_EQUALVERIFY', 'OP_CHECKSIG']) + sig2 = priv_key2.sign_segwit_input(tx, 1, script_code2, 0.001) + txin2.witness = [sig2, pub_key2.to_hex()] + + # Taproot input + priv_key3 = PrivateKey('taproot_priv_key_wif') + sig3 = priv_key3.sign_taproot_input( + tx, 2, + [ + {'value': 0.001, 'scriptPubKey': legacy_script_pub_key}, + {'value': 0.001, 'scriptPubKey': segwit_v0_script_pub_key}, + {'value': 0.001, 'scriptPubKey': taproot_script_pub_key} + ] + ) + txin3.witness = [sig3] + +OP_CHECKSIGADD Support +-------------------- + +Taproot introduces the new OP_CHECKSIGADD opcode for more efficient threshold multi-signature scripts: + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.script import Script + + setup('testnet') + + # Create a 2-of-3 multi-signature script using OP_CHECKSIGADD + multi_sig_script = Script([ + 'pub_key1', 'OP_CHECKSIG', + 'pub_key2', 'OP_CHECKSIGADD', + 'pub_key3', 'OP_CHECKSIGADD', + '2', 'OP_EQUAL' + ]) + + # This is more efficient than the traditional way: + traditional_multisig = Script([ + '2', 'pub_key1', 'pub_key2', 'pub_key3', '3', 'OP_CHECKMULTISIG' + ]) \ No newline at end of file diff --git a/docs/usage/transactions.rst b/docs/usage/transactions.rst index 130e0a1..77c711d 100644 --- a/docs/usage/transactions.rst +++ b/docs/usage/transactions.rst @@ -1,6 +1,226 @@ -Transactions module +Transactions +=========== + +The ``transactions`` module provides classes and methods for working with Bitcoin transactions. It includes functionality for creating, signing, and manipulating transactions. + +Overview +-------- + +Bitcoin transactions consist of inputs and outputs. Inputs spend UTXOs (Unspent Transaction Outputs) from previous transactions, and outputs create new UTXOs. Each transaction also has additional metadata like version and locktime. + +This module provides the following classes: + +- ``TxInput``: Represents a transaction input +- ``TxWitnessInput``: Represents witness data for SegWit inputs +- ``TxOutput``: Represents a transaction output +- ``Sequence``: Helps setting up sequence numbers for various timelock options +- ``Locktime``: Helps setting up locktime values +- ``Transaction``: Represents a complete Bitcoin transaction + +Basic Usage +---------- + +.. code-block:: python + + from bitcoinutils.setup import setup + from bitcoinutils.utils import to_satoshis + from bitcoinutils.transactions import Transaction, TxInput, TxOutput + from bitcoinutils.keys import PrivateKey, P2pkhAddress + from bitcoinutils.script import Script + + # Always remember to setup the network + setup('testnet') + + # Create transaction input from previous transaction + txin = TxInput('previous_tx_id', 0) + + # Create transaction output + addr = P2pkhAddress('n4bkvTyU1dVdzsrhWBqBw8fEMbHjJvtmJR') + txout = TxOutput(to_satoshis(0.1), addr.to_script_pub_key()) + + # Create transaction with the input and output + tx = Transaction([txin], [txout]) + + # Sign transaction + sk = PrivateKey('cSoXt6tHfMPPn8FVMh8dJHKVYGGVKgTQeUFCbUKvRsmQmJdqqHnJ') + from_addr = P2pkhAddress('mgTAMEMGCk4iqMMLvfxgYvQWgEPVbR8WN2') + sig = sk.sign_input(tx, 0, from_addr.to_script_pub_key()) + + # Complete the transaction + txin.script_sig = Script([sig, sk.get_public_key().to_hex()]) + signed_tx = tx.serialize() + + print(signed_tx) + +Creating Transaction Inputs +-------------------------- + +The ``TxInput`` class represents an input in a transaction: + +.. code-block:: python + + # Basic transaction input + txin = TxInput('previous_tx_id', 0) + + # With custom script_sig + script_sig = Script(['signature_hex', 'pubkey_hex']) + txin = TxInput('previous_tx_id', 0, script_sig) + + # With custom sequence (for RBF or timelocks) + txin = TxInput('previous_tx_id', 0, Script([]), 'fdffffff') + +Transaction input fields: + +- ``txid``: The transaction ID (hash) of the UTXO being spent +- ``txout_index``: The output index in the referenced transaction +- ``script_sig``: The unlocking script (signature script) +- ``sequence``: The sequence number (used for timelocks or replace-by-fee) + +Creating Transaction Outputs +--------------------------- + +The ``TxOutput`` class represents an output in a transaction: + +.. code-block:: python + + # Create transaction output using an address + addr = P2pkhAddress('mzx5YhAH9kNHtcN481u6WkjeHjYtVeKVh2') + txout = TxOutput(to_satoshis(0.1), addr.to_script_pub_key()) + + # Create transaction output with custom script + script = Script(['OP_DUP', 'OP_HASH160', 'hash160_hex', 'OP_EQUALVERIFY', 'OP_CHECKSIG']) + txout = TxOutput(to_satoshis(0.1), script) + +Transaction output fields: + +- ``amount``: The amount in satoshis +- ``script_pubkey``: The locking script (scriptPubKey) + +Working with SegWit Transactions +------------------------------- + +SegWit transactions require witness data for their inputs: + +.. code-block:: python + + # Create a SegWit transaction + txin = TxInput('previous_tx_id', 0) + addr = P2wpkhAddress('tb1q9h0yjdupyfpxfjg24rpx755zvy0stu8h86225m') + txout = TxOutput(to_satoshis(0.09), addr.to_script_pub_key()) + + # Create the transaction with SegWit flag + tx = Transaction([txin], [txout], has_segwit=True) + + # Create and add witness data + witness = TxWitnessInput(['signature_hex', 'pubkey_hex']) + tx.witnesses = [witness] + + # Get transaction hex + tx_hex = tx.serialize() + +Signing Transactions +------------------ + +The module provides methods for signing transaction inputs: + +.. code-block:: python + + # Sign a standard P2PKH input + sk = PrivateKey('private_key_wif') + from_addr = P2pkhAddress('address') + sig = sk.sign_input(tx, 0, from_addr.to_script_pub_key()) + txin.script_sig = Script([sig, sk.get_public_key().to_hex()]) + + # Sign a SegWit input + sig = sk.sign_segwit_input(tx, 0, redeem_script, input_amount) + tx.witnesses = [TxWitnessInput([sig, sk.get_public_key().to_hex()])] + + # Sign a Taproot input + sig = sk.sign_taproot_input(tx, 0, utxo_scripts, amounts) + tx.witnesses = [TxWitnessInput([sig])] + +Transaction Timelocks +-------------------- + +Timelocks allow creating transactions that can only be spent after a certain time: + +.. code-block:: python + + # Create a transaction with absolute timelock (can't be mined until block 650000) + locktime = Locktime(650000) + tx = Transaction([txin], [txout], locktime=locktime.for_transaction()) + + # Use relative timelock in an input (can't be mined until 10 blocks after the input was mined) + sequence = Sequence(TYPE_RELATIVE_TIMELOCK, 10, is_type_block=True) + txin = TxInput('previous_tx_id', 0, Script([]), sequence.for_input_sequence()) + +Replace-By-Fee (RBF) ------------------- -.. automodule:: transactions - :members: +RBF allows replacing a transaction with a higher-fee version before it's confirmed: + +.. code-block:: python + + # Create an input with RBF signal + sequence = Sequence(TYPE_REPLACE_BY_FEE, 0) + txin = TxInput('previous_tx_id', 0, Script([]), sequence.for_input_sequence()) + +Transaction Methods +----------------- + +The ``Transaction`` class provides several useful methods: + +- ``get_txid()``: Get the transaction ID (hash) +- ``get_wtxid()``: Get the witness transaction ID (hash including witness data) +- ``get_size()``: Get the transaction size in bytes +- ``get_vsize()``: Get the virtual size in vbytes (for fee calculation) +- ``to_hex()`` or ``serialize()``: Get the serialized transaction in hexadecimal +- ``from_raw()``: Create a transaction from raw hex data + +Advanced Features +--------------- + +The module also supports: + +1. **Parsing Transactions**: Create transaction objects from serialized hex data: + + .. code-block:: python + + # Parse a raw transaction + raw_tx = "0200000001b021a77dcaad3a2..." + tx = Transaction.from_raw(raw_tx) + + # Access transaction details + print(f"Transaction ID: {tx.get_txid()}") + print(f"Inputs: {len(tx.inputs)}") + print(f"Outputs: {len(tx.outputs)}") + +2. **Different Signature Hash Types**: + + .. code-block:: python + + # Sign with SIGHASH_ALL (default) + sig = sk.sign_input(tx, 0, redeem_script, SIGHASH_ALL) + + # Sign with SIGHASH_NONE + sig = sk.sign_input(tx, 0, redeem_script, SIGHASH_NONE) + + # Sign with SIGHASH_SINGLE + sig = sk.sign_input(tx, 0, redeem_script, SIGHASH_SINGLE) + + # Sign with SIGHASH_ANYONECANPAY + sig = sk.sign_input(tx, 0, redeem_script, SIGHASH_ALL | SIGHASH_ANYONECANPAY) + +3. **Taproot Transactions**: + .. code-block:: python + + # Create a Taproot transaction + tx = Transaction([txin], [txout], has_segwit=True) + + # Sign for key-path spending + sig = sk.sign_taproot_input(tx, 0, script_pubkeys, amounts) + + # Sign for script-path spending + sig = sk.sign_taproot_input(tx, 0, script_pubkeys, amounts, + script_path=True, tapleaf_script=Script([...])) \ No newline at end of file diff --git a/docs/usage/utils.rst b/docs/usage/utils.rst new file mode 100644 index 0000000..51ca830 --- /dev/null +++ b/docs/usage/utils.rst @@ -0,0 +1,208 @@ +Utils Module +=========== + +The `utils` module provides utility functions for various Bitcoin-related operations, including encoding and decoding data, cryptographic operations, and parameter handling for Taproot. + +Overview +-------- + +The utility functions in this module handle: + +- Conversion between different data formats (bytes, hex, int) +- Cryptographic parameters for SECP256k1 +- Message signing and verification utilities +- Taproot-specific utilities (key tweaking, hashing) + +Encoding and Decoding Functions +----------------------------- + +These functions help convert between different data formats: + +.. code-block:: python + + from bitcoinutils.utils import h_to_b, b_to_h, h_to_i, i_to_h, b_to_i, i_to_b32 + + # Convert hex string to bytes + hex_str = "1a2b3c4d" + byte_data = h_to_b(hex_str) + print(f"Hex to bytes: {byte_data}") + + # Convert bytes to hex string + hex_str_back = b_to_h(byte_data) + print(f"Bytes to hex: {hex_str_back}") + + # Convert hex string to integer + hex_str = "1a2b3c4d" + int_val = h_to_i(hex_str) + print(f"Hex to int: {int_val}") + + # Convert integer to hex string + int_val = 439041101 + hex_str = i_to_h(int_val) + print(f"Int to hex: {hex_str}") + + # Convert bytes to integer + int_val = b_to_i(byte_data) + print(f"Bytes to int: {int_val}") + + # Convert integer to 32-byte representation + int_val = 439041101 + bytes_32 = i_to_b32(int_val) + print(f"Int to 32 bytes: {bytes_32.hex()}") + +SECP256k1 Parameters +------------------ + +The module defines parameters for the SECP256k1 elliptic curve, which is used in Bitcoin: + +.. code-block:: python + + from bitcoinutils.utils import Secp256k1Params + + # Display SECP256k1 parameters + print(f"SECP256k1 Order: {Secp256k1Params._order}") + print(f"SECP256k1 Field Size: {Secp256k1Params._p}") + print(f"SECP256k1 A: {Secp256k1Params._a}") + print(f"SECP256k1 B: {Secp256k1Params._b}") + +Message Signing Utilities +---------------------- + +The module provides helper functions for message signing: + +.. code-block:: python + + from bitcoinutils.utils import add_magic_prefix + + # Add Bitcoin message magic prefix to a message for signing + message = "Hello, Bitcoin!" + prefixed_message = add_magic_prefix(message) + print(f"Original message: {message}") + print(f"Prefixed message: {prefixed_message}") + +Taproot Utilities +-------------- + +Functions for working with Taproot-specific operations: + +.. code-block:: python + + from bitcoinutils.utils import calculate_tweak, tweak_taproot_pubkey, tweak_taproot_privkey + from bitcoinutils.keys import PrivateKey, PublicKey + from bitcoinutils.script import Script + + # Create a key pair + private_key = PrivateKey() + public_key = private_key.get_public_key() + + # Create a script for Taproot + script = Script(['OP_CHECKSIG']) + + # Calculate tweak for Taproot + tweak = calculate_tweak(public_key, script) + print(f"Taproot tweak: {tweak}") + + # Tweak the public key for Taproot + tweaked_pubkey, is_odd = tweak_taproot_pubkey(public_key.key.to_string(), tweak) + print(f"Tweaked public key: {tweaked_pubkey.hex()}") + print(f"Is Y-coordinate odd: {is_odd}") + + # Tweak the private key for Taproot + tweaked_privkey = tweak_taproot_privkey(private_key.key.to_string(), tweak) + print(f"Tweaked private key: {tweaked_privkey.hex()}") + +Conversion Examples +---------------- + +Here are some practical examples of using the utility functions: + +.. code-block:: python + + from bitcoinutils.utils import h_to_b, b_to_h, h_to_i, i_to_h, b_to_i + + # Convert a transaction ID (little-endian) to a byte order suitable for RPC calls + txid_hex = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" + txid_bytes = h_to_b(txid_hex) + txid_bytes_reversed = txid_bytes[::-1] # Reverse the bytes + txid_reversed_hex = b_to_h(txid_bytes_reversed) + print(f"Original TXID: {txid_hex}") + print(f"Reversed TXID: {txid_reversed_hex}") + + # Convert an amount in satoshis to bitcoin + satoshis = 123456789 + bitcoin = satoshis / 100000000 + print(f"{satoshis} satoshis = {bitcoin} BTC") + + # Convert a hexadecimal script to its assembly representation + script_hex = "76a914751e76e8199196d454941c45d1b3a323f1433bd688ac" + script_bytes = h_to_b(script_hex) + # In a real implementation, you would use the Script class to parse this + +Practical Applications +------------------- + +Here are some practical applications of these utility functions: + +1. **Transaction Processing**: + +.. code-block:: python + + from bitcoinutils.utils import h_to_b, b_to_h + from bitcoinutils.transactions import Transaction + + # Parse a raw transaction hex + raw_tx_hex = "0100000001..." + tx = Transaction.from_hex(raw_tx_hex) + + # Serialize a transaction + serialized_tx = tx.serialize() + print(f"Serialized transaction: {serialized_tx}") + +2. **Key Management**: + +.. code-block:: python + + from bitcoinutils.utils import h_to_b + from bitcoinutils.keys import PrivateKey + + # Create a private key from known bytes + seed_hex = "000102030405060708090a0b0c0d0e0f" + seed_bytes = h_to_b(seed_hex) + private_key = PrivateKey.from_bytes(seed_bytes) + + # Get the WIF format + wif = private_key.to_wif() + print(f"WIF: {wif}") + +3. **Taproot Address Creation**: + +.. code-block:: python + + from bitcoinutils.utils import calculate_tweak, tweak_taproot_pubkey + from bitcoinutils.keys import PrivateKey, PublicKey, P2trAddress + + # Create a key pair + private_key = PrivateKey() + public_key = private_key.get_public_key() + + # Calculate tweak for Taproot (with no script path) + tweak = calculate_tweak(public_key, None) + + # Tweak the public key for Taproot + tweaked_pubkey, is_odd = tweak_taproot_pubkey(public_key.key.to_string(), tweak) + + # Create a Taproot address + p2tr_addr = P2trAddress(witness_program=tweaked_pubkey.hex(), is_odd=is_odd) + print(f"Taproot address: {p2tr_addr.to_string()}") + +Additional Utilities +----------------- + +The module contains various other utility functions for specific Bitcoin operations: + +- Hash functions (sha256, ripemd160) +- BIP-340 tagged hashes for Taproot +- Helper functions for variable-length integer encoding +- Script utility functions + +These utilities form the foundation for many of the higher-level functions in the library and are essential for Bitcoin operations. \ No newline at end of file diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..e4816ca --- /dev/null +++ b/mypy.ini @@ -0,0 +1,35 @@ +[mypy] +python_version = 3.12 +warn_return_any = True +warn_unused_configs = True +disallow_untyped_defs = False +disallow_incomplete_defs = False +check_untyped_defs = True +disallow_untyped_decorators = False +no_implicit_optional = False +strict_optional = True +warn_redundant_casts = True +warn_unused_ignores = True +warn_no_return = True + +# Configure mypy to use the stubs +mypy_path = ./stubs + +# Library specific settings +[mypy.plugins.numpy.*] +follow_imports = skip + +[mypy-ecdsa.*] +ignore_missing_imports = False + +[mypy-base58check.*] +ignore_missing_imports = False + +[mypy-sympy.*] +ignore_missing_imports = False + +[mypy-bitcoinrpc.*] +ignore_missing_imports = False + +[mypy-hdwallet.*] +ignore_missing_imports = True \ No newline at end of file diff --git a/notebooks/keys_and_addresses.ipynb b/notebooks/keys_and_addresses.ipynb new file mode 100644 index 0000000..200909a --- /dev/null +++ b/notebooks/keys_and_addresses.ipynb @@ -0,0 +1,364 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " \"cells\": [\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"# Bitcoin Keys and Addresses\\n\",\n", + " \"\\n\",\n", + " \"This notebook demonstrates how to use the `bitcoinutils` library to work with Bitcoin keys and addresses. We'll cover:\\n\",\n", + " \"\\n\",\n", + " \"1. Creating and using private keys\\n\",\n", + " \"2. Deriving public keys\\n\",\n", + " \"3. Creating different types of addresses\\n\",\n", + " \"4. Signing and verifying messages\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Setup\\n\",\n", + " \"\\n\",\n", + " \"First, let's import the necessary modules and set up the network.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"from bitcoinutils.setup import setup\\n\",\n", + " \"from bitcoinutils.keys import PrivateKey, PublicKey, P2pkhAddress, P2shAddress, P2wpkhAddress, P2wshAddress, P2trAddress\\n\",\n", + " \"from bitcoinutils.script import Script\\n\",\n", + " \"\\n\",\n", + " \"# Always remember to setup the network\\n\",\n", + " \"# For real Bitcoin, use 'mainnet'\\n\",\n", + " \"setup('testnet')\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Working with Private Keys\\n\",\n", + " \"\\n\",\n", + " \"Private keys are the foundation of Bitcoin ownership. Let's create and manipulate private keys.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Generate a random private key\\n\",\n", + " \"random_priv_key = PrivateKey()\\n\",\n", + " \"print(f\\\"Random private key (WIF): {random_priv_key.to_wif()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Create a private key from a known WIF\\n\",\n", + " \"known_wif = \\\"cVdte9ei2xsVjmZSPtyucG43YZgNkmKTqhwiUA8M4Fc3W8Xv9cmu\\\" # Example testnet WIF\\n\",\n", + " \"priv_key_from_wif = PrivateKey.from_wif(known_wif)\\n\",\n", + " \"print(f\\\"Private key from WIF: {priv_key_from_wif.to_wif()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Create a private key from bytes\\n\",\n", + " \"import os\\n\",\n", + " \"random_bytes = os.urandom(32) # 32 random bytes\\n\",\n", + " \"priv_key_from_bytes = PrivateKey.from_bytes(random_bytes)\\n\",\n", + " \"print(f\\\"Private key from bytes (WIF): {priv_key_from_bytes.to_wif()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Create a private key from a secret exponent (an integer)\\n\",\n", + " \"secret_exponent = 12345 # Very insecure, for demonstration only!\\n\",\n", + " \"priv_key_from_int = PrivateKey(secret_exponent=secret_exponent)\\n\",\n", + " \"print(f\\\"Private key from secret exponent (WIF): {priv_key_from_int.to_wif()}\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Deriving Public Keys\\n\",\n", + " \"\\n\",\n", + " \"Public keys are derived from private keys using elliptic curve cryptography.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Generate a private key\\n\",\n", + " \"priv_key = PrivateKey()\\n\",\n", + " \"print(f\\\"Private key (WIF): {priv_key.to_wif()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Derive the corresponding public key\\n\",\n", + " \"pub_key = priv_key.get_public_key()\\n\",\n", + " \"print(f\\\"Public key (compressed): {pub_key.to_hex()}\\\")\\n\",\n", + " \"print(f\\\"Public key (uncompressed): {pub_key.to_hex(compressed=False)}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# For Taproot, we need the x-only coordinate\\n\",\n", + " \"x_only_pubkey = pub_key.to_x_only_hex()\\n\",\n", + " \"print(f\\\"X-only public key: {x_only_pubkey}\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Creating Different Types of Addresses\\n\",\n", + " \"\\n\",\n", + " \"Bitcoin supports various address formats. Let's create addresses for different use cases.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Generate a private key\\n\",\n", + " \"priv_key = PrivateKey()\\n\",\n", + " \"pub_key = priv_key.get_public_key()\\n\",\n", + " \"\\n\",\n", + " \"# Create a legacy P2PKH address\\n\",\n", + " \"p2pkh_addr = pub_key.get_address()\\n\",\n", + " \"print(f\\\"P2PKH address: {p2pkh_addr.to_string()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Create a SegWit v0 P2WPKH address\\n\",\n", + " \"p2wpkh_addr = pub_key.get_segwit_address()\\n\",\n", + " \"print(f\\\"P2WPKH address: {p2wpkh_addr.to_string()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Create a P2SH-P2WPKH address (nested SegWit)\\n\",\n", + " \"p2sh_p2wpkh_addr = pub_key.get_p2sh_p2wpkh_address()\\n\",\n", + " \"print(f\\\"P2SH-P2WPKH address: {p2sh_p2wpkh_addr.to_string()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Create a Taproot (SegWit v1) address\\n\",\n", + " \"p2tr_addr = pub_key.get_taproot_address()\\n\",\n", + " \"print(f\\\"P2TR address: {p2tr_addr.to_string()}\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Creating Addresses from Scripts\\n\",\n", + " \"\\n\",\n", + " \"Bitcoin allows creating addresses from scripts, which enables more complex spending conditions.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Create two public keys for a multisig script\\n\",\n", + " \"priv_key1 = PrivateKey()\\n\",\n", + " \"priv_key2 = PrivateKey()\\n\",\n", + " \"pub_key1 = priv_key1.get_public_key()\\n\",\n", + " \"pub_key2 = priv_key2.get_public_key()\\n\",\n", + " \"\\n\",\n", + " \"# Create a 2-of-2 multisig script\\n\",\n", + " \"multisig_script = Script([2, pub_key1.to_hex(), pub_key2.to_hex(), 2, 'OP_CHECKMULTISIG'])\\n\",\n", + " \"print(f\\\"Multisig script: {multisig_script.to_string()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Create a P2SH address from the multisig script\\n\",\n", + " \"p2sh_addr = P2shAddress.from_script(multisig_script)\\n\",\n", + " \"print(f\\\"P2SH address: {p2sh_addr.to_string()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Create a P2WSH address from the multisig script\\n\",\n", + " \"p2wsh_addr = multisig_script.get_segwit_address()\\n\",\n", + " \"print(f\\\"P2WSH address: {p2wsh_addr.to_string()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Create a P2SH-P2WSH address from the multisig script\\n\",\n", + " \"p2sh_p2wsh_addr = multisig_script.get_p2sh_p2wsh_address()\\n\",\n", + " \"print(f\\\"P2SH-P2WSH address: {p2sh_p2wsh_addr.to_string()}\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Taproot Addresses with Script Paths\\n\",\n", + " \"\\n\",\n", + " \"Taproot allows defining alternative spending conditions through script paths.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Generate a key for the Taproot internal key\\n\",\n", + " \"internal_key = PrivateKey().get_public_key()\\n\",\n", + " \"\\n\",\n", + " \"# Create script paths\\n\",\n", + " \"# Example 1: Simple script path with a single public key\\n\",\n", + " \"script_pub_key = PrivateKey().get_public_key()\\n\",\n", + " \"script_path1 = Script([script_pub_key.to_hex(), 'OP_CHECKSIG'])\\n\",\n", + " \"\\n\",\n", + " \"# Example 2: Time-locked script path\\n\",\n", + " \"script_path2 = Script(['OP_DUP', 'OP_HASH160', script_pub_key.to_hash160(), 'OP_EQUALVERIFY', 'OP_CHECKSIG', 'OP_CHECKSEQUENCEVERIFY'])\\n\",\n", + " \"\\n\",\n", + " \"# Create a Taproot address with script paths\\n\",\n", + " \"p2tr_with_scripts_addr = internal_key.get_taproot_address([script_path1, script_path2])\\n\",\n", + " \"print(f\\\"Taproot address with script paths: {p2tr_with_scripts_addr.to_string()}\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Signing and Verifying Messages\\n\",\n", + " \"\\n\",\n", + " \"Private and public keys can be used to sign and verify messages.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Generate a private key\\n\",\n", + " \"priv_key = PrivateKey()\\n\",\n", + " \"pub_key = priv_key.get_public_key()\\n\",\n", + " \"address = pub_key.get_address()\\n\",\n", + " \"\\n\",\n", + " \"# Sign a message\\n\",\n", + " \"message = \\\"Hello, Bitcoin!\\\"\\n\",\n", + " \"signature = priv_key.sign_message(message)\\n\",\n", + " \"print(f\\\"Message: {message}\\\")\\n\",\n", + " \"print(f\\\"Signature: {signature}\\\")\\n\",\n", + " \"print(f\\\"Address: {address.to_string()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Verify the message\\n\",\n", + " \"is_valid = PublicKey.verify_message(address.to_string(), signature, message)\\n\",\n", + " \"print(f\\\"Signature valid: {is_valid}\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Working with Address Objects Directly\\n\",\n", + " \"\\n\",\n", + " \"You can also create address objects directly from address strings or hash160 values.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Create addresses from strings\\n\",\n", + " \"p2pkh_addr_str = \\\"mzF2sbdxcMqKFLoakdBcvZpUXMjgiXGZW1\\\" # Example testnet address\\n\",\n", + " \"p2pkh_addr = P2pkhAddress.from_address(p2pkh_addr_str)\\n\",\n", + " \"print(f\\\"P2PKH address from string: {p2pkh_addr.to_string()}\\\")\\n\",\n", + " \"print(f\\\"P2PKH hash160: {p2pkh_addr.to_hash160()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Create an address from a hash160 value\\n\",\n", + " \"hash160_value = p2pkh_addr.to_hash160()\\n\",\n", + " \"p2pkh_addr_from_hash = P2pkhAddress.from_hash160(hash160_value)\\n\",\n", + " \"print(f\\\"P2PKH address from hash160: {p2pkh_addr_from_hash.to_string()}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Get the scriptPubKey for an address\\n\",\n", + " \"script_pub_key = p2pkh_addr.to_script_pub_key()\\n\",\n", + " \"print(f\\\"ScriptPubKey: {script_pub_key.to_string()}\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Converting Between Address Types\\n\",\n", + " \"\\n\",\n", + " \"When you have a private key, you can easily convert between different address types.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Generate a private key\\n\",\n", + " \"priv_key = PrivateKey()\\n\",\n", + " \"pub_key = priv_key.get_public_key()\\n\",\n", + " \"\\n\",\n", + " \"# Generate different address types from the same key\\n\",\n", + " \"addresses = {\\n\",\n", + " \" \\\"P2PKH\\\": pub_key.get_address().to_string(),\\n\",\n", + " \" \\\"P2WPKH\\\": pub_key.get_segwit_address().to_string(),\\n\",\n", + " \" \\\"P2SH-P2WPKH\\\": pub_key.get_p2sh_p2wpkh_address().to_string(),\\n\",\n", + " \" \\\"P2TR\\\": pub_key.get_taproot_address().to_string()\\n\",\n", + " \"}\\n\",\n", + " \"\\n\",\n", + " \"print(\\\"Different address types from the same key:\\\")\\n\",\n", + " \"for addr_type, addr in addresses.items():\\n\",\n", + " \" print(f\\\"{addr_type}: {addr}\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Conclusion\\n\",\n", + " \"\\n\",\n", + " \"This notebook demonstrated how to work with Bitcoin keys and addresses using the `bitcoinutils` library. We covered:\\n\",\n", + " \"\\n\",\n", + " \"1. Creating and managing private keys\\n\",\n", + " \"2. Deriving public keys\\n\",\n", + " \"3. Creating various types of addresses\\n\",\n", + " \"4. Working with script-based addresses\\n\",\n", + " \"5. Signing and verifying messages\\n\",\n", + " \"\\n\",\n", + " \"These building blocks are essential for creating and working with Bitcoin transactions.\"\n", + " ]\n", + " }\n", + " ],\n", + " \"metadata\": {\n", + " \"kernelspec\": {\n", + " \"display_name\": \"Python 3\",\n", + " \"language\": \"python\",\n", + " \"name\": \"python3\"\n", + " },\n", + " \"language_info\": {\n", + " \"codemirror_mode\": {\n", + " \"name\": \"ipython\",\n", + " \"version\": 3\n", + " },\n", + " \"file_extension\": \".py\",\n", + " \"mimetype\": \"text/x-python\",\n", + " \"name\": \"python\",\n", + " \"nbconvert_exporter\": \"python\",\n", + " \"pygments_lexer\": \"ipython3\",\n", + " \"version\": \"3.7.6\"\n", + " }\n", + " },\n", + " \"nbformat\": 4,\n", + " \"nbformat_minor\": 4\n", + "}" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/proxy.ipynb b/notebooks/proxy.ipynb new file mode 100644 index 0000000..b161eb7 --- /dev/null +++ b/notebooks/proxy.ipynb @@ -0,0 +1,305 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " \"cells\": [\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"# Bitcoin Node Proxy\\n\",\n", + " \"\\n\",\n", + " \"This notebook demonstrates how to use the `NodeProxy` class from the `bitcoinutils` library to interact with a Bitcoin node.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Setup\\n\",\n", + " \"\\n\",\n", + " \"First, let's import the necessary modules and set up the network.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"from bitcoinutils.setup import setup\\n\",\n", + " \"from bitcoinutils.proxy import NodeProxy\\n\",\n", + " \"\\n\",\n", + " \"# Always remember to setup the network\\n\",\n", + " \"setup('testnet')\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Connecting to a Bitcoin Node\\n\",\n", + " \"\\n\",\n", + " \"To connect to a Bitcoin node, you need to create a `NodeProxy` instance with the appropriate credentials.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Replace these with your actual RPC credentials\\n\",\n", + " \"RPC_USER = 'your_rpc_username'\\n\",\n", + " \"RPC_PASSWORD = 'your_rpc_password'\\n\",\n", + " \"\\n\",\n", + " \"# Create a NodeProxy instance\\n\",\n", + " \"# If you're running Bitcoin Core locally, you can use the default host and port\\n\",\n", + " \"proxy = NodeProxy(RPC_USER, RPC_PASSWORD)\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Getting Blockchain Information\\n\",\n", + " \"\\n\",\n", + " \"Let's get some basic information about the blockchain.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Get the current block count\\n\",\n", + " \"block_count = proxy.get_block_count()\\n\",\n", + " \"print(f\\\"Current block count: {block_count}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Get detailed blockchain information\\n\",\n", + " \"blockchain_info = proxy.get_blockchain_info()\\n\",\n", + " \"print(f\\\"Chain: {blockchain_info['chain']}\\\")\\n\",\n", + " \"print(f\\\"Difficulty: {blockchain_info['difficulty']}\\\")\\n\",\n", + " \"print(f\\\"Median time: {blockchain_info['mediantime']}\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Working with Blocks\\n\",\n", + " \"\\n\",\n", + " \"Now let's retrieve information about a specific block.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Get the hash of the latest block\\n\",\n", + " \"latest_block_hash = proxy.get_block_hash(block_count)\\n\",\n", + " \"print(f\\\"Latest block hash: {latest_block_hash}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Get the block data\\n\",\n", + " \"block_data = proxy.get_block(latest_block_hash)\\n\",\n", + " \"\\n\",\n", + " \"# Display some block information\\n\",\n", + " \"print(f\\\"Block version: {block_data['version']}\\\")\\n\",\n", + " \"print(f\\\"Block time: {block_data['time']}\\\")\\n\",\n", + " \"print(f\\\"Number of transactions: {len(block_data['tx'])}\\\")\\n\",\n", + " \"print(f\\\"Merkle root: {block_data['merkleroot']}\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Wallet Operations\\n\",\n", + " \"\\n\",\n", + " \"Let's perform some wallet operations.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Get wallet information\\n\",\n", + " \"wallet_info = proxy.get_wallet_info()\\n\",\n", + " \"print(f\\\"Wallet name: {wallet_info['walletname']}\\\")\\n\",\n", + " \"print(f\\\"Balance: {wallet_info['balance']} BTC\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Generate a new address\\n\",\n", + " \"new_address = proxy.get_new_address(label=\\\"example\\\")\\n\",\n", + " \"print(f\\\"New address: {new_address}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# List unspent transaction outputs (UTXOs)\\n\",\n", + " \"utxos = proxy.list_unspent()\\n\",\n", + " \"print(f\\\"Number of UTXOs: {len(utxos)}\\\")\\n\",\n", + " \"if len(utxos) > 0:\\n\",\n", + " \" print(f\\\"First UTXO: {utxos[0]}\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Working with Transactions\\n\",\n", + " \"\\n\",\n", + " \"Now let's create and send a transaction.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# This is a complete example of creating and sending a transaction\\n\",\n", + " \"# You should have at least one UTXO in your wallet to run this\\n\",\n", + " \"\\n\",\n", + " \"# First, check if we have any UTXOs\\n\",\n", + " \"utxos = proxy.list_unspent()\\n\",\n", + " \"if len(utxos) == 0:\\n\",\n", + " \" print(\\\"No UTXOs available. Please send some funds to your wallet.\\\")\\n\",\n", + " \"else:\\n\",\n", + " \" # Select the first UTXO\\n\",\n", + " \" utxo = utxos[0]\\n\",\n", + " \" \\n\",\n", + " \" # Create a recipient address (here we use a new address from our own wallet for demonstration)\\n\",\n", + " \" recipient_address = proxy.get_new_address(label=\\\"recipient\\\")\\n\",\n", + " \" \\n\",\n", + " \" # Prepare inputs and outputs\\n\",\n", + " \" inputs = [{\\\"txid\\\": utxo[\\\"txid\\\"], \\\"vout\\\": utxo[\\\"vout\\\"]}]\\n\",\n", + " \" \\n\",\n", + " \" # Calculate amount to send (original amount minus fee)\\n\",\n", + " \" fee = 0.0001 # Fixed fee for simplicity\\n\",\n", + " \" amount_to_send = utxo[\\\"amount\\\"] - fee\\n\",\n", + " \" \\n\",\n", + " \" # Create outputs\\n\",\n", + " \" outputs = {recipient_address: amount_to_send}\\n\",\n", + " \" \\n\",\n", + " \" # Create a raw transaction\\n\",\n", + " \" raw_tx = proxy.create_raw_transaction(inputs, outputs)\\n\",\n", + " \" print(f\\\"Raw transaction created:\\\\n{raw_tx}\\\")\\n\",\n", + " \" \\n\",\n", + " \" # Sign the transaction\\n\",\n", + " \" signed_tx = proxy.sign_raw_transaction_with_wallet(raw_tx)\\n\",\n", + " \" if signed_tx[\\\"complete\\\"]:\\n\",\n", + " \" print(\\\"Transaction signed successfully!\\\")\\n\",\n", + " \" \\n\",\n", + " \" # Uncomment the following line to actually send the transaction\\n\",\n", + " \" # tx_id = proxy.send_raw_transaction(signed_tx[\\\"hex\\\"])\\n\",\n", + " \" # print(f\\\"Transaction sent! TXID: {tx_id}\\\")\\n\",\n", + " \" \\n\",\n", + " \" print(\\\"Transaction not sent (code is commented out for safety).\\\")\\n\",\n", + " \" else:\\n\",\n", + " \" print(\\\"Failed to sign transaction.\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Using Direct RPC Methods\\n\",\n", + " \"\\n\",\n", + " \"The `NodeProxy` class provides convenient methods for common operations, but you can also call any RPC method directly.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"# Call getnetworkinfo directly\\n\",\n", + " \"network_info = proxy.call('getnetworkinfo')\\n\",\n", + " \"print(f\\\"Bitcoin Core version: {network_info['version']}\\\")\\n\",\n", + " \"print(f\\\"Protocol version: {network_info['protocolversion']}\\\")\\n\",\n", + " \"print(f\\\"Connections: {network_info['connections']}\\\")\\n\",\n", + " \"\\n\",\n", + " \"# Alternative way to call RPC methods directly\\n\",\n", + " \"uptime = proxy('uptime')\\n\",\n", + " \"print(f\\\"Node uptime: {uptime} seconds\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Error Handling\\n\",\n", + " \"\\n\",\n", + " \"Let's see how to handle errors when working with the NodeProxy.\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"code\",\n", + " \"execution_count\": null,\n", + " \"metadata\": {},\n", + " \"outputs\": [],\n", + " \"source\": [\n", + " \"from bitcoinutils.proxy import RPCError\\n\",\n", + " \"\\n\",\n", + " \"try:\\n\",\n", + " \" # Try to get a non-existent block\\n\",\n", + " \" block = proxy.get_block(\\\"0000000000000000000000000000000000000000000000000000000000000000\\\")\\n\",\n", + " \"except RPCError as e:\\n\",\n", + " \" print(f\\\"RPC Error: {e}\\\")\\n\",\n", + " \" if e.code is not None:\\n\",\n", + " \" print(f\\\"Error code: {e.code}\\\")\"\n", + " ]\n", + " },\n", + " {\n", + " \"cell_type\": \"markdown\",\n", + " \"metadata\": {},\n", + " \"source\": [\n", + " \"## Conclusion\\n\",\n", + " \"\\n\",\n", + " \"The `NodeProxy` class provides a convenient interface to interact with a Bitcoin node. It offers methods for common operations as well as the ability to call any RPC method directly.\\n\",\n", + " \"\\n\",\n", + " \"For more information, refer to the [Bitcoin Core RPC documentation](https://developer.bitcoin.org/reference/rpc/index.html).\"\n", + " ]\n", + " }\n", + " ],\n", + " \"metadata\": {\n", + " \"kernelspec\": {\n", + " \"display_name\": \"Python 3\",\n", + " \"language\": \"python\",\n", + " \"name\": \"python3\"\n", + " },\n", + " \"language_info\": {\n", + " \"codemirror_mode\": {\n", + " \"name\": \"ipython\",\n", + " \"version\": 3\n", + " },\n", + " \"file_extension\": \".py\",\n", + " \"mimetype\": \"text/x-python\",\n", + " \"name\": \"python\",\n", + " \"nbconvert_exporter\": \"python\",\n", + " \"pygments_lexer\": \"ipython3\",\n", + " \"version\": \"3.7.6\"\n", + " }\n", + " },\n", + " \"nbformat\": 4,\n", + " \"nbformat_minor\": 4\n", + "}" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/stubs/base58check.pyi b/stubs/base58check.pyi new file mode 100644 index 0000000..9c5650b --- /dev/null +++ b/stubs/base58check.pyi @@ -0,0 +1,13 @@ +from typing import Union, overload + +@overload +def b58encode(v: bytes) -> bytes: ... + +@overload +def b58encode(v: str) -> str: ... + +@overload +def b58decode(v: bytes) -> bytes: ... + +@overload +def b58decode(v: str) -> str: ... \ No newline at end of file diff --git a/stubs/bitcoinrpc/authproxy.pyi b/stubs/bitcoinrpc/authproxy.pyi new file mode 100644 index 0000000..472671c --- /dev/null +++ b/stubs/bitcoinrpc/authproxy.pyi @@ -0,0 +1,15 @@ +from typing import Any, Callable, Dict, List, Optional, Union, overload + +class JSONRPCException(Exception): + error: Dict[str, Any] + + def __init__(self, rpc_error: Dict[str, Any]) -> None: ... + + +class AuthServiceProxy: + def __init__(self, service_url: str, service_name: Optional[str] = None, timeout: int = 30, + connection: Optional[Any] = None) -> None: ... + + def __getattr__(self, name: str) -> 'AuthServiceProxy': ... + + def __call__(self, *args: Any) -> Any: ... \ No newline at end of file diff --git a/stubs/ecdsa.pyi b/stubs/ecdsa.pyi new file mode 100644 index 0000000..5b36b56 --- /dev/null +++ b/stubs/ecdsa.pyi @@ -0,0 +1,183 @@ +from typing import Any, Callable, List, Optional, Tuple, Type, Union, overload +import hashlib +import six + +class BadSignatureError(Exception): ... +class BadDigestError(Exception): ... + +class PRNG: + @classmethod + def get_random_number_generator(cls) -> "PRNG": ... + def __init__(self, seed: Optional[bytes] = None) -> None: ... + def __call__(self, limit: int) -> int: ... + + +class Curve: + name: str + openssl_name: Optional[str] + baselen: int + + @property + def G(self) -> "Point": ... + @property + def generator(self) -> "generator.Generator": ... + @property + def order(self) -> int: ... + @property + def n(self) -> int: ... + @property + def p(self) -> int: ... + @property + def a(self) -> int: ... + @property + def b(self) -> int: ... + @property + def bits(self) -> int: ... + + def contains_point(self, x: int, y: int) -> bool: ... + + +class Point: + def __init__(self, curve: Curve, x: int, y: int, order: Optional[int] = None) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __add__(self, other: "Point") -> "Point": ... + def __mul__(self, scalar: int) -> "Point": ... + def __rmul__(self, scalar: int) -> "Point": ... + def double(self) -> "Point": ... + + +class PointJacobi: + def __init__(self, curve: Curve, x: int, y: int, z: int, order: Optional[int] = None) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __add__(self, other: "PointJacobi") -> "PointJacobi": ... + def __mul__(self, scalar: int) -> "PointJacobi": ... + def __rmul__(self, scalar: int) -> "PointJacobi": ... + def double(self) -> "PointJacobi": ... + + +class CurveFp(Curve): + def __init__(self, p: int, a: int, b: int, n: int, gx: int, gy: int, name: str, + oid: Optional[List[int]] = None, openssl_name: Optional[str] = None) -> None: ... + + +class NIST192p(CurveFp): ... +class NIST224p(CurveFp): ... +class NIST256p(CurveFp): ... +class NIST384p(CurveFp): ... +class NIST521p(CurveFp): ... +class SECP256k1(CurveFp): ... + + +class SigningKey: + @classmethod + def generate(cls, curve: Curve = NIST192p, entropy: Optional[Callable[[int], bytes]] = None, + hashfunc: Optional[Callable[[bytes], Any]] = None) -> "SigningKey": ... + + @classmethod + def from_secret_exponent(cls, secexp: int, curve: Curve = NIST192p, + hashfunc: Optional[Callable[[bytes], Any]] = None) -> "SigningKey": ... + + @classmethod + def from_string(cls, string: bytes, curve: Curve = NIST192p, + hashfunc: Optional[Callable[[bytes], Any]] = None) -> "SigningKey": ... + + @classmethod + def from_pem(cls, string: Union[bytes, str], hashfunc: Optional[Callable[[bytes], Any]] = None) -> "SigningKey": ... + + @classmethod + def from_der(cls, string: bytes, hashfunc: Optional[Callable[[bytes], Any]] = None) -> "SigningKey": ... + + def __init__(self, _error_do_not_use_init: Any = None) -> None: ... + def get_verifying_key(self) -> "VerifyingKey": ... + def sign_deterministic(self, data: bytes, hashfunc: Optional[Callable[[bytes], Any]] = None, + sigencode: Callable[..., Any] = None, extra_entropy: Optional[bytes] = None) -> bytes: ... + + def sign(self, data: bytes, entropy: Optional[Callable[[int], bytes]] = None, + sigencode: Callable[..., Any] = None) -> bytes: ... + + def sign_digest(self, digest: bytes, entropy: Optional[Callable[[int], bytes]] = None, + sigencode: Callable[..., Any] = None) -> bytes: ... + + def sign_digest_deterministic(self, digest: bytes, hashfunc: Optional[Callable[[bytes], Any]] = None, + sigencode: Callable[..., Any] = None, extra_entropy: Optional[bytes] = None) -> bytes: ... + + def to_string(self) -> bytes: ... + def to_pem(self, point_encoding: str = "uncompressed") -> bytes: ... + def to_der(self, point_encoding: str = "uncompressed") -> bytes: ... + + +class VerifyingKey: + @classmethod + def from_string(cls, string: bytes, curve: Curve = NIST192p, + hashfunc: Optional[Callable[[bytes], Any]] = None, + validate_point: bool = True) -> "VerifyingKey": ... + + @classmethod + def from_pem(cls, string: Union[bytes, str], hashfunc: Optional[Callable[[bytes], Any]] = None) -> "VerifyingKey": ... + + @classmethod + def from_der(cls, string: bytes, hashfunc: Optional[Callable[[bytes], Any]] = None) -> "VerifyingKey": ... + + @classmethod + def from_public_point(cls, point: Point, curve: Curve = NIST192p, + hashfunc: Optional[Callable[[bytes], Any]] = None) -> "VerifyingKey": ... + + @classmethod + def from_public_key_recovery(cls, signature: bytes, data: bytes, curve: Curve = NIST192p, + hashfunc: Optional[Callable[[bytes], Any]] = None, + sigdecode: Optional[Callable[..., Tuple[int, int]]] = None) -> List["VerifyingKey"]: ... + + @classmethod + def from_public_key_recovery_with_digest(cls, signature: bytes, digest: bytes, curve: Curve = NIST192p, + sigdecode: Optional[Callable[..., Tuple[int, int]]] = None) -> List["VerifyingKey"]: ... + + def __init__(self, _error_do_not_use_init: Any = None) -> None: ... + def verify(self, signature: bytes, data: bytes, hashfunc: Optional[Callable[[bytes], Any]] = None, + sigdecode: Optional[Callable[..., Tuple[int, int]]] = None) -> bool: ... + + def verify_digest(self, signature: bytes, digest: bytes, + sigdecode: Optional[Callable[..., Tuple[int, int]]] = None) -> bool: ... + + def to_string(self, encoding: str = "uncompressed") -> bytes: ... + def to_pem(self, point_encoding: str = "uncompressed") -> bytes: ... + def to_der(self, point_encoding: str = "uncompressed") -> bytes: ... + + +class BadSignatureError(Exception): ... + + +def int_to_string(x: int, orderlen: int) -> bytes: ... +def string_to_int(s: bytes) -> int: ... + + +class util: + @staticmethod + def randrange(order: int, entropy: Optional[Callable[[int], bytes]] = None) -> int: ... + @staticmethod + def randrange_from_seed__trytryagain(seed: bytes, order: int) -> Tuple[int, bytes]: ... + + +def sigencode_strings(r: int, s: int, order: Optional[int] = None) -> Tuple[bytes, bytes]: ... +def sigencode_string(r: int, s: int, order: Optional[int] = None) -> bytes: ... +def sigencode_der(r: int, s: int, order: Optional[int] = None) -> bytes: ... +def sigdecode_string(signature: bytes, order: Optional[int] = None) -> Tuple[int, int]: ... +def sigdecode_der(sig_der: bytes, order: Optional[int] = None) -> Tuple[int, int]: ... + + +class ellipticcurve: + class CurveFp: + def __init__(self, p: int, a: int, b: int) -> None: ... + def contains_point(self, x: int, y: int) -> bool: ... + + class Point: + def __init__(self, curve: CurveFp, x: int, y: int, order: Optional[int] = None) -> None: ... + def __add__(self, other: "ellipticcurve.Point") -> "ellipticcurve.Point": ... + def __mul__(self, scalar: int) -> "ellipticcurve.Point": ... + def __rmul__(self, scalar: int) -> "ellipticcurve.Point": ... + + +class numbertheory: + @staticmethod + def inverse_mod(a: int, m: int) -> int: ... + @staticmethod + def square_root_mod_prime(a: int, p: int) -> int: ... \ No newline at end of file diff --git a/stubs/sympy.pyi b/stubs/sympy.pyi new file mode 100644 index 0000000..6874f7f --- /dev/null +++ b/stubs/sympy.pyi @@ -0,0 +1,8 @@ +from typing import Optional, Tuple, Union, List, Any, Callable, overload + +# We'll only define the parts of sympy that are used in the bitcoin-utils code +# This is a minimal stub file for the specific functionality used + +class ntheory: + @staticmethod + def sqrt_mod(a: int, p: int, all_roots: bool = False) -> Optional[Union[int, Tuple[int, int]]]: ... \ No newline at end of file diff --git a/tests/test_proxy.py b/tests/test_proxy.py new file mode 100644 index 0000000..7a17c4f --- /dev/null +++ b/tests/test_proxy.py @@ -0,0 +1,363 @@ +# Copyright (C) 2018-2025 The python-bitcoin-utils developers +# +# This file is part of python-bitcoin-utils +# +# It is subject to the license terms in the LICENSE file found in the top-level +# directory of this distribution. +# +# No part of python-bitcoin-utils, including this file, may be copied, modified, +# propagated, or distributed except according to the terms contained in the +# LICENSE file. + +import unittest +from unittest.mock import patch, MagicMock, PropertyMock +import json +import sys +import os + +# Add parent directory to the path to import bitcoinutils +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from bitcoinutils.proxy import NodeProxy, RPCError +from bitcoinutils.constants import NETWORK_DEFAULT_PORTS +from bitcoinutils.setup import get_network, setup + + +class TestNodeProxy(unittest.TestCase): + """Test cases for the NodeProxy class.""" + + def setUp(self): + """Set up the test environment.""" + # Set up the network to testnet for testing + setup('testnet') + + # Create mock for AuthServiceProxy + self.auth_service_proxy_patcher = patch('bitcoinutils.proxy.AuthServiceProxy') + self.mock_auth_service_proxy = self.auth_service_proxy_patcher.start() + + # Mock instance of AuthServiceProxy + self.mock_proxy_instance = MagicMock() + self.mock_auth_service_proxy.return_value = self.mock_proxy_instance + + def tearDown(self): + """Clean up the test environment.""" + self.auth_service_proxy_patcher.stop() + + def test_init_with_defaults(self): + """Test initialization with default parameters.""" + proxy = NodeProxy('testuser', 'testpass') + + # Verify AuthServiceProxy was called with correct parameters + self.mock_auth_service_proxy.assert_called_once_with( + f"http://testuser:testpass@127.0.0.1:{NETWORK_DEFAULT_PORTS[get_network()]}", + timeout=30 + ) + + def test_init_with_custom_params(self): + """Test initialization with custom parameters.""" + proxy = NodeProxy( + 'testuser', + 'testpass', + host='192.168.1.1', + port=8888, + timeout=60, + use_https=True + ) + + # Verify AuthServiceProxy was called with correct parameters + self.mock_auth_service_proxy.assert_called_once_with( + "https://testuser:testpass@192.168.1.1:8888", + timeout=60 + ) + + def test_init_missing_credentials(self): + """Test initialization with missing credentials.""" + with self.assertRaises(ValueError): + proxy = NodeProxy('', 'testpass') + + with self.assertRaises(ValueError): + proxy = NodeProxy('testuser', '') + + def test_call_method(self): + """Test calling a method through the proxy.""" + # Set up the return value for the mock + self.mock_proxy_instance.getblockcount.return_value = 123456 + + proxy = NodeProxy('testuser', 'testpass') + result = proxy.call('getblockcount') + + # Verify the method was called and the result is correct + self.mock_proxy_instance.getblockcount.assert_called_once() + self.assertEqual(result, 123456) + + def test_call_method_with_params(self): + """Test calling a method with parameters.""" + # Set up the return value for the mock + mock_block = { + 'hash': '000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d', + 'confirmations': 1000, + 'size': 1234, + 'height': 123456, + 'version': 0x20000000, + 'merkleroot': '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + 'time': 1600000000, + 'nonce': 123456789, + 'bits': '1d00ffff', + 'difficulty': 1, + 'previousblockhash': 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + 'nextblockhash': 'fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321', + 'tx': ['tx1', 'tx2', 'tx3'] + } + self.mock_proxy_instance.getblock.return_value = mock_block + + proxy = NodeProxy('testuser', 'testpass') + result = proxy.call('getblock', '000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d') + + # Verify the method was called with the correct parameters + self.mock_proxy_instance.getblock.assert_called_once_with( + '000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d' + ) + self.assertEqual(result, mock_block) + + def test_call_nonexistent_method(self): + """Test calling a non-existent method.""" + # Set up the mock to raise an exception + self.mock_proxy_instance.nonexistentmethod.side_effect = Exception("Method not found") + + proxy = NodeProxy('testuser', 'testpass') + + # Verify that the correct exception is raised + with self.assertRaises(RPCError): + proxy.call('nonexistentmethod') + + def test_direct_call(self): + """Test calling a method using __call__.""" + # Set up the return value for the mock + self.mock_proxy_instance.getblockcount.return_value = 123456 + + proxy = NodeProxy('testuser', 'testpass') + result = proxy('getblockcount') + + # Verify the method was called and the result is correct + self.mock_proxy_instance.getblockcount.assert_called_once() + self.assertEqual(result, 123456) + + def test_get_blockchain_info(self): + """Test the get_blockchain_info method.""" + # Set up the return value for the mock + mock_info = { + 'chain': 'test', + 'blocks': 123456, + 'headers': 123456, + 'bestblockhash': '000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d', + 'difficulty': 1, + 'mediantime': 1600000000, + 'verificationprogress': 0.9999, + 'initialblockdownload': False, + 'chainwork': '0000000000000000000000000000000000000000000000000000000000000000', + 'size_on_disk': 1234567890, + 'pruned': False + } + self.mock_proxy_instance.getblockchaininfo.return_value = mock_info + + proxy = NodeProxy('testuser', 'testpass') + result = proxy.get_blockchain_info() + + # Verify the method was called and the result is correct + self.mock_proxy_instance.getblockchaininfo.assert_called_once() + self.assertEqual(result, mock_info) + self.assertEqual(result['chain'], 'test') + self.assertEqual(result['blocks'], 123456) + + def test_get_block_count(self): + """Test the get_block_count method.""" + # Set up the return value for the mock + self.mock_proxy_instance.getblockcount.return_value = 123456 + + proxy = NodeProxy('testuser', 'testpass') + result = proxy.get_block_count() + + # Verify the method was called and the result is correct + self.mock_proxy_instance.getblockcount.assert_called_once() + self.assertEqual(result, 123456) + + def test_get_block_hash(self): + """Test the get_block_hash method.""" + # Set up the return value for the mock + self.mock_proxy_instance.getblockhash.return_value = '000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d' + + proxy = NodeProxy('testuser', 'testpass') + result = proxy.get_block_hash(123456) + + # Verify the method was called with the correct parameters + self.mock_proxy_instance.getblockhash.assert_called_once_with(123456) + self.assertEqual(result, '000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d') + + def test_get_block(self): + """Test the get_block method.""" + # Set up the return value for the mock + mock_block = { + 'hash': '000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d', + 'confirmations': 1000, + 'size': 1234, + 'height': 123456, + 'version': 0x20000000, + 'merkleroot': '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + 'time': 1600000000, + 'nonce': 123456789, + 'bits': '1d00ffff', + 'difficulty': 1, + 'previousblockhash': 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', + 'nextblockhash': 'fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321', + 'tx': ['tx1', 'tx2', 'tx3'] + } + self.mock_proxy_instance.getblock.return_value = mock_block + + proxy = NodeProxy('testuser', 'testpass') + result = proxy.get_block('000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d') + + # Verify the method was called with the correct parameters + self.mock_proxy_instance.getblock.assert_called_once_with( + '000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d', 1 + ) + self.assertEqual(result, mock_block) + + # Test with different verbosity + self.mock_proxy_instance.getblock.reset_mock() + self.mock_proxy_instance.getblock.return_value = mock_block + + result = proxy.get_block('000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d', 2) + + # Verify the method was called with the correct parameters + self.mock_proxy_instance.getblock.assert_called_once_with( + '000000000000000000024bead8df69990852c202db0e0097c1a12ea637d7e96d', 2 + ) + self.assertEqual(result, mock_block) + + def test_wallet_methods(self): + """Test wallet-related methods.""" + # Set up return values for the mocks + self.mock_proxy_instance.getbalance.return_value = 1.23456789 + self.mock_proxy_instance.getwalletinfo.return_value = { + 'walletname': 'test_wallet', + 'walletversion': 169900, + 'balance': 1.23456789, + 'unconfirmed_balance': 0.0, + 'immature_balance': 0.0, + 'txcount': 100, + 'keypoololdest': 1600000000, + 'keypoolsize': 1000, + 'paytxfee': 0.0, + 'hdseedid': '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef', + 'private_keys_enabled': True + } + self.mock_proxy_instance.getnewaddress.return_value = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx' + + proxy = NodeProxy('testuser', 'testpass') + + # Test get_balance + balance = proxy.get_balance() + self.mock_proxy_instance.getbalance.assert_called_once_with('*', 0, False) + self.assertEqual(balance, 1.23456789) + + # Test get_wallet_info + wallet_info = proxy.get_wallet_info() + self.mock_proxy_instance.getwalletinfo.assert_called_once() + self.assertEqual(wallet_info['balance'], 1.23456789) + self.assertEqual(wallet_info['walletname'], 'test_wallet') + + # Test get_new_address + address = proxy.get_new_address() + self.mock_proxy_instance.getnewaddress.assert_called_once_with("") + self.assertEqual(address, 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx') + + # Test get_new_address with parameters + self.mock_proxy_instance.getnewaddress.reset_mock() + self.mock_proxy_instance.getnewaddress.return_value = 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx' + + address = proxy.get_new_address(label="test", address_type="bech32") + self.mock_proxy_instance.getnewaddress.assert_called_once_with("test", "bech32") + self.assertEqual(address, 'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx') + + def test_transaction_methods(self): + """Test transaction-related methods.""" + # Set up return values for the mocks + self.mock_proxy_instance.createrawtransaction.return_value = "0100000001a5fb7a3bc532e38b0f5bad8d87fe8858a0b9f648" + self.mock_proxy_instance.signrawtransactionwithwallet.return_value = { + 'hex': '0100000001a5fb7a3bc532e38b0f5bad8d87fe8858a0b9f648', + 'complete': True + } + self.mock_proxy_instance.sendrawtransaction.return_value = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + self.mock_proxy_instance.getrawtransaction.return_value = '0100000001a5fb7a3bc532e38b0f5bad8d87fe8858a0b9f648' + + # Create a NodeProxy instance + proxy = NodeProxy('testuser', 'testpass') + + # Test create_raw_transaction + inputs = [{"txid": "a5fb7a3bc532e38b0f5bad8d87fe8858a0b9f648", "vout": 0}] + outputs = {"tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx": 0.001} + + raw_tx = proxy.create_raw_transaction(inputs, outputs) + self.mock_proxy_instance.createrawtransaction.assert_called_once_with( + inputs, outputs, 0, False + ) + self.assertEqual(raw_tx, "0100000001a5fb7a3bc532e38b0f5bad8d87fe8858a0b9f648") + + # Test sign_raw_transaction_with_wallet + signed_tx = proxy.sign_raw_transaction_with_wallet(raw_tx) + self.mock_proxy_instance.signrawtransactionwithwallet.assert_called_once_with(raw_tx) + self.assertEqual(signed_tx['hex'], "0100000001a5fb7a3bc532e38b0f5bad8d87fe8858a0b9f648") + self.assertTrue(signed_tx['complete']) + + # Test send_raw_transaction + tx_id = proxy.send_raw_transaction(signed_tx['hex']) + self.mock_proxy_instance.sendrawtransaction.assert_called_once_with(signed_tx['hex']) + self.assertEqual(tx_id, '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef') + + # Test get_raw_transaction + raw_tx = proxy.get_raw_transaction(tx_id) + self.mock_proxy_instance.getrawtransaction.assert_called_once_with(tx_id, False) + self.assertEqual(raw_tx, '0100000001a5fb7a3bc532e38b0f5bad8d87fe8858a0b9f648') + + def test_error_handling(self): + """Test error handling in the NodeProxy class.""" + # Set up the mock to raise an exception with an error code + error_response = { + 'code': -8, + 'message': 'Invalid parameter' + } + mock_exception = Exception("Invalid parameter") + mock_exception.error = error_response + + self.mock_proxy_instance.getblockhash.side_effect = mock_exception + + # Create a NodeProxy instance + proxy = NodeProxy('testuser', 'testpass') + + # Verify that RPCError is raised with the correct error code + with self.assertRaises(RPCError) as cm: + proxy.get_block_hash(-1) + + self.assertEqual(str(cm.exception), "RPC Error (-8): Invalid parameter") + self.assertEqual(cm.exception.code, -8) + + # Test with an exception without an error code + self.mock_proxy_instance.getblockhash.side_effect = Exception("Network error") + + with self.assertRaises(RPCError) as cm: + proxy.get_block_hash(123456) + + self.assertEqual(str(cm.exception), "Network error") + self.assertIsNone(cm.exception.code) + + def test_compatibility_get_proxy(self): + """Test compatibility method get_proxy.""" + proxy = NodeProxy('testuser', 'testpass') + result = proxy.get_proxy() + + # Verify that the result is the AuthServiceProxy instance + self.assertEqual(result, self.mock_proxy_instance) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file