Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----

Expand Down
114 changes: 114 additions & 0 deletions bitcoinutils/psbt.py
Original file line number Diff line number Diff line change
@@ -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
58 changes: 58 additions & 0 deletions examples/psbt_create_basic.py
Original file line number Diff line number Diff line change
@@ -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()
57 changes: 57 additions & 0 deletions tests/test_psbt.py
Original file line number Diff line number Diff line change
@@ -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()