diff --git a/README.rst b/README.rst index 58a69dda..c22a4732 100644 --- a/README.rst +++ b/README.rst @@ -129,6 +129,15 @@ https://github.com/karask/python-bitcoin-utils/blob/master/examples/send_to_p2tr Spend taproot from script path (has three alternative script path spends - A, B and C) https://github.com/karask/python-bitcoin-utils/blob/master/examples/spend_p2tr_three_scripts_by_script_path.py - single input, single output, spend script path B. +Partially Signed Bitcoin Transactions (PSBT) +-------------------------------------------- + +**Note: PSBT support is currently in early development stages.** + +This library is adding support for BIP-174 Partially Signed Bitcoin Transactions (PSBT). +Current implementation includes basic data structures, with serialization and full functionality +to be added in future releases. + Other ----- diff --git a/bitcoinutils/psbt.py b/bitcoinutils/psbt.py new file mode 100644 index 00000000..eed8b993 --- /dev/null +++ b/bitcoinutils/psbt.py @@ -0,0 +1,114 @@ +""" +Partially Signed Bitcoin Transaction (PSBT) - BIP-174 Implementation + +This module provides classes and methods to create, parse, and manipulate +Partially Signed Bitcoin Transactions as defined in BIP-174. + +References: + - BIP-174: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki +""" + +from enum import Enum +from typing import Dict, List, Optional, Union, Any + +# Magic bytes for PSBT format (hex representation) +PSBT_MAGIC_BYTES = b'\x70\x73\x62\x74' # "psbt" in ASCII +PSBT_SEPARATOR = b'\xff' # Separator between maps +PSBT_DEFAULT_VERSION = 0 # Current PSBT version is 0 + +class PSBTTypeField(Enum): + """PSBT field types as defined in BIP-174.""" + # Global Types + UNSIGNED_TX = 0x00 + XPUB = 0x01 + VERSION = 0xfb + PROPRIETARY = 0xfc + + # Input Types + NON_WITNESS_UTXO = 0x00 + WITNESS_UTXO = 0x01 + PARTIAL_SIG = 0x02 + SIGHASH_TYPE = 0x03 + REDEEM_SCRIPT = 0x04 + WITNESS_SCRIPT = 0x05 + BIP32_DERIVATION = 0x06 + FINAL_SCRIPTSIG = 0x07 + FINAL_SCRIPTWITNESS = 0x08 + POR_COMMITMENT = 0x09 + + # Output Types + REDEEM_SCRIPT_OUTPUT = 0x00 + WITNESS_SCRIPT_OUTPUT = 0x01 + BIP32_DERIVATION_OUTPUT = 0x02 + + +class PSBTInput: + """Class representing a PSBT input with associated data.""" + + def __init__(self, tx_input=None): + """Initialize a PSBT input. + + Args: + tx_input: Related transaction input (optional) + """ + self.utxo = None # Non-witness UTXO (complete transaction) + self.witness_utxo = None # Witness UTXO (just the output) + self.partial_sigs = {} # {pubkey: signature} + self.sighash_type = None # Signature hash type if specified + self.redeem_script = None # Redeem script for P2SH + self.witness_script = None # Witness script for P2WSH + self.bip32_derivations = {} # {pubkey: (fingerprint, path)} + self.final_script_sig = None # Final scriptSig + self.final_script_witness = None # Final scriptWitness + self.proprietary = {} # Proprietary fields + self.unknown = {} # Unknown fields + + # Link to transaction input if provided + self.tx_input = tx_input + + +class PSBTOutput: + """Class representing a PSBT output with associated data.""" + + def __init__(self, tx_output=None): + """Initialize a PSBT output. + + Args: + tx_output: Related transaction output (optional) + """ + self.redeem_script = None # Redeem script + self.witness_script = None # Witness script + self.bip32_derivations = {} # {pubkey: (fingerprint, path)} + self.proprietary = {} # Proprietary fields + self.unknown = {} # Unknown fields + + # Link to transaction output if provided + self.tx_output = tx_output + + +class PSBT: + """Class representing a Partially Signed Bitcoin Transaction.""" + + def __init__(self, tx=None): + """Initialize a new PSBT object. + + Args: + tx: The unsigned transaction this PSBT is based on (optional) + """ + self.version = PSBT_DEFAULT_VERSION + self.tx = tx # Unsigned transaction + self.inputs = [] # List of PSBTInput objects + self.outputs = [] # List of PSBTOutput objects + self.xpubs = {} # Extended public keys: {xpub: (master_fingerprint, derivation_path)} + self.proprietary = {} # Proprietary fields + self.unknown = {} # Unknown fields + + # Initialize inputs and outputs based on transaction if provided + if tx: + for tx_in in tx.inputs: + self.inputs.append(PSBTInput(tx_in)) + + for tx_out in tx.outputs: + self.outputs.append(PSBTOutput(tx_out)) + + # Serialization methods will be implemented in future PRs \ No newline at end of file diff --git a/examples/psbt_create_basic.py b/examples/psbt_create_basic.py new file mode 100644 index 00000000..3ead1e45 --- /dev/null +++ b/examples/psbt_create_basic.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +""" +Example creating a basic PSBT (initial implementation) +""" +# Add parent directory to import path for running directly +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) +from bitcoinutils.setup import setup +from bitcoinutils.transactions import Transaction, TxInput, TxOutput +from bitcoinutils.keys import P2pkhAddress +from bitcoinutils.script import Script +from bitcoinutils.psbt import PSBT + +def main(): + # Setup the network + setup('testnet') + + # Create a transaction input + prev_tx_id = "6ecd66d88b1a976cde70ebbef1909edec5db80cdd7bc3d6b6d451b91715bb919" + prev_output_index = 0 + tx_in = TxInput(prev_tx_id, prev_output_index) + + # Create a transaction output (20000 satoshis to a dummy address) + dummy_addr = P2pkhAddress('n4bkvTyU1dVdzsrhWBqBw8fEMbHjJvtmJR') + tx_out = TxOutput(20000, dummy_addr.to_script_pub_key()) + + # Create an unsigned transaction + tx = Transaction([tx_in], [tx_out]) + + # Create a PSBT from the unsigned transaction + psbt = PSBT(tx) + + # Print information about the PSBT + print("\nPSBT Information:") + print(f"Number of inputs: {len(psbt.inputs)}") + print(f"Number of outputs: {len(psbt.outputs)}") + print(f"PSBT version: {psbt.version}") + + print("\nPSBT Input Information:") + for i, psbt_in in enumerate(psbt.inputs): + print(f"Input #{i}:") + print(f" Transaction ID: {psbt_in.tx_input.txid}") + # Use the stored previous output index from when we created the input + print(f" Output Index: {prev_output_index}") + print(f" Has UTXO data: {psbt_in.utxo is not None}") + print(f" Has partial signatures: {len(psbt_in.partial_sigs) > 0}") + + print("\nPSBT Output Information:") + for i, psbt_out in enumerate(psbt.outputs): + print(f"Output #{i}:") + print(f" Amount: {psbt_out.tx_output.amount}") + print(f" Script type: {psbt_out.tx_output.script_pubkey}") + + print("\nNote: Serialization and additional functionality coming in future updates") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_psbt.py b/tests/test_psbt.py new file mode 100644 index 00000000..bb266e5c --- /dev/null +++ b/tests/test_psbt.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +""" +Simple test for PSBT implementation - can be run directly without unittest +""" + +import sys +import os +# Add the parent directory to the path so we can import our module +sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) + +from bitcoinutils.setup import setup +from bitcoinutils.transactions import Transaction, TxInput, TxOutput +from bitcoinutils.keys import P2pkhAddress +from bitcoinutils.script import Script +from bitcoinutils.psbt import PSBT, PSBTInput, PSBTOutput + +def run_tests(): + """Run simple tests for PSBT implementation.""" + print("Testing PSBT implementation") + + # Setup + setup('testnet') + + # Test 1: Basic PSBT creation + print("\nTest 1: Basic PSBT creation") + tx_in = TxInput("6ecd66d88b1a976cde70ebbef1909edec5db80cdd7bc3d6b6d451b91715bb919", 0) + addr = P2pkhAddress('n4bkvTyU1dVdzsrhWBqBw8fEMbHjJvtmJR') + tx_out = TxOutput(20000, addr.to_script_pub_key()) + tx = Transaction([tx_in], [tx_out]) + + psbt = PSBT(tx) + + assert len(psbt.inputs) == 1, f"Expected 1 input, got {len(psbt.inputs)}" + assert len(psbt.outputs) == 1, f"Expected 1 output, got {len(psbt.outputs)}" + assert psbt.version == 0, f"Expected version 0, got {psbt.version}" + print("✓ Basic PSBT creation test passed") + + # Test 2: PSBTInput properties + print("\nTest 2: PSBTInput properties") + psbt_in = PSBTInput(tx_in) + assert psbt_in.tx_input == tx_in, "PSBTInput should reference the original TxInput" + assert psbt_in.utxo is None, "Initial utxo should be None" + assert psbt_in.partial_sigs == {}, "partial_sigs should be an empty dict" + print("✓ PSBTInput properties test passed") + + # Test 3: PSBTOutput properties + print("\nTest 3: PSBTOutput properties") + psbt_out = PSBTOutput(tx_out) + assert psbt_out.tx_output == tx_out, "PSBTOutput should reference the original TxOutput" + assert psbt_out.redeem_script is None, "Initial redeem_script should be None" + assert psbt_out.bip32_derivations == {}, "bip32_derivations should be an empty dict" + print("✓ PSBTOutput properties test passed") + + print("\nAll tests passed!") + +if __name__ == "__main__": + run_tests() \ No newline at end of file