From 56341ab5a2f5a61fc6223195d1148cd6d2cbc2f4 Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Tue, 1 Jul 2025 21:17:13 +0530 Subject: [PATCH 01/12] Complete PSBT Workflow Implementation Including Multisig Support and Script Enhancements --- bitcoinutils/psbt.py | 1181 ++++++++++++++++++++++++++++++ examples/combine_psbt.py | 0 examples/create_psbt_multisig.py | 0 examples/finalize_psbt.py | 0 examples/sign_psbt.py | 0 5 files changed, 1181 insertions(+) create mode 100644 bitcoinutils/psbt.py create mode 100644 examples/combine_psbt.py create mode 100644 examples/create_psbt_multisig.py create mode 100644 examples/finalize_psbt.py create mode 100644 examples/sign_psbt.py diff --git a/bitcoinutils/psbt.py b/bitcoinutils/psbt.py new file mode 100644 index 00000000..b430aec0 --- /dev/null +++ b/bitcoinutils/psbt.py @@ -0,0 +1,1181 @@ +""" +Partially Signed Bitcoin Transaction (PSBT) implementation following +BIP-174. + +This module provides the PSBT class which represents a partially signed +bitcoin transaction that can be shared between multiple parties for signing +before broadcasting to the network. + +A PSBT contains: +- The unsigned transaction +- Input metadata needed for signing (UTXOs, scripts, keys, etc.) +- Output metadata for validation +- Partial signatures from different signers + +The PSBT workflow typically involves: +1. Creator: Creates the PSBT with the unsigned transaction +2. Updater: Adds input/output metadata needed for signing +3. Signer: Signs inputs they can sign (sign_input() handles all script types automatically) +4. Combiner: Combines multiple PSBTs with different signatures +5. Finalizer: Finalizes the PSBT by adding final scriptSig/witness +6. Extractor: Extracts the final signed transaction +""" + +import struct +from io import BytesIO +from typing import Dict, List, Optional, Tuple, Union +from bitcoinutils.transactions import Transaction, TxInput, TxOutput +from bitcoinutils.script import Script +from bitcoinutils.keys import PrivateKey, PublicKey +from bitcoinutils.utils import to_satoshis + +class PSBTInput: + """ + Represents a single input in a PSBT with all associated metadata. + + Contains information needed to sign this input including: + - Non-witness UTXO (for legacy inputs) + - Witness UTXO (for segwit inputs) + - Partial signatures + - Sighash type + - Redeem script (for P2SH) + - Witness script (for P2WSH) + - BIP32 derivation paths + - Final scriptSig and witness + """ + + def __init__(self): + # BIP-174 defined fields + self.non_witness_utxo: Optional[Transaction] = None + self.witness_utxo: Optional[TxOutput] = None + self.partial_sigs: Dict[bytes, bytes] = {} # pubkey -> signature + self.sighash_type: Optional[int] = None + self.redeem_script: Optional[Script] = None + self.witness_script: Optional[Script] = None + self.bip32_derivs: Dict[bytes, Tuple[bytes, List[int]]] = {} # pubkey -> (fingerprint, path) + self.final_scriptsig: Optional[Script] = None + self.final_scriptwitness: List[bytes] = [] + + # Additional fields for validation + self.ripemd160_preimages: Dict[bytes, bytes] = {} + self.sha256_preimages: Dict[bytes, bytes] = {} + self.hash160_preimages: Dict[bytes, bytes] = {} + self.hash256_preimages: Dict[bytes, bytes] = {} + + # Proprietary fields + self.proprietary: Dict[bytes, bytes] = {} + self.unknown: Dict[bytes, bytes] = {} + +class PSBTOutput: + """ + Represents a single output in a PSBT with associated metadata. + + Contains information about the output including: + - Redeem script (for P2SH outputs) + - Witness script (for P2WSH outputs) + - BIP32 derivation paths + """ + + def __init__(self): + # BIP-174 defined fields + self.redeem_script: Optional[Script] = None + self.witness_script: Optional[Script] = None + self.bip32_derivs: Dict[bytes, Tuple[bytes, List[int]]] = {} # pubkey -> (fingerprint, path) + + # Proprietary fields + self.proprietary: Dict[bytes, bytes] = {} + self.unknown: Dict[bytes, bytes] = {} + +class PSBT: + """ + Partially Signed Bitcoin Transaction implementation following BIP-174. + + A PSBT is a data format that allows multiple parties to collaboratively + sign a bitcoin transaction. The PSBT contains the unsigned transaction + along with metadata needed for signing. + + Example usage: + # Create PSBT from unsigned transaction + psbt = PSBT(unsigned_tx) + + # Add input metadata + psbt.inputs[0].witness_utxo = prev_output + + # Sign with private key (automatically detects script type) + psbt.sign_input(0, private_key) + + # Finalize and extract signed transaction + final_tx = psbt.finalize() + """ + + # PSBT magic bytes and version + MAGIC = b'psbt' + VERSION = 0 + + # Key types as defined in BIP-174 + class GlobalTypes: + UNSIGNED_TX = 0x00 + XPUB = 0x01 + VERSION = 0xFB + PROPRIETARY = 0xFC + + class InputTypes: + 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 + RIPEMD160 = 0x0A + SHA256 = 0x0B + HASH160 = 0x0C + HASH256 = 0x0D + PROPRIETARY = 0xFC + + class OutputTypes: + REDEEM_SCRIPT = 0x00 + WITNESS_SCRIPT = 0x01 + BIP32_DERIVATION = 0x02 + PROPRIETARY = 0xFC + + def __init__(self, unsigned_tx: Optional[Transaction] = None): + """ + Initialize a new PSBT. + + Args: + unsigned_tx: The unsigned transaction. If None, an empty transaction is created. + """ + if unsigned_tx is None: + # Create empty transaction + self.tx = Transaction([], []) + else: + # Ensure transaction has no scripts/witnesses (must be unsigned) + inputs = [] + for tx_input in unsigned_tx.inputs: + # Create input without scriptSig + clean_input = TxInput(tx_input.txid, tx_input.txout_index) + inputs.append(clean_input) + + self.tx = Transaction(inputs, unsigned_tx.outputs[:], unsigned_tx.locktime, unsigned_tx.version) + + # Initialize PSBT-specific data + self.inputs: List[PSBTInput] = [PSBTInput() for _ in self.tx.inputs] + self.outputs: List[PSBTOutput] = [PSBTOutput() for _ in self.tx.outputs] + + # Global fields + self.version = self.VERSION + self.xpubs: Dict[bytes, Tuple[bytes, List[int]]] = {} # xpub -> (fingerprint, path) + self.proprietary: Dict[bytes, bytes] = {} + self.unknown: Dict[bytes, bytes] = {} + + @classmethod + def from_base64(cls, psbt_str: str) -> 'PSBT': + """ + Create PSBT from base64 encoded string. + + Args: + psbt_str: Base64 encoded PSBT + + Returns: + PSBT object + """ + import base64 + psbt_bytes = base64.b64decode(psbt_str) + return cls.from_bytes(psbt_bytes) + + @classmethod + def from_bytes(cls, psbt_bytes: bytes) -> 'PSBT': + """ + Deserialize PSBT from bytes. + + Args: + psbt_bytes: Serialized PSBT bytes + + Returns: + PSBT object + """ + stream = BytesIO(psbt_bytes) + + # Read and verify magic + magic = stream.read(4) + if magic != cls.MAGIC: + raise ValueError(f"Invalid PSBT magic: {magic.hex()}") + + # Read separator + separator = stream.read(1) + if separator != b'\xff': + raise ValueError("Invalid PSBT separator") + + # Parse global section + psbt = cls() + psbt._parse_global_section(stream) + + # Parse input sections + for i in range(len(psbt.tx.inputs)): + psbt._parse_input_section(stream, i) + + # Parse output sections + for i in range(len(psbt.tx.outputs)): + psbt._parse_output_section(stream, i) + + return psbt + + def to_base64(self) -> str: + """ + Serialize PSBT to base64 string. + + Returns: + Base64 encoded PSBT + """ + import base64 + return base64.b64encode(self.to_bytes()).decode('ascii') + + def to_bytes(self) -> bytes: + """ + Serialize PSBT to bytes. + + Returns: + Serialized PSBT bytes + """ + result = BytesIO() + + # Write magic and separator + result.write(self.MAGIC) + result.write(b'\xff') + + # Write global section + self._serialize_global_section(result) + + # Write input sections + for i, psbt_input in enumerate(self.inputs): + self._serialize_input_section(result, i) + + # Write output sections + for i, psbt_output in enumerate(self.outputs): + self._serialize_output_section(result, i) + + return result.getvalue() + + def add_input(self, tx_input: TxInput, psbt_input: Optional[PSBTInput] = None) -> None: + """ + Add input to the PSBT transaction. + + Args: + tx_input: Transaction input to add + psbt_input: PSBT input metadata. If None, empty metadata is created. + """ + # Create clean input without scriptSig + clean_input = TxInput(tx_input.txid, tx_input.txout_index) + self.tx.inputs.append(clean_input) + + if psbt_input is None: + psbt_input = PSBTInput() + self.inputs.append(psbt_input) + + def add_output(self, tx_output: TxOutput, psbt_output: Optional[PSBTOutput] = None) -> None: + """ + Add output to the PSBT transaction. + + Args: + tx_output: Transaction output to add + psbt_output: PSBT output metadata. If None, empty metadata is created. + """ + self.tx.outputs.append(tx_output) + + if psbt_output is None: + psbt_output = PSBTOutput() + self.outputs.append(psbt_output) + + def sign(self, private_key: PrivateKey, input_index: int, sighash_type: int = 1) -> bool: + """ + Legacy method for backward compatibility. Use sign_input() instead. + + Args: + private_key: Private key to sign with + input_index: Index of input to sign + sighash_type: Signature hash type (default: SIGHASH_ALL) + + Returns: + True if signature was added, False if input couldn't be signed + """ + return self.sign_input(input_index, private_key, sighash_type) + + def sign_input(self, input_index: int, private_key: PrivateKey, sighash_type: int = 1) -> bool: + """ + Sign a specific input with the given private key. + + Automatically detects the script type and uses appropriate signing method: + - P2PKH: Legacy pay-to-public-key-hash + - P2SH: Pay-to-script-hash (including nested SegWit) + - P2WPKH: Native SegWit pay-to-witness-public-key-hash + - P2WSH: Native SegWit pay-to-witness-script-hash + - P2TR: Taproot pay-to-taproot + + Args: + input_index: Index of input to sign + private_key: Private key to sign with + sighash_type: Signature hash type (default: SIGHASH_ALL) + + Returns: + True if signature was added, False if input couldn't be signed + """ + try: + # Validate input index + if input_index >= len(self.inputs): + return False + + input_data = self.inputs[input_index] + tx_input = self.tx.inputs[input_index] + + # Get the appropriate signature for this input + signature = self._get_signature_for_input(input_index, private_key, sighash_type) + + if signature: + # Add the signature to the PSBT + public_key_bytes = private_key.get_public_key().to_bytes() + input_data.partial_sigs[public_key_bytes] = signature + + # Set sighash type if not already set + if input_data.sighash_type is None: + input_data.sighash_type = sighash_type + + return True + else: + return False + + except Exception: + return False + + def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, sighash_type: int) -> bytes: + """ + Get the appropriate signature for an input based on its script type. + + Args: + input_index: Input index + private_key: Private key to sign with + sighash_type: Signature hash type + + Returns: + bytes: Signature if successful, None otherwise + """ + input_data = self.inputs[input_index] + tx_input = self.tx.inputs[input_index] + + try: + if input_data.redeem_script: + # P2SH-P2WSH or P2SH-P2WPKH or regular P2SH + redeem_script = input_data.redeem_script + + # Check if it's a P2SH-wrapped SegWit + if input_data.witness_script: + # P2SH-P2WSH + witness_script = input_data.witness_script + if input_data.witness_utxo: + amount = to_satoshis(input_data.witness_utxo.amount) + return private_key.sign_segwit_input(self.tx, input_index, witness_script, amount, sighash_type) + + elif self._is_p2wpkh_script(redeem_script): + # P2SH-P2WPKH + if input_data.witness_utxo: + amount = to_satoshis(input_data.witness_utxo.amount) + p2pkh_script = private_key.get_public_key().get_address().to_script_pub_key() + return private_key.sign_segwit_input(self.tx, input_index, p2pkh_script, amount, sighash_type) + + else: + # Regular P2SH + return private_key.sign_input(self.tx, input_index, redeem_script, sighash_type) + + elif input_data.witness_script: + # P2WSH input + witness_script = input_data.witness_script + if input_data.witness_utxo: + amount = to_satoshis(input_data.witness_utxo.amount) + return private_key.sign_segwit_input(self.tx, input_index, witness_script, amount, sighash_type) + + elif input_data.witness_utxo: + # Check if it's P2WPKH or P2TR + script_pubkey = input_data.witness_utxo.script_pubkey + amount = to_satoshis(input_data.witness_utxo.amount) + + if self._is_p2wpkh_script(script_pubkey): + # P2WPKH input + p2pkh_script = private_key.get_public_key().get_address().to_script_pub_key() + return private_key.sign_segwit_input(self.tx, input_index, p2pkh_script, amount, sighash_type) + + elif self._is_p2tr_script(script_pubkey): + # P2TR input + return private_key.sign_taproot_input(self.tx, input_index, amount, sighash_type) + + elif input_data.non_witness_utxo: + # Legacy P2PKH or P2SH + prev_tx_out = input_data.non_witness_utxo.outputs[tx_input.txout_index] + script_pubkey = prev_tx_out.script_pubkey + + if self._is_p2pkh_script(script_pubkey): + # P2PKH input + return private_key.sign_input(self.tx, input_index, script_pubkey, sighash_type) + + return None + + except Exception: + return None + + def _is_p2pkh_script(self, script) -> bool: + """Check if script is P2PKH (OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG).""" + try: + return script.is_p2pkh() if hasattr(script, 'is_p2pkh') else False + except: + return False + + def _is_p2wpkh_script(self, script) -> bool: + """Check if script is P2WPKH (OP_0 <20-byte-pubkeyhash>).""" + try: + return script.is_p2wpkh() if hasattr(script, 'is_p2wpkh') else False + except: + return False + + def _is_p2tr_script(self, script) -> bool: + """Check if script is P2TR (OP_1 <32-byte-taproot-output>).""" + try: + return script.is_p2tr() if hasattr(script, 'is_p2tr') else False + except: + return False + + def _is_input_finalized(self, input_data: PSBTInput) -> bool: + """ + Check if an input is already finalized. + + Args: + input_data: PSBT input to check + + Returns: + True if input is finalized + """ + return bool(input_data.final_scriptsig or input_data.final_scriptwitness) + + def _apply_final_fields(self, tx_input: TxInput, input_data: PSBTInput) -> None: + """ + Apply final scriptSig and witness to a transaction input. + + Args: + tx_input: Transaction input to modify + input_data: PSBT input with final fields + """ + if input_data.final_scriptsig: + tx_input.script_sig = input_data.final_scriptsig + else: + tx_input.script_sig = Script([]) + + def _validate_final_tx(self, tx: Transaction) -> Dict[str, any]: + """ + Validate a finalized transaction. + + Args: + tx: Transaction to validate + + Returns: + Dictionary with validation results + """ + validation_info = { + 'is_valid': True, + 'errors': [], + 'warnings': [] + } + + # Basic validation + if not tx.inputs: + validation_info['is_valid'] = False + validation_info['errors'].append("Transaction has no inputs") + + if not tx.outputs: + validation_info['is_valid'] = False + validation_info['errors'].append("Transaction has no outputs") + + # Check for empty scripts where they shouldn't be + for i, (tx_input, psbt_input) in enumerate(zip(tx.inputs, self.inputs)): + if not tx_input.script_sig and not psbt_input.final_scriptwitness: + validation_info['warnings'].append(f"Input {i} has empty scriptSig and witness") + + return validation_info + + def combine(self, other: 'PSBT') -> 'PSBT': + """ + Combine this PSBT with another PSBT (combiner role). + + Args: + other: Another PSBT to combine with + + Returns: + New combined PSBT + """ + # Ensure both PSBTs have the same unsigned transaction + if self.tx.serialize() != other.tx.serialize(): + raise ValueError("Cannot combine PSBTs with different transactions") + + # Create new PSBT with combined data + combined = PSBT(self.tx) + + # Combine global data + combined.xpubs.update(self.xpubs) + combined.xpubs.update(other.xpubs) + combined.proprietary.update(self.proprietary) + combined.proprietary.update(other.proprietary) + combined.unknown.update(self.unknown) + combined.unknown.update(other.unknown) + + # Combine input data + for i, (input1, input2) in enumerate(zip(self.inputs, other.inputs)): + combined_input = combined.inputs[i] + + # Combine UTXOs (prefer witness_utxo) + if input1.witness_utxo: + combined_input.witness_utxo = input1.witness_utxo + elif input2.witness_utxo: + combined_input.witness_utxo = input2.witness_utxo + elif input1.non_witness_utxo: + combined_input.non_witness_utxo = input1.non_witness_utxo + elif input2.non_witness_utxo: + combined_input.non_witness_utxo = input2.non_witness_utxo + + # Combine partial signatures + combined_input.partial_sigs.update(input1.partial_sigs) + combined_input.partial_sigs.update(input2.partial_sigs) + + # Combine other fields + combined_input.sighash_type = input1.sighash_type or input2.sighash_type + combined_input.redeem_script = input1.redeem_script or input2.redeem_script + combined_input.witness_script = input1.witness_script or input2.witness_script + combined_input.bip32_derivs.update(input1.bip32_derivs) + combined_input.bip32_derivs.update(input2.bip32_derivs) + + # Final scripts (should be same or one empty) + combined_input.final_scriptsig = input1.final_scriptsig or input2.final_scriptsig + if input1.final_scriptwitness: + combined_input.final_scriptwitness = input1.final_scriptwitness + elif input2.final_scriptwitness: + combined_input.final_scriptwitness = input2.final_scriptwitness + + # Combine preimages and proprietary data + combined_input.ripemd160_preimages.update(input1.ripemd160_preimages) + combined_input.ripemd160_preimages.update(input2.ripemd160_preimages) + combined_input.sha256_preimages.update(input1.sha256_preimages) + combined_input.sha256_preimages.update(input2.sha256_preimages) + combined_input.hash160_preimages.update(input1.hash160_preimages) + combined_input.hash160_preimages.update(input2.hash160_preimages) + combined_input.hash256_preimages.update(input1.hash256_preimages) + combined_input.hash256_preimages.update(input2.hash256_preimages) + combined_input.proprietary.update(input1.proprietary) + combined_input.proprietary.update(input2.proprietary) + combined_input.unknown.update(input1.unknown) + combined_input.unknown.update(input2.unknown) + + # Combine output data + for i, (output1, output2) in enumerate(zip(self.outputs, other.outputs)): + combined_output = combined.outputs[i] + combined_output.redeem_script = output1.redeem_script or output2.redeem_script + combined_output.witness_script = output1.witness_script or output2.witness_script + combined_output.bip32_derivs.update(output1.bip32_derivs) + combined_output.bip32_derivs.update(output2.bip32_derivs) + combined_output.proprietary.update(output1.proprietary) + combined_output.proprietary.update(output2.proprietary) + combined_output.unknown.update(output1.unknown) + combined_output.unknown.update(output2.unknown) + + return combined + + def combine_psbts(self, other_psbts: List['PSBT']) -> 'PSBT': + """ + Combine this PSBT with multiple other PSBTs. + + Wraps the pairwise `combine()` method in a loop for batch combining. + + Args: + other_psbts (List[PSBT]): A list of PSBTs to combine + + Returns: + PSBT: The final combined PSBT + """ + combined = self + for other in other_psbts: + combined = combined.combine(other) + return combined + + def finalize(self, validate: bool = False) -> Union[Transaction, Tuple[Transaction, Dict], bool]: + """ + Finalize all inputs and create the final broadcastable transaction or check if all inputs are finalized. + + If called with validate=False and no additional arguments, returns a boolean indicating if all inputs were finalized successfully. + If called with validate=True or no arguments, builds a complete Transaction object with all final scriptSigs and witnesses. + + Args: + validate: If True, validate the final transaction and return validation info + + Returns: + If validate=False: Transaction object ready for broadcast or boolean if simple finalize + If validate=True: Tuple of (Transaction, validation_info dict) + + Raises: + ValueError: If not all inputs can be finalized + """ + # Simple finalize returning boolean + if not validate: + all_finalized = True + for i in range(len(self.inputs)): + if not self._finalize_input(i): + all_finalized = False + return all_finalized + + # Existing finalize logic from Untitled document-10.docx + finalized_count = 0 + for i in range(len(self.inputs)): + if self._finalize_input(i): + finalized_count += 1 + + if finalized_count != len(self.inputs): + raise ValueError(f"Could not finalize all inputs. Finalized: {finalized_count}/{len(self.inputs)}") + + final_inputs = [] + for i, (tx_input, psbt_input) in enumerate(zip(self.tx.inputs, self.inputs)): + final_input = TxInput( + tx_input.txid, + tx_input.txout_index, + psbt_input.final_scriptsig or Script([]), + tx_input.sequence + ) + final_inputs.append(final_input) + + final_tx = Transaction( + final_inputs, + self.tx.outputs[:], + self.tx.locktime, + self.tx.version + ) + + final_tx.witnesses = [] + for psbt_input in self.inputs: + if psbt_input.final_scriptwitness: + final_tx.witnesses.append(psbt_input.final_scriptwitness) + else: + final_tx.witnesses.append([]) + + if validate: + validation_info = self._validate_final_tx(final_tx) + return final_tx, validation_info + else: + return final_tx + + def finalize_input(self, input_index: int) -> bool: + """ + Finalize a specific input by constructing final scriptSig and witness. + + Args: + input_index: Index of input to finalize + + Returns: + True if input was finalized successfully + """ + if input_index >= len(self.inputs): + raise ValueError(f"Input index {input_index} out of range") + + return self._finalize_input(input_index) + + def _finalize_input(self, input_index: int) -> bool: + """ + Enhanced input finalization with better script type detection. + + Args: + input_index: Index of input to finalize + + Returns: + True if input was finalized successfully + """ + psbt_input = self.inputs[input_index] + + # Skip if already finalized + if self._is_input_finalized(psbt_input): + return True + + # Need partial signatures to finalize + if not psbt_input.partial_sigs: + return False + + # Get UTXO info + if psbt_input.witness_utxo: + prev_output = psbt_input.witness_utxo + script_pubkey = prev_output.script_pubkey + elif psbt_input.non_witness_utxo: + prev_vout = self.tx.inputs[input_index].txout_index + prev_output = psbt_input.non_witness_utxo.outputs[prev_vout] + script_pubkey = prev_output.script_pubkey + else: + return False + + # Handle different script types with improved detection + try: + if script_pubkey.is_p2pkh(): + return self._finalize_p2pkh(psbt_input) + elif script_pubkey.is_p2wpkh(): + return self._finalize_p2wpkh(psbt_input) + elif script_pubkey.is_p2sh(): + return self._finalize_p2sh(psbt_input) + elif script_pubkey.is_p2wsh(): + return self._finalize_p2wsh(psbt_input) + elif self._is_p2tr_script(script_pubkey): + return self._finalize_p2tr(psbt_input) + except Exception: + pass + + return False + + def _finalize_p2pkh(self, psbt_input: PSBTInput) -> bool: + """Finalize P2PKH input.""" + if len(psbt_input.partial_sigs) != 1: + return False + + pubkey, signature = next(iter(psbt_input.partial_sigs.items())) + psbt_input.final_scriptsig = Script([signature, pubkey]) + return True + + def _finalize_p2wpkh(self, psbt_input: PSBTInput) -> bool: + """Finalize P2WPKH input.""" + if len(psbt_input.partial_sigs) != 1: + return False + + pubkey, signature = next(iter(psbt_input.partial_sigs.items())) + psbt_input.final_scriptsig = Script([]) + psbt_input.final_scriptwitness = [signature, pubkey] + return True + + def _finalize_p2sh(self, psbt_input: PSBTInput) -> bool: + """Finalize P2SH input with support for nested SegWit.""" + if not psbt_input.redeem_script: + return False + + redeem_script = psbt_input.redeem_script + + # Handle P2SH-wrapped SegWit + if redeem_script.is_p2wpkh(): + return self._finalize_p2sh_p2wpkh(psbt_input) + elif redeem_script.is_p2wsh(): + return self._finalize_p2sh_p2wsh(psbt_input) + else: + # Regular P2SH + success = self._finalize_script(psbt_input, redeem_script, is_witness=False) + if success: + # Add redeem script to the end of scriptSig + current_elements = psbt_input.final_scriptsig.script if psbt_input.final_scriptsig else [] + psbt_input.final_scriptsig = Script(current_elements + [redeem_script.to_bytes()]) + return success + + def _finalize_p2sh_p2wpkh(self, psbt_input: PSBTInput) -> bool: + """Finalize P2SH-wrapped P2WPKH input.""" + if len(psbt_input.partial_sigs) != 1: + return False + + pubkey, signature = next(iter(psbt_input.partial_sigs.items())) + + # scriptSig contains just the redeem script + psbt_input.final_scriptsig = Script([psbt_input.redeem_script.to_bytes()]) + # Witness contains signature and pubkey + psbt_input.final_scriptwitness = [signature, pubkey] + return True + + def _finalize_p2sh_p2wsh(self, psbt_input: PSBTInput) -> bool: + """Finalize P2SH-wrapped P2WSH input.""" + if not psbt_input.witness_script: + return False + + # Finalize the witness script part + success = self._finalize_script(psbt_input, psbt_input.witness_script, is_witness=True) + if success: + # Add the redeem script to scriptSig + psbt_input.final_scriptsig = Script([psbt_input.redeem_script.to_bytes()]) + # Add witness script to the end of witness + psbt_input.final_scriptwitness.append(psbt_input.witness_script.to_bytes()) + return success + + def _finalize_p2wsh(self, psbt_input: PSBTInput) -> bool: + """Finalize P2WSH input.""" + if not psbt_input.witness_script: + return False + + return self._finalize_script(psbt_input, psbt_input.witness_script, is_witness=True) + + def _finalize_p2tr(self, psbt_input: PSBTInput) -> bool: + """Finalize P2TR (Taproot) input.""" + if len(psbt_input.partial_sigs) != 1: + return False + + # For key-path spending, we expect a single signature + signature = next(iter(psbt_input.partial_sigs.values())) + psbt_input.final_scriptsig = Script([]) + psbt_input.final_scriptwitness = [signature] + return True + + def _finalize_script(self, psbt_input: PSBTInput, script: Script, is_witness: bool) -> bool: + """ + Enhanced script finalization with better multisig support. + + Args: + psbt_input: PSBT input to finalize + script: Script to finalize against + is_witness: Whether this is a witness script + + Returns: + True if finalized successfully + """ + script_ops = script.script + + # Enhanced multisig detection and handling + if (len(script_ops) >= 4 and + isinstance(script_ops[0], int) and 1 <= script_ops[0] <= 16 and + isinstance(script_ops[-2], int) and 1 <= script_ops[-2] <= 16 and + script_ops[-1] == 174): # OP_CHECKMULTISIG + + m = script_ops[0] # Required signatures + n = script_ops[-2] # Total pubkeys + + # Extract public keys from script + pubkeys = [] + for i in range(1, n + 1): + if i < len(script_ops) and isinstance(script_ops[i], bytes): + pubkeys.append(script_ops[i]) + + # Collect signatures in the correct order + signatures = [] + valid_sig_count = 0 + + for pubkey in pubkeys: + if pubkey in psbt_input.partial_sigs: + signatures.append(psbt_input.partial_sigs[pubkey]) + valid_sig_count += 1 + else: + signatures.append(b'') # Placeholder for missing signature + + if valid_sig_count >= m: + break + + # Check if we have enough signatures + if valid_sig_count < m: + return False + + # Trim signatures to required amount, keeping only valid ones + final_sigs = [] + sig_count = 0 + for sig in signatures: + if sig and sig_count < m: + final_sigs.append(sig) + sig_count += 1 + + # Multisig requires OP_0 prefix due to Bitcoin's off-by-one bug + final_script_elements = [b''] + final_sigs + + if is_witness: + final_script_elements.append(script.to_bytes()) + psbt_input.final_scriptsig = Script([]) + psbt_input.final_scriptwitness = final_script_elements + else: + final_script_elements.append(script.to_bytes()) + psbt_input.final_scriptsig = Script(final_script_elements) + + return True + + # Handle single-sig scripts (P2PK, custom scripts, etc.) + elif len(psbt_input.partial_sigs) == 1: + pubkey, signature = next(iter(psbt_input.partial_sigs.items())) + + if is_witness: + psbt_input.final_scriptsig = Script([]) + psbt_input.final_scriptwitness = [signature, pubkey, script.to_bytes()] + else: + psbt_input.final_scriptsig = Script([signature, pubkey, script.to_bytes()]) + + return True + + # Handle other script types (can be extended) + return False + + def _parse_global_section(self, stream: BytesIO) -> None: + """Parse the global section of a PSBT.""" + while True: + # Read key-value pair + key_data = self._read_key_value_pair(stream) + if key_data is None: + break + + key_type, key_data, value_data = key_data + + if key_type == self.GlobalTypes.UNSIGNED_TX: + self.tx = Transaction.from_bytes(value_data) + # Initialize input/output arrays + self.inputs = [PSBTInput() for _ in self.tx.inputs] + self.outputs = [PSBTOutput() for _ in self.tx.outputs] + elif key_type == self.GlobalTypes.XPUB: + fingerprint_path = struct.unpack(' None: + """Parse an input section of a PSBT.""" + psbt_input = self.inputs[input_index] + + while True: + key_data = self._read_key_value_pair(stream) + if key_data is None: + break + + key_type, key_data, value_data = key_data + + if key_type == self.InputTypes.NON_WITNESS_UTXO: + psbt_input.non_witness_utxo = Transaction.from_bytes(value_data) + elif key_type == self.InputTypes.WITNESS_UTXO: + psbt_input.witness_utxo = TxOutput.from_bytes(value_data) + elif key_type == self.InputTypes.PARTIAL_SIG: + psbt_input.partial_sigs[key_data] = value_data + elif key_type == self.InputTypes.SIGHASH_TYPE: + psbt_input.sighash_type = struct.unpack(' None: + """Parse an output section of a PSBT.""" + psbt_output = self.outputs[output_index] + + while True: + key_data = self._read_key_value_pair(stream) + if key_data is None: + break + + key_type, key_data, value_data = key_data + + if key_type == self.OutputTypes.REDEEM_SCRIPT: + psbt_output.redeem_script = Script.from_bytes(value_data) + elif key_type == self.OutputTypes.WITNESS_SCRIPT: + psbt_output.witness_script = Script.from_bytes(value_data) + elif key_type == self.OutputTypes.BIP32_DERIVATION: + fingerprint = struct.unpack(' Optional[Tuple[int, bytes, bytes]]: + """ + Read a key-value pair from the stream. + + Returns: + Tuple of (key_type, key_data, value_data) or None if separator found + """ + # Read key length + key_len_bytes = stream.read(1) + if not key_len_bytes or key_len_bytes == b'\x00': + return None + + key_len = key_len_bytes[0] + if key_len == 0: + return None + + # Read key + key = stream.read(key_len) + if len(key) != key_len: + raise ValueError("Unexpected end of stream reading key") + + # Read value length + value_len_bytes = stream.read(1) + if not value_len_bytes: + raise ValueError("Unexpected end of stream reading value length") + + value_len = value_len_bytes[0] + + # Read value + value = stream.read(value_len) + if len(value) != value_len: + raise ValueError("Unexpected end of stream reading value") + + # Parse key + key_type = key[0] + key_data = key[1:] if len(key) > 1 else b'' + + return key_type, key_data, value + + def _serialize_global_section(self, result: BytesIO) -> None: + """Serialize the global section.""" + # Unsigned transaction + self._write_key_value_pair(result, self.GlobalTypes.UNSIGNED_TX, b'', self.tx.serialize()) + + # XPubs + for xpub, (fingerprint, path) in self.xpubs.items(): + key_data = struct.pack(' None: + """Serialize an input section.""" + psbt_input = self.inputs[input_index] + + # Non-witness UTXO + if psbt_input.non_witness_utxo: + self._write_key_value_pair(result, self.InputTypes.NON_WITNESS_UTXO, b'', + psbt_input.non_witness_utxo.serialize()) + + # Witness UTXO + if psbt_input.witness_utxo: + self._write_key_value_pair(result, self.InputTypes.WITNESS_UTXO, b'', + psbt_input.witness_utxo.serialize()) + + # Partial signatures + for pubkey, signature in psbt_input.partial_sigs.items(): + self._write_key_value_pair(result, self.InputTypes.PARTIAL_SIG, pubkey, signature) + + # Sighash type + if psbt_input.sighash_type is not None: + self._write_key_value_pair(result, self.InputTypes.SIGHASH_TYPE, b'', + struct.pack(' None: + """Serialize an output section.""" + psbt_output = self.outputs[output_index] + + # Redeem script + if psbt_output.redeem_script: + self._write_key_value_pair(result, self.OutputTypes.REDEEM_SCRIPT, b'', + psbt_output.redeem_script.to_bytes()) + + # Witness script + if psbt_output.witness_script: + self._write_key_value_pair(result, self.OutputTypes.WITNESS_SCRIPT, b'', + psbt_output.witness_script.to_bytes()) + + # BIP32 derivations + for pubkey, (fingerprint, path) in psbt_output.bip32_derivs.items(): + value_data = struct.pack(' None: + """Write a key-value pair to the stream.""" + key = bytes([key_type]) + key_data + result.write(bytes([len(key)])) # Key length + result.write(key) # Key + result.write(bytes([len(value_data)])) # Value length + result.write(value_data) # Value \ No newline at end of file diff --git a/examples/combine_psbt.py b/examples/combine_psbt.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/create_psbt_multisig.py b/examples/create_psbt_multisig.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/finalize_psbt.py b/examples/finalize_psbt.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/sign_psbt.py b/examples/sign_psbt.py new file mode 100644 index 00000000..e69de29b From 5d8eb3523cc4fa09b4acd2f824e5a6e9f772fd76 Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Tue, 1 Jul 2025 21:22:52 +0530 Subject: [PATCH 02/12] create_psbt_multisig,py --- examples/create_psbt_multisig.py | 356 +++++++++++++++++++++++++++++++ 1 file changed, 356 insertions(+) diff --git a/examples/create_psbt_multisig.py b/examples/create_psbt_multisig.py index e69de29b..4d0c72d5 100644 --- a/examples/create_psbt_multisig.py +++ b/examples/create_psbt_multisig.py @@ -0,0 +1,356 @@ +# Copyright (C) 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. + +""" +Example of creating a 2-of-3 multisig PSBT using REAL TESTNET4 UTXOs. + +This example demonstrates: +1. Creating a 2-of-3 multisig P2WSH address (Segwit multisig) +2. Creating a PSBT for spending from that address using real Testnet4 UTXOs +3. Setting up the PSBT with proper input information for signing + +IMPORTANT: This uses REAL TESTNET4 transactions that can be verified on: +https://blockstream.info/testnet/ + +Note: This script uses bitcoinutils with setup('testnet'), which defaults to Testnet3. + For Testnet4 compatibility, ensure your UTXOs are from Testnet4 and verify + using a Testnet4-compatible explorer or wallet. + +Before running this example: +1. Get Testnet4 coins from a faucet (e.g., https://faucet.testnet4.dev/) +2. Create the multisig address shown below +3. Send Testnet4 coins to that address +4. Update the UTXO details below with your real transaction +""" + +from bitcoinutils.setup import setup +from bitcoinutils.transactions import Transaction, TxInput, TxOutput, Locktime +from bitcoinutils.keys import PrivateKey, PublicKey +from bitcoinutils.script import Script +from bitcoinutils.utils import to_satoshis +from bitcoinutils.psbt import PSBT +from bitcoinutils.constants import TYPE_RELATIVE_TIMELOCK + + +def get_real_testnet_utxo(): + """ + STEP-BY-STEP GUIDE TO GET REAL TESTNET4 UTXO: + + 1. Visit a Testnet4 block explorer (e.g., https://blockstream.info/testnet/) + 2. Find any recent transaction with outputs + 3. Click on a transaction, copy its TXID + 4. Replace the values below with real Testnet4 data + 5. Verify the TXID works on a Testnet4-compatible explorer + + EXAMPLE OF HOW TO FIND REAL DATA: + - Go to a Testnet4-compatible explorer + - Click "Recent Transactions" + - Pick any transaction (e.g., click on a TXID) + - Copy the TXID from the URL + - Check the outputs for amount and vout index + """ + + # METHOD 1: Use a funding transaction you create yourself + # (Recommended - you control the UTXO) + create_own_funding = True + + if create_own_funding: + # TODO: After running this script once: + # 1. Note the multisig address printed below + # 2. Get Testnet4 coins from faucet + # 3. Send coins to the multisig address + # 4. Update these values with YOUR funding transaction + utxo_details = { + 'txid': 'YOUR_FUNDING_TXID_HERE', # ← Replace with your funding TXID + 'vout': 0, # ← Usually 0, but check the transaction + 'amount': to_satoshis(0.001), # ← Replace with actual amount sent + 'address': None, # Will be set to multisig address + 'is_placeholder': True # Set to False when using real data + } + else: + # METHOD 2: Use any existing Testnet4 UTXO (not recommended for production) + # This is just for demonstration - don't spend other people's UTXOs! + utxo_details = { + 'txid': 'SOME_EXISTING_TESTNET4_TXID', + 'vout': 0, + 'amount': to_satoshis(0.001), + 'address': None, + 'is_placeholder': True + } + + # Validation + if utxo_details['is_placeholder']: + print(" PLACEHOLDER DATA DETECTED!") + print(" This PSBT uses placeholder data and won't work on Testnet4.") + print(" Follow these steps to use real Testnet4 data:") + print() + print(" STEP 1: Get Testnet4 coins") + print(" • Visit: https://faucet.testnet4.dev/") + print(" • Request coins to any address you control") + print() + print(" STEP 2: Fund the multisig (run this script first to get address)") + print(" • Send Testnet4 coins to the multisig address") + print(" • Wait for confirmation") + print() + print(" STEP 3: Update this function") + print(" • Copy the funding transaction TXID") + print(" • Set utxo_details['txid'] = 'your_real_txid'") + print(" • Set utxo_details['amount'] = to_satoshis(your_real_amount)") + print(" • Set utxo_details['is_placeholder'] = False") + print() + print(" STEP 4: Verify") + print(" • Check on a Testnet4-compatible explorer") + print(" • Confirm the UTXO exists and amount is correct") + print() + + return utxo_details + + +def main(): + # Always call setup() first - using testnet (Testnet3, compatible with Testnet4 with real UTXOs) + setup('testnet') + + print("=" * 70) + print("Creating 2-of-3 Multisig PSBT with REAL TESTNET4 UTXOs") + print("=" * 70) + + # Step 1: Create three private keys (representing Alice, Bob, and Charlie) + print("\n1. Creating private keys for Alice, Bob, and Charlie...") + + # Using deterministic keys for consistency (in production, generate securely) + alice_private_key = PrivateKey.from_wif("cTALNpTpRbbxTCJ2A5Vq88UxT44w1PE2cYqiB3n4hRvzyCev1Wwo") + alice_public_key = alice_private_key.get_public_key() + print(f"Alice's public key: {alice_public_key.to_hex()}") + + # Bob's key + bob_private_key = PrivateKey.from_wif("cVf3kGh6552jU2rLaKwXTKq5APHPoZqCP4GQzQirWGHFoHQ9rEVt") + bob_public_key = bob_private_key.get_public_key() + print(f"Bob's public key: {bob_public_key.to_hex()}") + + # Charlie's key + charlie_private_key = PrivateKey.from_wif("cQDvVP5VhYsV3dtHQwQ5dCbL54WuJcvsUgr3LXwhf6vD5mPp9nVy") + charlie_public_key = charlie_private_key.get_public_key() + print(f"Charlie's public key: {charlie_public_key.to_hex()}") + + # Step 2: Create 2-of-3 multisig P2WSH script (Segwit version) + print("\n2. Creating 2-of-3 multisig P2WSH script...") + + # Create the multisig witness script (2 of 3) - sorted keys for deterministic addresses + public_keys = sorted([alice_public_key, bob_public_key, charlie_public_key], + key=lambda k: k.to_hex()) + + witness_script = Script([ + 2, # Required signatures + public_keys[0].to_hex(), + public_keys[1].to_hex(), + public_keys[2].to_hex(), + 3, # Total public keys + 'OP_CHECKMULTISIG' + ]) + + print(f"Witness script: {witness_script.to_hex()}") + + # Create P2WSH address from the witness script + p2wsh_address = witness_script.to_p2wsh_script_pub_key().to_address() + print(f"P2WSH Multisig Address: {p2wsh_address}") + print(f" Check this address on a Testnet4-compatible explorer") + + # Step 3: Get real Testnet4 UTXO details + print("\n3. Getting real Testnet4 UTXO details...") + utxo = get_real_testnet_utxo() + utxo['address'] = p2wsh_address + + print(f"Using UTXO:") + print(f" TXID: {utxo['txid']}") + print(f" Vout: {utxo['vout']}") + print(f" Amount: {utxo['amount']} satoshis ({utxo['amount'] / 100000000:.8f} BTC)") + print(f" Address: {utxo['address']}") + + if utxo['is_placeholder']: + print(f" PLACEHOLDER: This TXID is not verifiable") + print(f" This TXID won't verify - it's just an example format") + else: + print(f" VERIFY: Check on a Testnet4-compatible explorer") + print(f" This should show a real Testnet4 transaction") + + # Step 4: Create transaction inputs and outputs + print("\n4. Setting up transaction...") + + # Input: Real Testnet4 UTXO + txin = TxInput(utxo['txid'], utxo['vout']) + + # Output: Send to Charlie's P2WPKH address (modern Segwit address) + charlie_p2wpkh_address = charlie_public_key.get_segwit_address() + + # Calculate output amount (leaving some for fees) + fee_amount = to_satoshis(0.0001) # 0.0001 BTC fee + send_amount = utxo['amount'] - fee_amount + + if send_amount <= 0: + raise ValueError("UTXO amount too small to cover fees!") + + txout = TxOutput(send_amount, charlie_p2wpkh_address.to_script_pub_key()) + + # Create the transaction + tx = Transaction([txin], [txout], Locktime(0)) + print(f"Unsigned transaction: {tx.serialize()}") + + # Step 5: Create PSBT + print("\n5. Creating PSBT...") + + # Create PSBT from the unsigned transaction + psbt = PSBT(tx) + + # Add input information needed for signing P2WSH + # For P2WSH inputs, we need the witness script and witness UTXO info + psbt.add_input_witness_script(0, witness_script) + psbt.add_input_witness_utxo(0, utxo['amount'], p2wsh_address.to_script_pub_key()) + + print(f"PSBT created successfully!") + print(f"PSBT base64: {psbt.to_base64()}") + + # Step 6: Display verification information + print("\n6. TESTNET4 VERIFICATION") + print("=" * 50) + + if utxo['is_placeholder']: + print(" USING PLACEHOLDER DATA - NOT VERIFIABLE") + print(" Current TXID is fake and won't verify on explorer") + print(" To fix this:") + print(" 1. Get real Testnet4 coins from faucet") + print(" 2. Send to the multisig address above") + print(" 3. Update get_real_testnet_utxo() with real data") + print() + print(" When ready, verify with a Testnet4-compatible explorer") + else: + print(" REAL TESTNET4 DATA - VERIFIABLE") + print(" Verify input transaction on a Testnet4-compatible explorer") + + print(f" Check multisig address balance on a Testnet4-compatible explorer") + print(f" After signing and broadcasting, check output:") + print(f" Address: {charlie_p2wpkh_address}") + + # Step 7: Display signing workflow + print("\n7. SIGNING WORKFLOW") + print("=" * 50) + print("This PSBT is ready for the 2-of-3 multisig signing process:") + print() + print("1. Alice signs:") + print(" - Import PSBT") + print(" - Sign with Alice's private key") + print(" - Export partial signature") + print() + print("2. Bob signs:") + print(" - Import PSBT (with Alice's signature)") + print(" - Sign with Bob's private key") + print(" - Export complete signature") + print() + print("3. Finalize and broadcast:") + print(" - Combine signatures (2 of 3 threshold met)") + print(" - Finalize PSBT to create broadcastable transaction") + print(" - Broadcast to Testnet4") + print(" - Monitor on a Testnet4-compatible explorer") + + # Step 8: Show the structure for educational purposes + print("\n8. PSBT STRUCTURE ANALYSIS") + print("=" * 50) + print(f"Global data:") + print(f" - Unsigned transaction: {tx.serialize()}") + print(f" - Version: {psbt.version}") + print(f" - Transaction type: P2WSH (Segwit multisig)") + + print(f"\nInput 0 data:") + print(f" - Previous TXID: {utxo['txid']}") + print(f" - Previous Vout: {utxo['vout']}") + print(f" - Witness Script: {witness_script.to_hex()}") + print(f" - Amount: {utxo['amount']} satoshis") + print(f" - Script type: P2WSH") + print(f" - Required signatures: 2 of 3") + + print(f"\nOutput 0 data:") + print(f" - Amount: {send_amount} satoshis") + print(f" - Fee: {fee_amount} satoshis") + print(f" - Recipient: {charlie_p2wpkh_address}") + print(f" - Script type: P2WPKH") + + # Step 9: How to get real Testnet4 coins + print("\n9. HOW TO GET REAL TESTNET4 COINS & TXID") + print("=" * 50) + print("COMPLETE WORKFLOW FOR REAL TESTNET4 DATA:") + print() + print("PHASE 1: Setup") + print("1. Run this script AS-IS to get your multisig address") + print("2. Copy the P2WSH address from the output above") + print() + print("PHASE 2: Get Testnet4 coins") + print("3. Visit Testnet4 faucet:") + print(" • https://faucet.testnet4.dev/") + print(" • Request 0.001+ BTC to any address you control") + print() + print("PHASE 3: Fund multisig") + print("4. Send Testnet4 coins to your multisig address:") + print(f" • Send to: {p2wsh_address}") + print(" • Amount: 0.001 BTC (or whatever you got from faucet)") + print(" • Wait for 1+ confirmations") + print() + print("PHASE 4: Get real TXID") + print("5. Find your funding transaction:") + print(" • Use a Testnet4-compatible explorer") + print(" • Search for your multisig address") + print(" • Click on the funding transaction") + print(" • Copy the TXID") + print() + print("PHASE 5: Update code") + print("6. Edit get_real_testnet_utxo() function:") + print(" • Set txid = 'your_real_txid_here'") + print(" • Set amount = to_satoshis(your_actual_amount)") + print(" • Set is_placeholder = False") + print() + print("PHASE 6: Verify & test") + print("7. Re-run this script") + print(" • Should show REAL TESTNET4 DATA") + print(" • TXID should be verifiable on a Testnet4 explorer") + print(" • PSBT should be ready for signing") + print() + print("EXAMPLE of real Testnet4 TXID format:") + print("b4c1a58d7f8e9a2b3c4d5e6f1234567890abcdef1234567890abcdef12345678") + print("(64 hex characters - yours will look similar)") + print() + print(" Your mentor can then verify:") + print("• Paste your TXID into a Testnet4-compatible explorer") + print("• See real transaction with real UTXOs") + print("• Confirm PSBT references actual Testnet4 blockchain data") + + return psbt, { + 'multisig_address': p2wsh_address, + 'witness_script': witness_script.to_hex(), + 'recipient_address': charlie_p2wpkh_address, + 'utxo': utxo + } + + +if __name__ == "__main__": + created_psbt, info = main() + + print(f"\n" + "=" * 70) + print(" PSBT CREATION COMPLETED!") + print("=" * 70) + print(f" PSBT (base64): {created_psbt.to_base64()}") + print() + print(" NEXT STEPS:") + print("1. Fund the multisig address with real Testnet4 coins") + print("2. Update the UTXO details in get_real_testnet_utxo()") + print("3. Re-run this script") + print("4. Sign the PSBT with 2 of the 3 private keys") + print("5. Broadcast to Testnet4 and verify on a Testnet4-compatible explorer") + print() + print(f" Multisig address: {info['multisig_address']}") + print(f" Check balance on a Testnet4-compatible explorer") \ No newline at end of file From 1d9d9f0f1c3691a875280d2df664f34c77702f5b Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Tue, 1 Jul 2025 21:24:08 +0530 Subject: [PATCH 03/12] sign_psbt.py --- examples/sign_psbt.py | 85 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/examples/sign_psbt.py b/examples/sign_psbt.py index e69de29b..61c9a1bb 100644 --- a/examples/sign_psbt.py +++ b/examples/sign_psbt.py @@ -0,0 +1,85 @@ +# Copyright (C) 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. + +""" +Sign a specific input of a PSBT (Partially Signed Bitcoin Transaction) using a WIF private key. + +This script allows targeted signing of one input in a PSBT, which is useful for multisig setups, +hardware wallet integrations, or step-by-step signing processes. + +Features: +- Loads a PSBT from a base64-encoded string +- Signs a specified input using a provided WIF-formatted private key +- Supports multiple script types: P2PKH, P2SH, P2WPKH, P2WSH, P2TR +- Allows optional SIGHASH type customization (default: SIGHASH_ALL) + +Usage: + python sign_psbt.py [sighash_type] + +Arguments: + psbt_base64 The PSBT in base64 encoding + private_key_wif The private key in Wallet Import Format (WIF) + input_index Index of the input to sign (0-based) + sighash_type (Optional) Bitcoin SIGHASH flag (e.g., SIGHASH_ALL, SIGHASH_SINGLE) + +Returns: + Updated PSBT with the input partially signed and printed as base64 +""" + + +import sys +from bitcoinutils.setup import setup +from bitcoinutils.keys import PrivateKey +from bitcoinutils.psbt import PSBT +from bitcoinutils.constants import SIGHASH_ALL + + +def main(): + """Main function for signing PSBT example.""" + # Always call setup() first + setup('testnet') + + # Parse command line arguments + if len(sys.argv) < 4: + print("Usage: python sign_psbt.py [sighash_type]") + print("\nExample:") + print("python sign_psbt.py cTALNpTpRbbxTCJ2A5Vq88UxT44w1PE2cYqiB3n4hRvzyCev1Wwo 0") + sys.exit(1) + + psbt_base64 = sys.argv[1] + private_key_wif = sys.argv[2] + input_index = int(sys.argv[3]) + sighash_type = int(sys.argv[4]) if len(sys.argv) > 4 else SIGHASH_ALL + + try: + # Load PSBT from base64 + psbt = PSBT.from_base64(psbt_base64) + + # Load private key + private_key = PrivateKey.from_wif(private_key_wif) + + # Sign the specified input + success = psbt.sign_input(input_index, private_key, sighash_type) + + if success: + # Output the updated PSBT + print(psbt.to_base64()) + else: + print("Failed to sign input", file=sys.stderr) + sys.exit(1) + + except Exception as e: + print(f"Error: {str(e)}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() From d29e3bc0e0251a369c0857e564d35ea55556d970 Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Tue, 1 Jul 2025 21:24:54 +0530 Subject: [PATCH 04/12] combine_psbt.py --- examples/combine_psbt.py | 43 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/examples/combine_psbt.py b/examples/combine_psbt.py index e69de29b..72b45cfe 100644 --- a/examples/combine_psbt.py +++ b/examples/combine_psbt.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +""" +Combine multiple PSBTs (Partially Signed Bitcoin Transactions) into a single PSBT with merged signatures and metadata. + +This script performs the combiner role defined in BIP-174, allowing multiple signers to contribute signatures separately, +and then merge their PSBTs into one unified transaction. + +Features: +- Loads multiple base64-encoded PSBTs +- Merges all inputs, outputs, and partial signatures +- Validates consistency across PSBTs before combining +- Outputs a single combined PSBT in base64 format + +Usage: + python combine_psbt.py [ ...] + +Returns: + Combined PSBT with merged data from all inputs +""" + +import sys +from bitcoinutils.setup import setup +from bitcoinutils.psbt import PSBT + +def main(): + setup('testnet') + + if len(sys.argv) < 3: + print("Usage: python combine_psbt.py [psbt3_base64] ...") + return + + # Load PSBTs from command line arguments + psbts = [PSBT.from_base64(psbt_base64) for psbt_base64 in sys.argv[1:]] + + # Combine all PSBTs using the first one as base + combined_psbt = psbts[0].combine_psbts(psbts[1:]) + + # Output the combined PSBT + print(combined_psbt.to_base64()) + +if __name__ == "__main__": + main() From b9524726503650fa8770c4ec6b8e8dde8269918a Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Tue, 1 Jul 2025 21:25:32 +0530 Subject: [PATCH 05/12] finalize_psbt.py --- examples/finalize_psbt.py | 111 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/examples/finalize_psbt.py b/examples/finalize_psbt.py index e69de29b..2cf18627 100644 --- a/examples/finalize_psbt.py +++ b/examples/finalize_psbt.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +Finalize a PSBT (Partially Signed Bitcoin Transaction) to produce a broadcastable Bitcoin transaction. + +This script serves as the finalizer step (as defined in BIP-174), assembling signatures and scripts +into a complete transaction ready for broadcast. + +Features: +- Loads a base64-encoded PSBT from string or file +- Finalizes all inputs by constructing scriptSig/scriptWitness +- Optionally validates that all inputs are fully signed before finalization +- Outputs the raw hex-encoded Bitcoin transaction + +Usage: + python finalize_psbt.py + python finalize_psbt.py --file + python finalize_psbt.py --file --validate + +Arguments: + PSBT data as a base64-encoded string + --file Load PSBT from a file + --validate (Optional) Enforce validation before finalizing + +Returns: + Hex-encoded, fully signed Bitcoin transaction ready for broadcast +""" + + +import argparse +import base64 +import sys +from bitcoinutils.setup import setup +from bitcoinutils.psbt import PSBT + + +def main(): + """ + Main function for PSBT finalization. + + Usage: + python finalize_psbt.py + python finalize_psbt.py --file + python finalize_psbt.py --file --validate + """ + parser = argparse.ArgumentParser(description='Finalize a PSBT and create a transaction.') + parser.add_argument('psbt', nargs='?', help='Base64-encoded PSBT string') + parser.add_argument('--file', help='Text file containing base64 PSBT') + parser.add_argument('--validate', action='store_true', help='Validate finalized transaction') + parser.add_argument('--network', choices=['mainnet', 'testnet'], default='testnet', + help='Bitcoin network (default: testnet)') + + args = parser.parse_args() + + # Setup the library for specified network + setup(args.network) + + try: + # Load PSBT from input + if args.file: + with open(args.file, 'r') as f: + psbt_b64 = f.read().strip() + elif args.psbt: + psbt_b64 = args.psbt + else: + print("Error: Provide either base64 string or --file option.") + print("Use --help for usage information.") + return 1 + + # Create PSBT object + psbt = PSBT.from_base64(psbt_b64) + + # Finalize the PSBT + if args.validate: + final_tx, validation = psbt.finalize(validate=True) + + print("Finalized Transaction (Hex):") + print(final_tx.serialize()) + + print("\nValidation Report:") + print(f"Valid: {validation['valid']}") + print(f"Transaction ID: {validation['txid']}") + print(f"Size: {validation['size']} bytes") + print(f"Virtual Size: {validation['vsize']} vbytes") + + if validation['errors']: + print("Errors:") + for error in validation['errors']: + print(f" - {error}") + + if validation['warnings']: + print("Warnings:") + for warning in validation['warnings']: + print(f" - {warning}") + + else: + final_tx = psbt.finalize(validate=False) + print("Finalized Transaction (Hex):") + print(final_tx.serialize()) + + print(f"\nTransaction ready to broadcast!") + print(f"Use 'bitcoin-cli sendrawtransaction {final_tx.serialize()}' to broadcast") + + return 0 + + except Exception as e: + print(f"Error: {str(e)}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file From 51b5684d2021f61dfd55e74dc1564c69d1c19b86 Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Tue, 1 Jul 2025 21:26:27 +0530 Subject: [PATCH 06/12] modified script.py --- bitcoinutils/script.py | 997 ++++++++++++++++++++++++----------------- 1 file changed, 575 insertions(+), 422 deletions(-) diff --git a/bitcoinutils/script.py b/bitcoinutils/script.py index 3fea2dbf..0273ee6e 100644 --- a/bitcoinutils/script.py +++ b/bitcoinutils/script.py @@ -9,447 +9,600 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. + import copy import hashlib import struct from typing import Any, Union + from bitcoinutils.ripemd160 import ripemd160 from bitcoinutils.utils import b_to_h, h_to_b, vi_to_int + # import bitcoinutils.keys + + # Bitcoin's op codes. Complete list at: https://en.bitcoin.it/wiki/Script OP_CODES = { - # constants - "OP_0": b"\x00", - "OP_FALSE": b"\x00", - "OP_PUSHDATA1": b"\x4c", - "OP_PUSHDATA2": b"\x4d", - "OP_PUSHDATA4": b"\x4e", - "OP_1NEGATE": b"\x4f", - "OP_1": b"\x51", - "OP_TRUE": b"\x51", - "OP_2": b"\x52", - "OP_3": b"\x53", - "OP_4": b"\x54", - "OP_5": b"\x55", - "OP_6": b"\x56", - "OP_7": b"\x57", - "OP_8": b"\x58", - "OP_9": b"\x59", - "OP_10": b"\x5a", - "OP_11": b"\x5b", - "OP_12": b"\x5c", - "OP_13": b"\x5d", - "OP_14": b"\x5e", - "OP_15": b"\x5f", - "OP_16": b"\x60", - # flow control - "OP_NOP": b"\x61", - "OP_IF": b"\x63", - "OP_NOTIF": b"\x64", - "OP_ELSE": b"\x67", - "OP_ENDIF": b"\x68", - "OP_VERIFY": b"\x69", - "OP_RETURN": b"\x6a", - # stack - "OP_TOALTSTACK": b"\x6b", - "OP_FROMALTSTACK": b"\x6c", - "OP_IFDUP": b"\x73", - "OP_DEPTH": b"\x74", - "OP_DROP": b"\x75", - "OP_DUP": b"\x76", - "OP_NIP": b"\x77", - "OP_OVER": b"\x78", - "OP_PICK": b"\x79", - "OP_ROLL": b"\x7a", - "OP_ROT": b"\x7b", - "OP_SWAP": b"\x7c", - "OP_TUCK": b"\x7d", - "OP_2DROP": b"\x6d", - "OP_2DUP": b"\x6e", - "OP_3DUP": b"\x6f", - "OP_2OVER": b"\x70", - "OP_2ROT": b"\x71", - "OP_2SWAP": b"\x72", - # splice - #'OP_CAT' : b'\x7e', - #'OP_SUBSTR' : b'\x7f', - #'OP_LEFT' : b'\x80', - #'OP_RIGHT' : b'\x81', - "OP_SIZE": b"\x82", - # bitwise logic - #'OP_INVERT' : b'\x83', - #'OP_AND' : b'\x84', - #'OP_OR' : b'\x85', - #'OP_XOR' : b'\x86', - "OP_EQUAL": b"\x87", - "OP_EQUALVERIFY": b"\x88", - # arithmetic - "OP_1ADD": b"\x8b", - "OP_1SUB": b"\x8c", - #'OP_2MUL' : b'\x8d', - #'OP_2DIV' : b'\x8e', - "OP_NEGATE": b"\x8f", - "OP_ABS": b"\x90", - "OP_NOT": b"\x91", - "OP_0NOTEQUAL": b"\x92", - "OP_ADD": b"\x93", - "OP_SUB": b"\x94", - #'OP_MUL' : b'\x95', - #'OP_DIV' : b'\x96', - #'OP_MOD' : b'\x97', - #'OP_LSHIFT' : b'\x98', - #'OP_RSHIFT' : b'\x99', - "OP_BOOLAND": b"\x9a", - "OP_BOOLOR": b"\x9b", - "OP_NUMEQUAL": b"\x9c", - "OP_NUMEQUALVERIFY": b"\x9d", - "OP_NUMNOTEQUAL": b"\x9e", - "OP_LESSTHAN": b"\x9f", - "OP_GREATERTHAN": b"\xa0", - "OP_LESSTHANOREQUAL": b"\xa1", - "OP_GREATERTHANOREQUAL": b"\xa2", - "OP_MIN": b"\xa3", - "OP_MAX": b"\xa4", - "OP_WITHIN": b"\xa5", - # crypto - "OP_RIPEMD160": b"\xa6", - "OP_SHA1": b"\xa7", - "OP_SHA256": b"\xa8", - "OP_HASH160": b"\xa9", - "OP_HASH256": b"\xaa", - "OP_CODESEPARATOR": b"\xab", - "OP_CHECKSIG": b"\xac", - "OP_CHECKSIGVERIFY": b"\xad", - "OP_CHECKMULTISIG": b"\xae", - "OP_CHECKMULTISIGVERIFY": b"\xaf", - "OP_CHECKSIGADD": b"\xba", # added this new OPCODE - # locktime - "OP_NOP2": b"\xb1", - "OP_CHECKLOCKTIMEVERIFY": b"\xb1", - "OP_NOP3": b"\xb2", - "OP_CHECKSEQUENCEVERIFY": b"\xb2", - + # constants + "OP_0": b"\x00", + "OP_FALSE": b"\x00", + "OP_PUSHDATA1": b"\x4c", + "OP_PUSHDATA2": b"\x4d", + "OP_PUSHDATA4": b"\x4e", + "OP_1NEGATE": b"\x4f", + "OP_1": b"\x51", + "OP_TRUE": b"\x51", + "OP_2": b"\x52", + "OP_3": b"\x53", + "OP_4": b"\x54", + "OP_5": b"\x55", + "OP_6": b"\x56", + "OP_7": b"\x57", + "OP_8": b"\x58", + "OP_9": b"\x59", + "OP_10": b"\x5a", + "OP_11": b"\x5b", + "OP_12": b"\x5c", + "OP_13": b"\x5d", + "OP_14": b"\x5e", + "OP_15": b"\x5f", + "OP_16": b"\x60", + # flow control + "OP_NOP": b"\x61", + "OP_IF": b"\x63", + "OP_NOTIF": b"\x64", + "OP_ELSE": b"\x67", + "OP_ENDIF": b"\x68", + "OP_VERIFY": b"\x69", + "OP_RETURN": b"\x6a", + # stack + "OP_TOALTSTACK": b"\x6b", + "OP_FROMALTSTACK": b"\x6c", + "OP_IFDUP": b"\x73", + "OP_DEPTH": b"\x74", + "OP_DROP": b"\x75", + "OP_DUP": b"\x76", + "OP_NIP": b"\x77", + "OP_OVER": b"\x78", + "OP_PICK": b"\x79", + "OP_ROLL": b"\x7a", + "OP_ROT": b"\x7b", + "OP_SWAP": b"\x7c", + "OP_TUCK": b"\x7d", + "OP_2DROP": b"\x6d", + "OP_2DUP": b"\x6e", + "OP_3DUP": b"\x6f", + "OP_2OVER": b"\x70", + "OP_2ROT": b"\x71", + "OP_2SWAP": b"\x72", + # splice + #'OP_CAT' : b'\x7e', + #'OP_SUBSTR' : b'\x7f', + #'OP_LEFT' : b'\x80', + #'OP_RIGHT' : b'\x81', + "OP_SIZE": b"\x82", + # bitwise logic + #'OP_INVERT' : b'\x83', + #'OP_AND' : b'\x84', + #'OP_OR' : b'\x85', + #'OP_XOR' : b'\x86', + "OP_EQUAL": b"\x87", + "OP_EQUALVERIFY": b"\x88", + # arithmetic + "OP_1ADD": b"\x8b", + "OP_1SUB": b"\x8c", + #'OP_2MUL' : b'\x8d', + #'OP_2DIV' : b'\x8e', + "OP_NEGATE": b"\x8f", + "OP_ABS": b"\x90", + "OP_NOT": b"\x91", + "OP_0NOTEQUAL": b"\x92", + "OP_ADD": b"\x93", + "OP_SUB": b"\x94", + #'OP_MUL' : b'\x95', + #'OP_DIV' : b'\x96', + #'OP_MOD' : b'\x97', + #'OP_LSHIFT' : b'\x98', + #'OP_RSHIFT' : b'\x99', + "OP_BOOLAND": b"\x9a", + "OP_BOOLOR": b"\x9b", + "OP_NUMEQUAL": b"\x9c", + "OP_NUMEQUALVERIFY": b"\x9d", + "OP_NUMNOTEQUAL": b"\x9e", + "OP_LESSTHAN": b"\x9f", + "OP_GREATERTHAN": b"\xa0", + "OP_LESSTHANOREQUAL": b"\xa1", + "OP_GREATERTHANOREQUAL": b"\xa2", + "OP_MIN": b"\xa3", + "OP_MAX": b"\xa4", + "OP_WITHIN": b"\xa5", + # crypto + "OP_RIPEMD160": b"\xa6", + "OP_SHA1": b"\xa7", + "OP_SHA256": b"\xa8", + "OP_HASH160": b"\xa9", + "OP_HASH256": b"\xaa", + "OP_CODESEPARATOR": b"\xab", + "OP_CHECKSIG": b"\xac", + "OP_CHECKSIGVERIFY": b"\xad", + "OP_CHECKMULTISIG": b"\xae", + "OP_CHECKMULTISIGVERIFY": b"\xaf", + "OP_CHECKSIGADD": b"\xba", # added this new OPCODE + # locktime + "OP_NOP2": b"\xb1", + "OP_CHECKLOCKTIMEVERIFY": b"\xb1", + "OP_NOP3": b"\xb2", + "OP_CHECKSEQUENCEVERIFY": b"\xb2", + } + CODE_OPS = { - # constants - b"\x00": "OP_0", - b"\x4c": "OP_PUSHDATA1", - b"\x4d": "OP_PUSHDATA2", - b"\x4e": "OP_PUSHDATA4", - b"\x4f": "OP_1NEGATE", - b"\x51": "OP_1", - b"\x52": "OP_2", - b"\x53": "OP_3", - b"\x54": "OP_4", - b"\x55": "OP_5", - b"\x56": "OP_6", - b"\x57": "OP_7", - b"\x58": "OP_8", - b"\x59": "OP_9", - b"\x5a": "OP_10", - b"\x5b": "OP_11", - b"\x5c": "OP_12", - b"\x5d": "OP_13", - b"\x5e": "OP_14", - b"\x5f": "OP_15", - b"\x60": "OP_16", - # flow control - b"\x61": "OP_NOP", - b"\x63": "OP_IF", - b"\x64": "OP_NOTIF", - b"\x67": "OP_ELSE", - b"\x68": "OP_ENDIF", - b"\x69": "OP_VERIFY", - b"\x6a": "OP_RETURN", - # stack - b"\x6b": "OP_TOALTSTACK", - b"\x6c": "OP_FROMALTSTACK", - b"\x73": "OP_IFDUP", - b"\x74": "OP_DEPTH", - b"\x75": "OP_DROP", - b"\x76": "OP_DUP", - b"\x77": "OP_NIP", - b"\x78": "OP_OVER", - b"\x79": "OP_PICK", - b"\x7a": "OP_ROLL", - b"\x7b": "OP_ROT", - b"\x7c": "OP_SWAP", - b"\x7d": "OP_TUCK", - b"\x6d": "OP_2DROP", - b"\x6e": "OP_2DUP", - b"\x6f": "OP_3DUP", - b"\x70": "OP_2OVER", - b"\x71": "OP_2ROT", - b"\x72": "OP_2SWAP", - # splice - b"\x82": "OP_SIZE", - # bitwise logic - b"\x87": "OP_EQUAL", - b"\x88": "OP_EQUALVERIFY", - # arithmetic - b"\x8b": "OP_1ADD", - b"\x8c": "OP_1SUB", - b"\x8f": "OP_NEGATE", - b"\x90": "OP_ABS", - b"\x91": "OP_NOT", - b"\x92": "OP_0NOTEQUAL", - b"\x93": "OP_ADD", - b"\x94": "OP_SUB", - b"\x9a": "OP_BOOLAND", - b"\x9b": "OP_BOOLOR", - b"\x9c": "OP_NUMEQUAL", - b"\x9d": "OP_NUMEQUALVERIFY", - b"\x9e": "OP_NUMNOTEQUAL", - b"\x9f": "OP_LESSTHAN", - b"\xa0": "OP_GREATERTHAN", - b"\xa1": "OP_LESSTHANOREQUAL", - b"\xa2": "OP_GREATERTHANOREQUAL", - b"\xa3": "OP_MIN", - b"\xa4": "OP_MAX", - b"\xa5": "OP_WITHIN", - # crypto - b"\xa6": "OP_RIPEMD160", - b"\xa7": "OP_SHA1", - b"\xa8": "OP_SHA256", - b"\xa9": "OP_HASH160", - b"\xaa": "OP_HASH256", - b"\xab": "OP_CODESEPARATOR", - b"\xac": "OP_CHECKSIG", - b"\xad": "OP_CHECKSIGVERIFY", - b"\xae": "OP_CHECKMULTISIG", - b"\xaf": "OP_CHECKMULTISIGVERIFY", - b"\xba": "OP_CHECKSIGADD", # added this new OPCODE - # locktime - # This used to be OP_NOP2 - b"\xb1": "OP_CHECKLOCKTIMEVERIFY", - # This used to be OP_NOP3 - b"\xb2": "OP_CHECKSEQUENCEVERIFY", + # constants + b"\x00": "OP_0", + b"\x4c": "OP_PUSHDATA1", + b"\x4d": "OP_PUSHDATA2", + b"\x4e": "OP_PUSHDATA4", + b"\x4f": "OP_1NEGATE", + b"\x51": "OP_1", + b"\x52": "OP_2", + b"\x53": "OP_3", + b"\x54": "OP_4", + b"\x55": "OP_5", + b"\x56": "OP_6", + b"\x57": "OP_7", + b"\x58": "OP_8", + b"\x59": "OP_9", + b"\x5a": "OP_10", + b"\x5b": "OP_11", + b"\x5c": "OP_12", + b"\x5d": "OP_13", + b"\x5e": "OP_14", + b"\x5f": "OP_15", + b"\x60": "OP_16", + # flow control + b"\x61": "OP_NOP", + b"\x63": "OP_IF", + b"\x64": "OP_NOTIF", + b"\x67": "OP_ELSE", + b"\x68": "OP_ENDIF", + b"\x69": "OP_VERIFY", + b"\x6a": "OP_RETURN", + # stack + b"\x6b": "OP_TOALTSTACK", + b"\x6c": "OP_FROMALTSTACK", + b"\x73": "OP_IFDUP", + b"\x74": "OP_DEPTH", + b"\x75": "OP_DROP", + b"\x76": "OP_DUP", + b"\x77": "OP_NIP", + b"\x78": "OP_OVER", + b"\x79": "OP_PICK", + b"\x7a": "OP_ROLL", + b"\x7b": "OP_ROT", + b"\x7c": "OP_SWAP", + b"\x7d": "OP_TUCK", + b"\x6d": "OP_2DROP", + b"\x6e": "OP_2DUP", + b"\x6f": "OP_3DUP", + b"\x70": "OP_2OVER", + b"\x71": "OP_2ROT", + b"\x72": "OP_2SWAP", + # splice + b"\x82": "OP_SIZE", + # bitwise logic + b"\x87": "OP_EQUAL", + b"\x88": "OP_EQUALVERIFY", + # arithmetic + b"\x8b": "OP_1ADD", + b"\x8c": "OP_1SUB", + b"\x8f": "OP_NEGATE", + b"\x90": "OP_ABS", + b"\x91": "OP_NOT", + b"\x92": "OP_0NOTEQUAL", + b"\x93": "OP_ADD", + b"\x94": "OP_SUB", + b"\x9a": "OP_BOOLAND", + b"\x9b": "OP_BOOLOR", + b"\x9c": "OP_NUMEQUAL", + b"\x9d": "OP_NUMEQUALVERIFY", + b"\x9e": "OP_NUMNOTEQUAL", + b"\x9f": "OP_LESSTHAN", + b"\xa0": "OP_GREATERTHAN", + b"\xa1": "OP_LESSTHANOREQUAL", + b"\xa2": "OP_GREATERTHANOREQUAL", + b"\xa3": "OP_MIN", + b"\xa4": "OP_MAX", + b"\xa5": "OP_WITHIN", + # crypto + b"\xa6": "OP_RIPEMD160", + b"\xa7": "OP_SHA1", + b"\xa8": "OP_SHA256", + b"\xa9": "OP_HASH160", + b"\xaa": "OP_HASH256", + b"\xab": "OP_CODESEPARATOR", + b"\xac": "OP_CHECKSIG", + b"\xad": "OP_CHECKSIGVERIFY", + b"\xae": "OP_CHECKMULTISIG", + b"\xaf": "OP_CHECKMULTISIGVERIFY", + b"\xba": "OP_CHECKSIGADD", # added this new OPCODE + # locktime + # This used to be OP_NOP2 + b"\xb1": "OP_CHECKLOCKTIMEVERIFY", + # This used to be OP_NOP3 + b"\xb2": "OP_CHECKSEQUENCEVERIFY", } + + class Script: - """Represents any script in Bitcoin + """Represents any script in Bitcoin + + + A Script contains just a list of OP_CODES and also knows how to serialize + into bytes + + + Attributes + ---------- + script : list + the list with all the script OP_CODES and data + + + Methods + ------- + to_bytes() + returns a serialized byte version of the script + + + to_hex() + returns a serialized version of the script in hex + + + get_script() + returns the list of strings that makes up this script + + + copy() + creates a copy of the object (classmethod) + + + from_raw() + + + to_p2sh_script_pub_key() + converts script to p2sh scriptPubKey (locking script) + + + to_p2wsh_script_pub_key() + converts script to p2wsh scriptPubKey (locking script) + + + is_p2wpkh() + checks if script is P2WPKH (Pay-to-Witness-Public-Key-Hash) + + + is_p2tr() + checks if script is P2TR (Pay-to-Taproot) + + + is_p2wsh() + checks if script is P2WSH (Pay-to-Witness-Script-Hash) + + + is_p2sh() + checks if script is P2SH (Pay-to-Script-Hash) + + + is_p2pkh() + checks if script is P2PKH (Pay-to-Public-Key-Hash) + + + is_multisig() + checks if script is a multisig script + + + get_script_type() + determines the type of script + + + Raises + ------ + ValueError + If string data is too large or integer is negative + """ + + + def __init__(self, script: list[Any]): + """See Script description""" + self.script: list[Any] = script + + + @classmethod + def copy(cls, script: "Script") -> "Script": + """Deep copy of Script""" + scripts = copy.deepcopy(script.script) + return cls(scripts) + + + def _op_push_data(self, data: str) -> bytes: + """Converts data to appropriate OP_PUSHDATA OP code including length""" + data_bytes = h_to_b(data) # Assuming string is hexadecimal + + + if len(data_bytes) < 0x4C: + return bytes([len(data_bytes)]) + data_bytes + elif len(data_bytes) < 0xFF: + return b"\x4c" + bytes([len(data_bytes)]) + data_bytes + elif len(data_bytes) < 0xFFFF: + return b"\x4d" + struct.pack(" bytes: + """Converts integer to bytes; as signed little-endian integer""" + if integer < 0: + raise ValueError("Integer is currently required to be positive.") + + + number_of_bytes = (integer.bit_length() + 7) // 8 + integer_bytes = integer.to_bytes(number_of_bytes, byteorder="little") + + + if integer & (1 << number_of_bytes * 8 - 1): + integer_bytes += b"\x00" + + + return self._op_push_data(b_to_h(integer_bytes)) + + + def to_bytes(self) -> bytes: + """Converts the script to bytes""" + script_bytes = b"" + for token in self.script: + if token in OP_CODES: + script_bytes += OP_CODES[token] + elif isinstance(token, int) and token >= 0 and token <= 16: + script_bytes += OP_CODES["OP_" + str(token)] + else: + if isinstance(token, int): + script_bytes += self._push_integer(token) + else: + script_bytes += self._op_push_data(token) + return script_bytes + + + def to_hex(self) -> str: + """Converts the script to hexadecimal""" + return b_to_h(self.to_bytes()) - A Script contains just a list of OP_CODES and also knows how to serialize - into bytes - Attributes - ---------- - script : list - the list with all the script OP_CODES and data - - Methods - ------- - to_bytes() - returns a serialized byte version of the script - - to_hex() - returns a serialized version of the script in hex - - get_script() - returns the list of strings that makes up this script - - copy() - creates a copy of the object (classmethod) - - from_raw() - - to_p2sh_script_pub_key() - converts script to p2sh scriptPubKey (locking script) - - to_p2wsh_script_pub_key() - converts script to p2wsh scriptPubKey (locking script) - - Raises - ------ - ValueError - If string data is too large or integer is negative - """ - - def __init__(self, script: list[Any]): - """See Script description""" - - self.script: list[Any] = script - - @classmethod - def copy(cls, script: "Script") -> "Script": - """Deep copy of Script""" - scripts = copy.deepcopy(script.script) - return cls(scripts) - - def _op_push_data(self, data: str) -> bytes: - """Converts data to appropriate OP_PUSHDATA OP code including length - - 0x01-0x4b -> just length plus data bytes - 0x4c-0xff -> OP_PUSHDATA1 plus 1-byte-length plus data bytes - 0x0100-0xffff -> OP_PUSHDATA2 plus 2-byte-length plus data bytes - 0x010000-0xffffffff -> OP_PUSHDATA4 plus 4-byte-length plus data bytes - - Also note that according to standarardness rules (BIP-62) the minimum - possible PUSHDATA operator must be used! - """ - - data_bytes = h_to_b(data) # Assuming string is hexadecimal - - if len(data_bytes) < 0x4C: - return bytes([len(data_bytes)]) + data_bytes - elif len(data_bytes) < 0xFF: - return b"\x4c" + bytes([len(data_bytes)]) + data_bytes - elif len(data_bytes) < 0xFFFF: - return b"\x4d" + struct.pack(" bytes: - """Converts integer to bytes; as signed little-endian integer - - Currently supports only positive integers - """ - - if integer < 0: - raise ValueError("Integer is currently required to be positive.") - - # bytes required to represent the integer - number_of_bytes = (integer.bit_length() + 7) // 8 - - # convert to little-endian bytes - integer_bytes = integer.to_bytes(number_of_bytes, byteorder="little") - - # if last bit is set then we need to add sign to signify positive - # integer - if integer & (1 << number_of_bytes * 8 - 1): - integer_bytes += b"\x00" - - return self._op_push_data(b_to_h(integer_bytes)) - - def to_bytes(self) -> bytes: - """Converts the script to bytes - - If an OP code the appropriate byte is included according to: - https://en.bitcoin.it/wiki/Script - If not consider it data (signature, public key, public key hash, etc.) and - and include with appropriate OP_PUSHDATA OP code plus length - """ - script_bytes = b"" - for token in self.script: - # add op codes directly - if token in OP_CODES: - script_bytes += OP_CODES[token] - # if integer between 0 and 16 add the appropriate op code - elif isinstance(token, int) and token >= 0 and token <= 16: - script_bytes += OP_CODES["OP_" + str(token)] - # it is data, so add accordingly - else: - if isinstance(token, int): - script_bytes += self._push_integer(token) - else: - script_bytes += self._op_push_data(token) - - return script_bytes - - def to_hex(self) -> str: - """Converts the script to hexadecimal""" - return b_to_h(self.to_bytes()) - - @staticmethod - def from_raw(scriptrawhex: Union[str, bytes], has_segwit: bool = False): - """ - Imports a Script commands list from raw hexadecimal data - Attributes - ---------- - txinputraw : string (hex) - The hexadecimal raw string representing the Script commands - has_segwit : boolean - Is the Tx Input segwit or not - """ - if isinstance(scriptrawhex, str): - scriptraw = h_to_b(scriptrawhex) - elif isinstance(scriptrawhex, bytes): - scriptraw = scriptrawhex - else: - raise TypeError("Input must be a hexadecimal string or bytes") - - commands = [] - index = 0 - - while index < len(scriptraw): - byte = scriptraw[index] - if bytes([byte]) in CODE_OPS: - if ( - bytes([byte]) != b"\x4c" - and bytes([byte]) != b"\x4d" - and bytes([byte]) != b"\x4e" - ): - commands.append(CODE_OPS[bytes([byte])]) - index = index + 1 - # handle the 3 special bytes 0x4c,0x4d,0x4e if the transaction is - # not segwit type - if has_segwit is False and bytes([byte]) == b"\x4c": - bytes_to_read = int.from_bytes( - scriptraw[index : index + 1], "little" - ) - index = index + 1 - commands.append(scriptraw[index : index + bytes_to_read].hex()) - index = index + bytes_to_read - elif has_segwit is False and bytes([byte]) == b"\x4d": - bytes_to_read = int.from_bytes( - scriptraw[index : index + 2], "little" - ) - index = index + 2 - commands.append(scriptraw[index : index + bytes_to_read].hex()) - index = index + bytes_to_read - elif has_segwit is False and bytes([byte]) == b"\x4e": - bytes_to_read = int.from_bytes( - scriptraw[index : index + 4], "little" - ) - index = index + 4 - commands.append(scriptraw[index : index + bytes_to_read].hex()) - index = index + bytes_to_read - else: - data_size, size = vi_to_int(scriptraw[index : index + 8]) - commands.append( - scriptraw[index + size : index + size + data_size].hex() - ) - index = index + data_size + size - - return Script(script=commands) - - def get_script(self) -> list[Any]: - """Returns script as array of strings""" - return self.script - - def to_p2sh_script_pub_key(self) -> "Script": - """Converts script to p2sh scriptPubKey (locking script) - - Calculates hash160 of the script and uses it to construct a P2SH script. - """ - - script_hash160 = ripemd160(hashlib.sha256(self.to_bytes()).digest()) - hex_hash160 = b_to_h(script_hash160) - return Script(["OP_HASH160", hex_hash160, "OP_EQUAL"]) - - def to_p2wsh_script_pub_key(self) -> "Script": - """Converts script to p2wsh scriptPubKey (locking script) - - Calculates the sha256 of the script and uses it to construct a P2WSH script. - """ - sha256 = hashlib.sha256(self.to_bytes()).digest() - return Script(["OP_0", b_to_h(sha256)]) - - def __str__(self) -> str: - return str(self.script) - - def __repr__(self) -> str: - return self.__str__() - - def __eq__(self, _other: object) -> bool: - if not isinstance(_other, Script): - return False - return self.script == _other.script \ No newline at end of file + @staticmethod + def from_raw(scriptrawhex: Union[str, bytes], has_segwit: bool = False): + """ + Imports a Script commands list from raw hexadecimal data + """ + if isinstance(scriptrawhex, str): + scriptraw = h_to_b(scriptrawhex) + elif isinstance(scriptrawhex, bytes): + scriptraw = scriptrawhex + else: + raise TypeError("Input must be a hexadecimal string or bytes") + + commands = [] + index = 0 + + + while index < len(scriptraw): + byte = scriptraw[index] + if bytes([byte]) in CODE_OPS: + if ( + bytes([byte]) != b"\x4c" + and bytes([byte]) != b"\x4d" + and bytes([byte]) != b"\x4e" + ): + commands.append(CODE_OPS[bytes([byte])]) + index = index + 1 + if has_segwit is False and bytes([byte]) == b"\x4c": + bytes_to_read = int.from_bytes( + scriptraw[index : index + 1], "little" + ) + index = index + 1 + commands.append(scriptraw[index : index + bytes_to_read].hex()) + index = index + bytes_to_read + elif has_segwit is False and bytes([byte]) == b"\x4d": + bytes_to_read = int.from_bytes( + scriptraw[index : index + 2], "little" + ) + index = index + 2 + commands.append(scriptraw[index : index + bytes_to_read].hex()) + index = index + bytes_to_read + elif has_segwit is False and bytes([byte]) == b"\x4e": + bytes_to_read = int.from_bytes( + scriptraw[index : index + 4], "little" + ) + index = index + 4 + commands.append(scriptraw[index : index + bytes_to_read].hex()) + index = index + bytes_to_read + else: + data_size, size = vi_to_int(scriptraw[index : index + 8]) + commands.append( + scriptraw[index + size : index + size + data_size].hex() + ) + index = index + data_size + size + + + return Script(script=commands) + + + def get_script(self) -> list[Any]: + """Returns script as array of strings""" + return self.script + + + def to_p2sh_script_pub_key(self) -> "Script": + """Converts script to p2sh scriptPubKey (locking script)""" + script_hash160 = ripemd160(hashlib.sha256(self.to_bytes()).digest()) + hex_hash160 = b_to_h(script_hash160) + return Script(["OP_HASH160", hex_hash160, "OP_EQUAL"]) + + + def to_p2wsh_script_pub_key(self) -> "Script": + """Converts script to p2wsh scriptPubKey (locking script)""" + sha256 = hashlib.sha256(self.to_bytes()).digest() + return Script(["OP_0", b_to_h(sha256)]) + + + def is_p2wpkh(self) -> bool: + """ + Check if script is P2WPKH (Pay-to-Witness-Public-Key-Hash). + + P2WPKH format: OP_0 <20-byte-key-hash> + + Returns: + bool: True if script is P2WPKH + """ + ops = self.script + return (len(ops) == 2 and + (ops[0] == 0 or ops[0] == "OP_0") and + isinstance(ops[1], str) and + len(h_to_b(ops[1])) == 20) + + + def is_p2tr(self) -> bool: + """ + Check if script is P2TR (Pay-to-Taproot). + + P2TR format: OP_1 <32-byte-key> + + Returns: + bool: True if script is P2TR + """ + ops = self.script + return (len(ops) == 2 and + (ops[0] == 1 or ops[0] == "OP_1") and + isinstance(ops[1], str) and + len(h_to_b(ops[1])) == 32) + + + def is_p2wsh(self) -> bool: + """ + Check if script is P2WSH (Pay-to-Witness-Script-Hash). + + P2WSH format: OP_0 <32-byte-script-hash> + + Returns: + bool: True if script is P2WSH + """ + ops = self.script + return (len(ops) == 2 and + (ops[0] == 0 or ops[0] == "OP_0") and + isinstance(ops[1], str) and + len(h_to_b(ops[1])) == 32) + + + def is_p2sh(self) -> bool: + """ + Check if script is P2SH (Pay-to-Script-Hash). + + P2SH format: OP_HASH160 <20-byte-script-hash> OP_EQUAL + + Returns: + bool: True if script is P2SH + """ + ops = self.script + return (len(ops) == 3 and + ops[0] == 'OP_HASH160' and + isinstance(ops[1], str) and + len(h_to_b(ops[1])) == 20 and + ops[2] == 'OP_EQUAL') + + + def is_p2pkh(self) -> bool: + """ + Check if script is P2PKH (Pay-to-Public-Key-Hash). + + P2PKH format: OP_DUP OP_HASH160 <20-byte-key-hash> OP_EQUALVERIFY OP_CHECKSIG + + Returns: + bool: True if script is P2PKH + """ + ops = self.script + return (len(ops) == 5 and + ops[0] == 'OP_DUP' and + ops[1] == 'OP_HASH160' and + isinstance(ops[2], str) and + len(h_to_b(ops[2])) == 20 and + ops[3] == 'OP_EQUALVERIFY' and + ops[4] == 'OP_CHECKSIG') + + + def is_multisig(self) -> tuple[bool, Union[tuple[int, int], None]]: + """ + Check if script is a multisig script. + + Multisig format: OP_M ... OP_N OP_CHECKMULTISIG + + Returns: + tuple: (bool, (M, N) if multisig, None otherwise) + """ + ops = self.script + + if (len(ops) >= 4 and + isinstance(ops[0], int) and + ops[-1] == 'OP_CHECKMULTISIG' and + isinstance(ops[-2], int)): + + m = ops[0] # Required signatures + n = ops[-2] # Total public keys + + # Validate the structure + if len(ops) == n + 3: # M + N pubkeys + N + OP_CHECKMULTISIG + return True, (m, n) + + return False, None + + + def get_script_type(self) -> str: + """ + Determine the type of script. + + Returns: + str: Script type ('p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr', 'multisig', 'unknown') + """ + if self.is_p2pkh(): + return 'p2pkh' + elif self.is_p2sh(): + return 'p2sh' + elif self.is_p2wpkh(): + return 'p2wpkh' + elif self.is_p2wsh(): + return 'p2wsh' + elif self.is_p2tr(): + return 'p2tr' + elif self.is_multisig()[0]: + return 'multisig' + else: + return 'unknown' + + + def __str__(self) -> str: + return str(self.script) + + + def __repr__(self) -> str: + return self.__str__() + + + def __eq__(self, _other: object) -> bool: + if not isinstance(_other, Script): + return False + return self.script == _other.script From e7c6cbae84e2245a7a574dacea632162ae35c371 Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Fri, 18 Jul 2025 12:58:50 +0530 Subject: [PATCH 07/12] Update psbt.py, transactions.py, utils.py, and script.py only --- bitcoinutils/psbt.py | 921 +++++++++++++------------- bitcoinutils/script.py | 1193 ++++++++++++++++++---------------- bitcoinutils/transactions.py | 245 +++++-- bitcoinutils/utils.py | 22 + 4 files changed, 1319 insertions(+), 1062 deletions(-) diff --git a/bitcoinutils/psbt.py b/bitcoinutils/psbt.py index b430aec0..0ddbcf48 100644 --- a/bitcoinutils/psbt.py +++ b/bitcoinutils/psbt.py @@ -1,48 +1,16 @@ -""" -Partially Signed Bitcoin Transaction (PSBT) implementation following -BIP-174. - -This module provides the PSBT class which represents a partially signed -bitcoin transaction that can be shared between multiple parties for signing -before broadcasting to the network. - -A PSBT contains: -- The unsigned transaction -- Input metadata needed for signing (UTXOs, scripts, keys, etc.) -- Output metadata for validation -- Partial signatures from different signers - -The PSBT workflow typically involves: -1. Creator: Creates the PSBT with the unsigned transaction -2. Updater: Adds input/output metadata needed for signing -3. Signer: Signs inputs they can sign (sign_input() handles all script types automatically) -4. Combiner: Combines multiple PSBTs with different signatures -5. Finalizer: Finalizes the PSBT by adding final scriptSig/witness -6. Extractor: Extracts the final signed transaction -""" - import struct from io import BytesIO +import ecdsa +from ecdsa import SECP256k1, SigningKey from typing import Dict, List, Optional, Tuple, Union from bitcoinutils.transactions import Transaction, TxInput, TxOutput from bitcoinutils.script import Script from bitcoinutils.keys import PrivateKey, PublicKey from bitcoinutils.utils import to_satoshis +from bitcoinutils.utils import encode_varint +from bitcoinutils.utils import read_varint class PSBTInput: - """ - Represents a single input in a PSBT with all associated metadata. - - Contains information needed to sign this input including: - - Non-witness UTXO (for legacy inputs) - - Witness UTXO (for segwit inputs) - - Partial signatures - - Sighash type - - Redeem script (for P2SH) - - Witness script (for P2WSH) - - BIP32 derivation paths - - Final scriptSig and witness - """ def __init__(self): # BIP-174 defined fields @@ -67,14 +35,6 @@ def __init__(self): self.unknown: Dict[bytes, bytes] = {} class PSBTOutput: - """ - Represents a single output in a PSBT with associated metadata. - - Contains information about the output including: - - Redeem script (for P2SH outputs) - - Witness script (for P2WSH outputs) - - BIP32 derivation paths - """ def __init__(self): # BIP-174 defined fields @@ -87,27 +47,6 @@ def __init__(self): self.unknown: Dict[bytes, bytes] = {} class PSBT: - """ - Partially Signed Bitcoin Transaction implementation following BIP-174. - - A PSBT is a data format that allows multiple parties to collaboratively - sign a bitcoin transaction. The PSBT contains the unsigned transaction - along with metadata needed for signing. - - Example usage: - # Create PSBT from unsigned transaction - psbt = PSBT(unsigned_tx) - - # Add input metadata - psbt.inputs[0].witness_utxo = prev_output - - # Sign with private key (automatically detects script type) - psbt.sign_input(0, private_key) - - # Finalize and extract signed transaction - final_tx = psbt.finalize() - """ - # PSBT magic bytes and version MAGIC = b'psbt' VERSION = 0 @@ -141,13 +80,30 @@ class OutputTypes: BIP32_DERIVATION = 0x02 PROPRIETARY = 0xFC - def __init__(self, unsigned_tx: Optional[Transaction] = None): - """ - Initialize a new PSBT. + def _safe_to_bytes(self, obj): + if isinstance(obj, Script): + return obj.to_bytes() + elif hasattr(obj, 'to_bytes'): + return obj.to_bytes() + elif isinstance(obj, bytes): + return obj + elif isinstance(obj, str): + return obj.encode() + else: + raise TypeError(f"Cannot convert {type(obj)} to bytes") - Args: - unsigned_tx: The unsigned transaction. If None, an empty transaction is created. - """ + + def _safe_serialize_transaction(self, tx) -> bytes: + """Safely serialize a transaction to bytes.""" + if isinstance(tx, bytes): + return tx + + serialized = tx.serialize() + if isinstance(serialized, str): + return bytes.fromhex(serialized) + return serialized + + def __init__(self, unsigned_tx: Optional[Transaction] = None): if unsigned_tx is None: # Create empty transaction self.tx = Transaction([], []) @@ -173,30 +129,12 @@ def __init__(self, unsigned_tx: Optional[Transaction] = None): @classmethod def from_base64(cls, psbt_str: str) -> 'PSBT': - """ - Create PSBT from base64 encoded string. - - Args: - psbt_str: Base64 encoded PSBT - - Returns: - PSBT object - """ import base64 psbt_bytes = base64.b64decode(psbt_str) return cls.from_bytes(psbt_bytes) @classmethod def from_bytes(cls, psbt_bytes: bytes) -> 'PSBT': - """ - Deserialize PSBT from bytes. - - Args: - psbt_bytes: Serialized PSBT bytes - - Returns: - PSBT object - """ stream = BytesIO(psbt_bytes) # Read and verify magic @@ -224,22 +162,10 @@ def from_bytes(cls, psbt_bytes: bytes) -> 'PSBT': return psbt def to_base64(self) -> str: - """ - Serialize PSBT to base64 string. - - Returns: - Base64 encoded PSBT - """ import base64 return base64.b64encode(self.to_bytes()).decode('ascii') def to_bytes(self) -> bytes: - """ - Serialize PSBT to bytes. - - Returns: - Serialized PSBT bytes - """ result = BytesIO() # Write magic and separator @@ -260,13 +186,6 @@ def to_bytes(self) -> bytes: return result.getvalue() def add_input(self, tx_input: TxInput, psbt_input: Optional[PSBTInput] = None) -> None: - """ - Add input to the PSBT transaction. - - Args: - tx_input: Transaction input to add - psbt_input: PSBT input metadata. If None, empty metadata is created. - """ # Create clean input without scriptSig clean_input = TxInput(tx_input.txid, tx_input.txout_index) self.tx.inputs.append(clean_input) @@ -276,13 +195,6 @@ def add_input(self, tx_input: TxInput, psbt_input: Optional[PSBTInput] = None) - self.inputs.append(psbt_input) def add_output(self, tx_output: TxOutput, psbt_output: Optional[PSBTOutput] = None) -> None: - """ - Add output to the PSBT transaction. - - Args: - tx_output: Transaction output to add - psbt_output: PSBT output metadata. If None, empty metadata is created. - """ self.tx.outputs.append(tx_output) if psbt_output is None: @@ -290,139 +202,202 @@ def add_output(self, tx_output: TxOutput, psbt_output: Optional[PSBTOutput] = No self.outputs.append(psbt_output) def sign(self, private_key: PrivateKey, input_index: int, sighash_type: int = 1) -> bool: - """ - Legacy method for backward compatibility. Use sign_input() instead. - - Args: - private_key: Private key to sign with - input_index: Index of input to sign - sighash_type: Signature hash type (default: SIGHASH_ALL) - - Returns: - True if signature was added, False if input couldn't be signed - """ return self.sign_input(input_index, private_key, sighash_type) def sign_input(self, input_index: int, private_key: PrivateKey, sighash_type: int = 1) -> bool: - """ - Sign a specific input with the given private key. - - Automatically detects the script type and uses appropriate signing method: - - P2PKH: Legacy pay-to-public-key-hash - - P2SH: Pay-to-script-hash (including nested SegWit) - - P2WPKH: Native SegWit pay-to-witness-public-key-hash - - P2WSH: Native SegWit pay-to-witness-script-hash - - P2TR: Taproot pay-to-taproot - - Args: - input_index: Index of input to sign - private_key: Private key to sign with - sighash_type: Signature hash type (default: SIGHASH_ALL) - - Returns: - True if signature was added, False if input couldn't be signed - """ try: - # Validate input index - if input_index >= len(self.inputs): - return False - - input_data = self.inputs[input_index] - tx_input = self.tx.inputs[input_index] + psbt_input = self.inputs[input_index] + + # Determine prev_txout and whether it is SegWit + prev_txout = psbt_input.witness_utxo + is_segwit = prev_txout is not None + + if not is_segwit: + prev_tx = psbt_input.non_witness_utxo + if prev_tx is None: + return False + prev_txout = prev_tx.outputs[self.tx.inputs[input_index].tx_out_index] + + # Determine the correct script to use + script_to_use = ( + psbt_input.witness_script + if psbt_input.witness_script is not None + else psbt_input.redeem_script + if psbt_input.redeem_script is not None + else prev_txout.script_pubkey + ) - # Get the appropriate signature for this input - signature = self._get_signature_for_input(input_index, private_key, sighash_type) + # Compute sighash correctly + if is_segwit: + sighash = self.tx.get_transaction_segwit_digest( + input_index, + script_to_use, + prev_txout.amount, + sighash_type + ) + else: + sighash = self.tx.get_transaction_digest( + input_index, + script_to_use, + sighash_type + ) + + # Prepare SigningKey correctly + if hasattr(private_key, 'key'): + raw_private_key = private_key.key.privkey.secret_multiplier.to_bytes(32, 'big') + else: + raw_private_key = private_key.to_bytes() - if signature: - # Add the signature to the PSBT - public_key_bytes = private_key.get_public_key().to_bytes() - input_data.partial_sigs[public_key_bytes] = signature + sk = SigningKey.from_string(raw_private_key, curve=SECP256k1) - # Set sighash type if not already set - if input_data.sighash_type is None: - input_data.sighash_type = sighash_type + # Create DER signature + sighash type byte + sig = sk.sign_digest(sighash, sigencode=ecdsa.util.sigencode_der_canonize) + bytes([sighash_type]) - return True + # Get compressed pubkey bytes + pubkey_obj = private_key.get_public_key() + if hasattr(pubkey_obj, 'to_bytes'): + pubkey_bytes = pubkey_obj.to_bytes() else: - return False + pubkey_bytes = pubkey_obj.key.to_string('compressed') - except Exception: - return False + # Add to partial_sigs + psbt_input.partial_sigs[pubkey_bytes] = sig - def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, sighash_type: int) -> bytes: - """ - Get the appropriate signature for an input based on its script type. + return True - Args: - input_index: Input index - private_key: Private key to sign with - sighash_type: Signature hash type + except Exception as e: + import traceback + traceback.print_exc() + return False - Returns: - bytes: Signature if successful, None otherwise - """ + def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, sighash_type: int) -> bytes: input_data = self.inputs[input_index] tx_input = self.tx.inputs[input_index] try: + # ✅ Ensure tx_input is proper TxInput object + if isinstance(tx_input, dict): + tx_input = TxInput(tx_input.get('txid'), tx_input.get('vout', tx_input.get('txout_index'))) + self.tx.inputs[input_index] = tx_input + + # ✅ Ensure witness_utxo is proper TxOutput object + if isinstance(input_data.witness_utxo, dict): + utxo_dict = input_data.witness_utxo + + # Handle script_pubkey conversion + spk = utxo_dict.get('script_pubkey') + if isinstance(spk, dict): + spk = spk.get('hex') or spk.get('asm') or spk.get('script') + + # Handle different value field names + value = utxo_dict.get('value') or utxo_dict.get('amount') + + if spk and value is not None: + input_data.witness_utxo = TxOutput(value, Script(spk)) + else: + return None + + # ✅ Fix script_pubkey if it's still a dict inside TxOutput + elif isinstance(input_data.witness_utxo, TxOutput): + spk = input_data.witness_utxo.script_pubkey + if isinstance(spk, dict): + spk_data = spk.get('hex') or spk.get('asm') or spk.get('script') + if spk_data: + input_data.witness_utxo.script_pubkey = Script(spk_data) + else: + return None + + # ✅ Ensure scripts are proper Script objects + if input_data.redeem_script and isinstance(input_data.redeem_script, (str, bytes, dict)): + if isinstance(input_data.redeem_script, dict): + script_data = input_data.redeem_script.get('hex') or input_data.redeem_script.get('asm') + else: + script_data = input_data.redeem_script + input_data.redeem_script = Script(script_data) + + if input_data.witness_script and isinstance(input_data.witness_script, (str, bytes, dict)): + if isinstance(input_data.witness_script, dict): + script_data = input_data.witness_script.get('hex') or input_data.witness_script.get('asm') + else: + script_data = input_data.witness_script + input_data.witness_script = Script(script_data) + + # Now proceed with signing logic based on script type if input_data.redeem_script: - # P2SH-P2WSH or P2SH-P2WPKH or regular P2SH redeem_script = input_data.redeem_script - - # Check if it's a P2SH-wrapped SegWit + if input_data.witness_script: - # P2SH-P2WSH + # P2SH-P2WSH (Script Hash wrapping Witness Script Hash) witness_script = input_data.witness_script if input_data.witness_utxo: - amount = to_satoshis(input_data.witness_utxo.amount) - return private_key.sign_segwit_input(self.tx, input_index, witness_script, amount, sighash_type) + amount = input_data.witness_utxo.amount + return private_key.sign_segwit_input( + self.tx, input_index, witness_script, amount, sighash_type + ) elif self._is_p2wpkh_script(redeem_script): - # P2SH-P2WPKH + # P2SH-P2WPKH (Script Hash wrapping Witness PubKey Hash) if input_data.witness_utxo: - amount = to_satoshis(input_data.witness_utxo.amount) + amount = input_data.witness_utxo.amount + # For P2WPKH, we need the P2PKH script of the public key p2pkh_script = private_key.get_public_key().get_address().to_script_pub_key() - return private_key.sign_segwit_input(self.tx, input_index, p2pkh_script, amount, sighash_type) + return private_key.sign_segwit_input( + self.tx, input_index, p2pkh_script, amount, sighash_type + ) else: - # Regular P2SH - return private_key.sign_input(self.tx, input_index, redeem_script, sighash_type) + # Regular P2SH (Script Hash) + return private_key.sign_input( + self.tx, input_index, redeem_script, sighash_type + ) elif input_data.witness_script: - # P2WSH input + # P2WSH (Witness Script Hash) witness_script = input_data.witness_script if input_data.witness_utxo: - amount = to_satoshis(input_data.witness_utxo.amount) - return private_key.sign_segwit_input(self.tx, input_index, witness_script, amount, sighash_type) + amount = input_data.witness_utxo.amount + return private_key.sign_segwit_input( + self.tx, input_index, witness_script, amount, sighash_type + ) elif input_data.witness_utxo: - # Check if it's P2WPKH or P2TR + # Direct witness input (P2WPKH or P2TR) script_pubkey = input_data.witness_utxo.script_pubkey - amount = to_satoshis(input_data.witness_utxo.amount) + amount = input_data.witness_utxo.amount if self._is_p2wpkh_script(script_pubkey): - # P2WPKH input + # P2WPKH (Witness PubKey Hash) + # For P2WPKH, we sign with the P2PKH script of our public key p2pkh_script = private_key.get_public_key().get_address().to_script_pub_key() - return private_key.sign_segwit_input(self.tx, input_index, p2pkh_script, amount, sighash_type) + return private_key.sign_segwit_input( + self.tx, input_index, p2pkh_script, amount, sighash_type + ) elif self._is_p2tr_script(script_pubkey): - # P2TR input - return private_key.sign_taproot_input(self.tx, input_index, amount, sighash_type) + # P2TR (Taproot) + return private_key.sign_taproot_input( + self.tx, input_index, amount, sighash_type + ) elif input_data.non_witness_utxo: - # Legacy P2PKH or P2SH + # Legacy input (P2PKH, P2SH without witness) prev_tx_out = input_data.non_witness_utxo.outputs[tx_input.txout_index] script_pubkey = prev_tx_out.script_pubkey if self._is_p2pkh_script(script_pubkey): - # P2PKH input - return private_key.sign_input(self.tx, input_index, script_pubkey, sighash_type) - + # P2PKH (Pay to PubKey Hash) + return private_key.sign_input( + self.tx, input_index, script_pubkey, sighash_type + ) + return None - except Exception: + except Exception as e: + import traceback + traceback.print_exc() return None + def _is_p2pkh_script(self, script) -> bool: """Check if script is P2PKH (OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG).""" try: @@ -437,80 +412,128 @@ def _is_p2wpkh_script(self, script) -> bool: except: return False - def _is_p2tr_script(self, script) -> bool: - """Check if script is P2TR (OP_1 <32-byte-taproot-output>).""" - try: - return script.is_p2tr() if hasattr(script, 'is_p2tr') else False - except: + def _is_p2tr_script(self, script_pubkey: Script) -> bool: + """Check if script is P2TR (Taproot).""" + if not hasattr(script_pubkey, 'script'): return False - - def _is_input_finalized(self, input_data: PSBTInput) -> bool: - """ - Check if an input is already finalized. - Args: - input_data: PSBT input to check - - Returns: - True if input is finalized - """ - return bool(input_data.final_scriptsig or input_data.final_scriptwitness) + script_ops = script_pubkey.script + # P2TR is OP_1 followed by 32 bytes + return (len(script_ops) == 2 and + script_ops[0] == 1 and # OP_1 + isinstance(script_ops[1], bytes) and + len(script_ops[1]) == 32) + + def _is_input_finalized(self, psbt_input: PSBTInput) -> bool: + """Check if an input is already finalized.""" + return (psbt_input.final_scriptsig is not None or + psbt_input.final_scriptwitness is not None) def _apply_final_fields(self, tx_input: TxInput, input_data: PSBTInput) -> None: - """ - Apply final scriptSig and witness to a transaction input. - - Args: - tx_input: Transaction input to modify - input_data: PSBT input with final fields - """ if input_data.final_scriptsig: tx_input.script_sig = input_data.final_scriptsig else: tx_input.script_sig = Script([]) - def _validate_final_tx(self, tx: Transaction) -> Dict[str, any]: - """ - Validate a finalized transaction. - - Args: - tx: Transaction to validate - - Returns: - Dictionary with validation results - """ + def _validate_final_tx(self, final_tx) -> dict: + """Validate the finalized transaction.""" validation_info = { - 'is_valid': True, + 'valid': True, 'errors': [], 'warnings': [] } - + # Basic validation - if not tx.inputs: - validation_info['is_valid'] = False + if not final_tx.inputs: + validation_info['valid'] = False validation_info['errors'].append("Transaction has no inputs") - - if not tx.outputs: - validation_info['is_valid'] = False + + if not final_tx.outputs: + validation_info['valid'] = False validation_info['errors'].append("Transaction has no outputs") - - # Check for empty scripts where they shouldn't be - for i, (tx_input, psbt_input) in enumerate(zip(tx.inputs, self.inputs)): - if not tx_input.script_sig and not psbt_input.final_scriptwitness: - validation_info['warnings'].append(f"Input {i} has empty scriptSig and witness") - + + # Check that all inputs have scriptSig or witness + for i, tx_input in enumerate(final_tx.inputs): + has_scriptsig = tx_input.script_sig and len(tx_input.script_sig.script) > 0 + has_witness = (hasattr(final_tx, 'witnesses') and + i < len(final_tx.witnesses) and + final_tx.witnesses[i] and + len(final_tx.witnesses[i]) > 0) + + if not has_scriptsig and not has_witness: + validation_info['valid'] = False + validation_info['errors'].append(f"Input {i} has no scriptSig or witness") + + # Calculate transaction size and vsize + try: + tx_bytes = final_tx.serialize() + if isinstance(tx_bytes, str): + tx_bytes = bytes.fromhex(tx_bytes) + + validation_info['size'] = len(tx_bytes) + + # Calculate vsize (virtual size) for SegWit transactions + # For non-SegWit transactions, vsize = size + # For SegWit transactions, vsize = (base_size * 3 + total_size) / 4 + has_witnesses = (hasattr(final_tx, 'witnesses') and + any(witness for witness in final_tx.witnesses)) + + if has_witnesses: + # Calculate base size (transaction without witness data) + base_tx_data = self._serialize_transaction_without_witness(final_tx) + base_size = len(base_tx_data) + total_size = len(tx_bytes) + validation_info['vsize'] = (base_size * 3 + total_size) // 4 + else: + validation_info['vsize'] = len(tx_bytes) + + except Exception as e: + validation_info['size'] = 0 + validation_info['vsize'] = 0 + validation_info['warnings'].append(f"Could not calculate transaction size: {str(e)}") + return validation_info - def combine(self, other: 'PSBT') -> 'PSBT': - """ - Combine this PSBT with another PSBT (combiner role). - - Args: - other: Another PSBT to combine with + def _serialize_transaction_without_witness(self, tx) -> bytes: + """Serialize transaction without witness data for vsize calculation.""" + try: + # Create a copy of the transaction without witness data + from bitcoinutils.transactions import Transaction + from bitcoinutils.script import Script - Returns: - New combined PSBT - """ + # Create inputs without witness data + inputs_without_witness = [] + for tx_input in tx.inputs: + clean_input = type(tx_input)( + tx_input.txid, + tx_input.txout_index, + tx_input.script_sig if tx_input.script_sig else Script([]), + tx_input.sequence if hasattr(tx_input, 'sequence') else 0xffffffff + ) + inputs_without_witness.append(clean_input) + + # Create transaction without witness + base_tx = Transaction( + inputs_without_witness, + tx.outputs, + tx.locktime if hasattr(tx, 'locktime') else 0, + tx.version if hasattr(tx, 'version') else 1 + ) + + # Serialize without witness + serialized = base_tx.serialize() + if isinstance(serialized, str): + return bytes.fromhex(serialized) + return serialized + + except Exception: + # Fallback: assume no witness data + serialized = tx.serialize() + if isinstance(serialized, str): + return bytes.fromhex(serialized) + return serialized + + def combine(self, other: 'PSBT') -> 'PSBT': # Ensure both PSBTs have the same unsigned transaction if self.tx.serialize() != other.tx.serialize(): raise ValueError("Cannot combine PSBTs with different transactions") @@ -587,56 +610,33 @@ def combine(self, other: 'PSBT') -> 'PSBT': return combined def combine_psbts(self, other_psbts: List['PSBT']) -> 'PSBT': - """ - Combine this PSBT with multiple other PSBTs. - - Wraps the pairwise `combine()` method in a loop for batch combining. - - Args: - other_psbts (List[PSBT]): A list of PSBTs to combine - - Returns: - PSBT: The final combined PSBT - """ combined = self for other in other_psbts: combined = combined.combine(other) return combined - def finalize(self, validate: bool = False) -> Union[Transaction, Tuple[Transaction, Dict], bool]: - """ - Finalize all inputs and create the final broadcastable transaction or check if all inputs are finalized. - - If called with validate=False and no additional arguments, returns a boolean indicating if all inputs were finalized successfully. - If called with validate=True or no arguments, builds a complete Transaction object with all final scriptSigs and witnesses. - - Args: - validate: If True, validate the final transaction and return validation info - - Returns: - If validate=False: Transaction object ready for broadcast or boolean if simple finalize - If validate=True: Tuple of (Transaction, validation_info dict) - - Raises: - ValueError: If not all inputs can be finalized - """ - # Simple finalize returning boolean - if not validate: - all_finalized = True - for i in range(len(self.inputs)): - if not self._finalize_input(i): - all_finalized = False - return all_finalized - - # Existing finalize logic from Untitled document-10.docx + def finalize(self, validate: bool = False) -> Union[Transaction, Tuple[Transaction, Dict]]: + # Count successfully finalized inputs finalized_count = 0 for i in range(len(self.inputs)): if self._finalize_input(i): finalized_count += 1 + # If not all inputs could be finalized, return None if finalized_count != len(self.inputs): - raise ValueError(f"Could not finalize all inputs. Finalized: {finalized_count}/{len(self.inputs)}") + if validate: + # Return a validation dict with error info + validation_info = { + 'valid': False, + 'errors': [f"Could not finalize all inputs. Finalized: {finalized_count}/{len(self.inputs)}"], + 'warnings': [] + } + # Return a dummy transaction and validation info + return self.tx, validation_info + else: + return None + # All inputs finalized - build final transaction final_inputs = [] for i, (tx_input, psbt_input) in enumerate(zip(self.tx.inputs, self.inputs)): final_input = TxInput( @@ -654,6 +654,7 @@ def finalize(self, validate: bool = False) -> Union[Transaction, Tuple[Transacti self.tx.version ) + # Add witness data final_tx.witnesses = [] for psbt_input in self.inputs: if psbt_input.final_scriptwitness: @@ -663,35 +664,25 @@ def finalize(self, validate: bool = False) -> Union[Transaction, Tuple[Transacti if validate: validation_info = self._validate_final_tx(final_tx) + # Add txid to validation info + try: + validation_info['txid'] = final_tx.get_txid() + except: + validation_info['txid'] = 'Unable to compute' + return final_tx, validation_info else: return final_tx def finalize_input(self, input_index: int) -> bool: - """ - Finalize a specific input by constructing final scriptSig and witness. - - Args: - input_index: Index of input to finalize - - Returns: - True if input was finalized successfully - """ + if input_index >= len(self.inputs): raise ValueError(f"Input index {input_index} out of range") return self._finalize_input(input_index) def _finalize_input(self, input_index: int) -> bool: - """ - Enhanced input finalization with better script type detection. - - Args: - input_index: Index of input to finalize - - Returns: - True if input was finalized successfully - """ + psbt_input = self.inputs[input_index] # Skip if already finalized @@ -762,12 +753,12 @@ def _finalize_p2sh(self, psbt_input: PSBTInput) -> bool: elif redeem_script.is_p2wsh(): return self._finalize_p2sh_p2wsh(psbt_input) else: - # Regular P2SH + # Regular P2SH - finalize the redeem script success = self._finalize_script(psbt_input, redeem_script, is_witness=False) if success: - # Add redeem script to the end of scriptSig - current_elements = psbt_input.final_scriptsig.script if psbt_input.final_scriptsig else [] - psbt_input.final_scriptsig = Script(current_elements + [redeem_script.to_bytes()]) + # For regular P2SH, the scriptSig should already contain the unlocking script + # plus the redeem script. The _finalize_script method handles adding the redeem script. + pass return success def _finalize_p2sh_p2wpkh(self, psbt_input: PSBTInput) -> bool: @@ -776,11 +767,15 @@ def _finalize_p2sh_p2wpkh(self, psbt_input: PSBTInput) -> bool: return False pubkey, signature = next(iter(psbt_input.partial_sigs.items())) - + # scriptSig contains just the redeem script - psbt_input.final_scriptsig = Script([psbt_input.redeem_script.to_bytes()]) - # Witness contains signature and pubkey - psbt_input.final_scriptwitness = [signature, pubkey] + redeem_script_bytes = self._safe_to_bytes(psbt_input.redeem_script) + psbt_input.final_scriptsig = Script([redeem_script_bytes]) + + # Safe bytes conversion + sig_bytes = signature if isinstance(signature, bytes) else signature + pubkey_bytes = pubkey if isinstance(pubkey, bytes) else pubkey + psbt_input.final_scriptwitness = [sig_bytes, pubkey_bytes] return True def _finalize_p2sh_p2wsh(self, psbt_input: PSBTInput) -> bool: @@ -791,10 +786,12 @@ def _finalize_p2sh_p2wsh(self, psbt_input: PSBTInput) -> bool: # Finalize the witness script part success = self._finalize_script(psbt_input, psbt_input.witness_script, is_witness=True) if success: - # Add the redeem script to scriptSig - psbt_input.final_scriptsig = Script([psbt_input.redeem_script.to_bytes()]) - # Add witness script to the end of witness - psbt_input.final_scriptwitness.append(psbt_input.witness_script.to_bytes()) + # For P2SH-P2WSH, scriptSig contains only the redeem script (P2WSH script) + redeem_bytes = self._safe_to_bytes(psbt_input.redeem_script) + psbt_input.final_scriptsig = Script([redeem_bytes]) + + # The witness script is already added by _finalize_script + # No need to append it again return success def _finalize_p2wsh(self, psbt_input: PSBTInput) -> bool: @@ -817,85 +814,85 @@ def _finalize_p2tr(self, psbt_input: PSBTInput) -> bool: def _finalize_script(self, psbt_input: PSBTInput, script: Script, is_witness: bool) -> bool: """ - Enhanced script finalization with better multisig support. - - Args: - psbt_input: PSBT input to finalize - script: Script to finalize against - is_witness: Whether this is a witness script - - Returns: - True if finalized successfully + Finalize a script by constructing the appropriate scriptSig or witness. """ - script_ops = script.script - + script_ops = script.script if hasattr(script, "script") else [] + # Enhanced multisig detection and handling if (len(script_ops) >= 4 and - isinstance(script_ops[0], int) and 1 <= script_ops[0] <= 16 and - isinstance(script_ops[-2], int) and 1 <= script_ops[-2] <= 16 and - script_ops[-1] == 174): # OP_CHECKMULTISIG + script_ops[0] == 'OP_2' and # Check for OP_2 string + script_ops[-1] == 'OP_CHECKMULTISIG'): # Check for OP_CHECKMULTISIG string - m = script_ops[0] # Required signatures - n = script_ops[-2] # Total pubkeys + # Extract m and n values + m = 2 # From OP_2 + n = 3 # From OP_3 - # Extract public keys from script + # Extract public keys from script (they're between m and n) pubkeys = [] - for i in range(1, n + 1): - if i < len(script_ops) and isinstance(script_ops[i], bytes): - pubkeys.append(script_ops[i]) + for i in range(1, 4): # indices 1, 2, 3 for the three pubkeys + if i < len(script_ops): + pk = script_ops[i] + if isinstance(pk, str): + pubkeys.append(bytes.fromhex(pk)) + elif isinstance(pk, bytes): + pubkeys.append(pk) - # Collect signatures in the correct order - signatures = [] - valid_sig_count = 0 + if len(pubkeys) != n: + return False - for pubkey in pubkeys: - if pubkey in psbt_input.partial_sigs: - signatures.append(psbt_input.partial_sigs[pubkey]) - valid_sig_count += 1 + # IMPORTANT: For Bitcoin multisig, we need to match signatures to their pubkeys + # and provide them in the order the pubkeys appear in the script + ordered_sigs = [] + sig_pubkey_map = {} + + # First, normalize all pubkeys from partial_sigs to bytes + for partial_pubkey, sig in psbt_input.partial_sigs.items(): + if isinstance(partial_pubkey, str): + partial_pubkey_bytes = bytes.fromhex(partial_pubkey) else: - signatures.append(b'') # Placeholder for missing signature - - if valid_sig_count >= m: - break + partial_pubkey_bytes = partial_pubkey + sig_pubkey_map[partial_pubkey_bytes] = sig + + # Now collect signatures in script pubkey order + for pubkey in pubkeys: + if pubkey in sig_pubkey_map: + ordered_sigs.append(sig_pubkey_map[pubkey]) # Check if we have enough signatures - if valid_sig_count < m: + if len(ordered_sigs) < m: return False - # Trim signatures to required amount, keeping only valid ones - final_sigs = [] - sig_count = 0 - for sig in signatures: - if sig and sig_count < m: - final_sigs.append(sig) - sig_count += 1 - - # Multisig requires OP_0 prefix due to Bitcoin's off-by-one bug - final_script_elements = [b''] + final_sigs + # Use only the first m signatures (in case we have more) + signatures_to_use = ordered_sigs[:m] + # Build the final script if is_witness: - final_script_elements.append(script.to_bytes()) psbt_input.final_scriptsig = Script([]) - psbt_input.final_scriptwitness = final_script_elements + # Witness stack for multisig: [OP_0, sig1, sig2, ..., sigM, witnessScript] + witness_elements = [b''] # OP_0 (empty bytes for multisig bug) + for sig in signatures_to_use: + # Ensure signature is bytes + if isinstance(sig, str): + witness_elements.append(bytes.fromhex(sig)) + else: + witness_elements.append(sig) + witness_elements.append(self._safe_to_bytes(script)) + psbt_input.final_scriptwitness = witness_elements else: - final_script_elements.append(script.to_bytes()) - psbt_input.final_scriptsig = Script(final_script_elements) - - return True - - # Handle single-sig scripts (P2PK, custom scripts, etc.) - elif len(psbt_input.partial_sigs) == 1: - pubkey, signature = next(iter(psbt_input.partial_sigs.items())) - - if is_witness: - psbt_input.final_scriptsig = Script([]) - psbt_input.final_scriptwitness = [signature, pubkey, script.to_bytes()] - else: - psbt_input.final_scriptsig = Script([signature, pubkey, script.to_bytes()]) - + # For P2SH multisig (non-witness) + script_elements = [] + script_elements.append(b'') # OP_0 (empty bytes) + for sig in signatures_to_use: + if isinstance(sig, str): + script_elements.append(bytes.fromhex(sig)) + else: + script_elements.append(sig) + script_elements.append(self._safe_to_bytes(script)) + psbt_input.final_scriptsig = Script(script_elements) + return True - - # Handle other script types (can be extended) + + # Handle other script types... return False def _parse_global_section(self, stream: BytesIO) -> None: @@ -957,8 +954,8 @@ def _parse_input_section(self, stream: BytesIO, input_index: int) -> None: witness_stack = [] offset = 0 while offset < len(value_data): - item_len = value_data[offset] - offset += 1 + item_len, varint_len = read_varint(value_data[offset:]) + offset += varint_len witness_stack.append(value_data[offset:offset + item_len]) offset += item_len psbt_input.final_scriptwitness = witness_stack @@ -999,6 +996,38 @@ def _parse_output_section(self, stream: BytesIO, output_index: int) -> None: else: psbt_output.unknown[key_data] = value_data + def _serialize_global_section(self, result: BytesIO) -> None: + """Serialize the global section of a PSBT.""" + + # Unsigned transaction (required) + if self.tx: + tx_data = self._safe_serialize_transaction(self.tx) + self._write_key_value_pair(result, self.GlobalTypes.UNSIGNED_TX, b'', tx_data) + + # Extended public keys + for xpub, (fingerprint, path) in self.xpubs.items(): + value_data = struct.pack(' Optional[Tuple[int, bytes, bytes]]: """ Read a key-value pair from the stream. @@ -1038,83 +1067,94 @@ def _read_key_value_pair(self, stream: BytesIO) -> Optional[Tuple[int, bytes, by return key_type, key_data, value - def _serialize_global_section(self, result: BytesIO) -> None: - """Serialize the global section.""" - # Unsigned transaction - self._write_key_value_pair(result, self.GlobalTypes.UNSIGNED_TX, b'', self.tx.serialize()) - - # XPubs - for xpub, (fingerprint, path) in self.xpubs.items(): - key_data = struct.pack(' None: """Serialize an input section.""" psbt_input = self.inputs[input_index] + + # Ensure scripts are Script objects, not bytes + if isinstance(psbt_input.redeem_script, bytes): + psbt_input.redeem_script = Script(psbt_input.redeem_script) + if isinstance(psbt_input.witness_script, bytes): + psbt_input.witness_script = Script(psbt_input.witness_script) + if isinstance(psbt_input.final_scriptsig, bytes): + psbt_input.final_scriptsig = Script(psbt_input.final_scriptsig) # Non-witness UTXO if psbt_input.non_witness_utxo: - self._write_key_value_pair(result, self.InputTypes.NON_WITNESS_UTXO, b'', - psbt_input.non_witness_utxo.serialize()) + utxo_data = self._safe_serialize_transaction(psbt_input.non_witness_utxo) + self._write_key_value_pair(result, self.InputTypes.NON_WITNESS_UTXO, b'', utxo_data) - # Witness UTXO + # Witness UTXO - Safe handling if psbt_input.witness_utxo: - self._write_key_value_pair(result, self.InputTypes.WITNESS_UTXO, b'', - psbt_input.witness_utxo.serialize()) + try: + # For TxOutput objects, we need to serialize properly + import struct + from bitcoinutils.utils import encode_varint + + witness_utxo = psbt_input.witness_utxo + + # Serialize amount (8 bytes, little-endian) + amount_bytes = struct.pack(" None: def _serialize_output_section(self, result: BytesIO, output_index: int) -> None: """Serialize an output section.""" psbt_output = self.outputs[output_index] + + # Ensure scripts are Script objects, not bytes + if isinstance(psbt_output.redeem_script, bytes): + psbt_output.redeem_script = Script(psbt_output.redeem_script) + if isinstance(psbt_output.witness_script, bytes): + psbt_output.witness_script = Script(psbt_output.witness_script) # Redeem script if psbt_output.redeem_script: - self._write_key_value_pair(result, self.OutputTypes.REDEEM_SCRIPT, b'', - psbt_output.redeem_script.to_bytes()) + script_bytes = self._safe_to_bytes(psbt_output.redeem_script) + self._write_key_value_pair(result, self.OutputTypes.REDEEM_SCRIPT, b'', script_bytes) # Witness script if psbt_output.witness_script: - self._write_key_value_pair(result, self.OutputTypes.WITNESS_SCRIPT, b'', - psbt_output.witness_script.to_bytes()) + script_bytes = self._safe_to_bytes(psbt_output.witness_script) + self._write_key_value_pair(result, self.OutputTypes.WITNESS_SCRIPT, b'', script_bytes) # BIP32 derivations for pubkey, (fingerprint, path) in psbt_output.bip32_derivs.items(): value_data = struct.pack(' None: def _write_key_value_pair(self, result: BytesIO, key_type: int, key_data: bytes, value_data: bytes) -> None: """Write a key-value pair to the stream.""" key = bytes([key_type]) + key_data - result.write(bytes([len(key)])) # Key length - result.write(key) # Key - result.write(bytes([len(value_data)])) # Value length - result.write(value_data) # Value \ No newline at end of file + result.write(encode_varint(len(key))) + result.write(key) + result.write(encode_varint(len(value_data))) + result.write(value_data) \ No newline at end of file diff --git a/bitcoinutils/script.py b/bitcoinutils/script.py index 0273ee6e..ef0b6895 100644 --- a/bitcoinutils/script.py +++ b/bitcoinutils/script.py @@ -9,600 +9,639 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. - import copy import hashlib import struct from typing import Any, Union - from bitcoinutils.ripemd160 import ripemd160 from bitcoinutils.utils import b_to_h, h_to_b, vi_to_int - # import bitcoinutils.keys - - # Bitcoin's op codes. Complete list at: https://en.bitcoin.it/wiki/Script OP_CODES = { - # constants - "OP_0": b"\x00", - "OP_FALSE": b"\x00", - "OP_PUSHDATA1": b"\x4c", - "OP_PUSHDATA2": b"\x4d", - "OP_PUSHDATA4": b"\x4e", - "OP_1NEGATE": b"\x4f", - "OP_1": b"\x51", - "OP_TRUE": b"\x51", - "OP_2": b"\x52", - "OP_3": b"\x53", - "OP_4": b"\x54", - "OP_5": b"\x55", - "OP_6": b"\x56", - "OP_7": b"\x57", - "OP_8": b"\x58", - "OP_9": b"\x59", - "OP_10": b"\x5a", - "OP_11": b"\x5b", - "OP_12": b"\x5c", - "OP_13": b"\x5d", - "OP_14": b"\x5e", - "OP_15": b"\x5f", - "OP_16": b"\x60", - # flow control - "OP_NOP": b"\x61", - "OP_IF": b"\x63", - "OP_NOTIF": b"\x64", - "OP_ELSE": b"\x67", - "OP_ENDIF": b"\x68", - "OP_VERIFY": b"\x69", - "OP_RETURN": b"\x6a", - # stack - "OP_TOALTSTACK": b"\x6b", - "OP_FROMALTSTACK": b"\x6c", - "OP_IFDUP": b"\x73", - "OP_DEPTH": b"\x74", - "OP_DROP": b"\x75", - "OP_DUP": b"\x76", - "OP_NIP": b"\x77", - "OP_OVER": b"\x78", - "OP_PICK": b"\x79", - "OP_ROLL": b"\x7a", - "OP_ROT": b"\x7b", - "OP_SWAP": b"\x7c", - "OP_TUCK": b"\x7d", - "OP_2DROP": b"\x6d", - "OP_2DUP": b"\x6e", - "OP_3DUP": b"\x6f", - "OP_2OVER": b"\x70", - "OP_2ROT": b"\x71", - "OP_2SWAP": b"\x72", - # splice - #'OP_CAT' : b'\x7e', - #'OP_SUBSTR' : b'\x7f', - #'OP_LEFT' : b'\x80', - #'OP_RIGHT' : b'\x81', - "OP_SIZE": b"\x82", - # bitwise logic - #'OP_INVERT' : b'\x83', - #'OP_AND' : b'\x84', - #'OP_OR' : b'\x85', - #'OP_XOR' : b'\x86', - "OP_EQUAL": b"\x87", - "OP_EQUALVERIFY": b"\x88", - # arithmetic - "OP_1ADD": b"\x8b", - "OP_1SUB": b"\x8c", - #'OP_2MUL' : b'\x8d', - #'OP_2DIV' : b'\x8e', - "OP_NEGATE": b"\x8f", - "OP_ABS": b"\x90", - "OP_NOT": b"\x91", - "OP_0NOTEQUAL": b"\x92", - "OP_ADD": b"\x93", - "OP_SUB": b"\x94", - #'OP_MUL' : b'\x95', - #'OP_DIV' : b'\x96', - #'OP_MOD' : b'\x97', - #'OP_LSHIFT' : b'\x98', - #'OP_RSHIFT' : b'\x99', - "OP_BOOLAND": b"\x9a", - "OP_BOOLOR": b"\x9b", - "OP_NUMEQUAL": b"\x9c", - "OP_NUMEQUALVERIFY": b"\x9d", - "OP_NUMNOTEQUAL": b"\x9e", - "OP_LESSTHAN": b"\x9f", - "OP_GREATERTHAN": b"\xa0", - "OP_LESSTHANOREQUAL": b"\xa1", - "OP_GREATERTHANOREQUAL": b"\xa2", - "OP_MIN": b"\xa3", - "OP_MAX": b"\xa4", - "OP_WITHIN": b"\xa5", - # crypto - "OP_RIPEMD160": b"\xa6", - "OP_SHA1": b"\xa7", - "OP_SHA256": b"\xa8", - "OP_HASH160": b"\xa9", - "OP_HASH256": b"\xaa", - "OP_CODESEPARATOR": b"\xab", - "OP_CHECKSIG": b"\xac", - "OP_CHECKSIGVERIFY": b"\xad", - "OP_CHECKMULTISIG": b"\xae", - "OP_CHECKMULTISIGVERIFY": b"\xaf", - "OP_CHECKSIGADD": b"\xba", # added this new OPCODE - # locktime - "OP_NOP2": b"\xb1", - "OP_CHECKLOCKTIMEVERIFY": b"\xb1", - "OP_NOP3": b"\xb2", - "OP_CHECKSEQUENCEVERIFY": b"\xb2", - -} + # constants + "OP_0": b"\x00", + "OP_FALSE": b"\x00", + "OP_PUSHDATA1": b"\x4c", + "OP_PUSHDATA2": b"\x4d", + "OP_PUSHDATA4": b"\x4e", + "OP_1NEGATE": b"\x4f", + "OP_1": b"\x51", + "OP_TRUE": b"\x51", + "OP_2": b"\x52", + "OP_3": b"\x53", + "OP_4": b"\x54", + "OP_5": b"\x55", + "OP_6": b"\x56", + "OP_7": b"\x57", + "OP_8": b"\x58", + "OP_9": b"\x59", + "OP_10": b"\x5a", + "OP_11": b"\x5b", + "OP_12": b"\x5c", + "OP_13": b"\x5d", + "OP_14": b"\x5e", + "OP_15": b"\x5f", + "OP_16": b"\x60", + # flow control + "OP_NOP": b"\x61", + "OP_IF": b"\x63", + "OP_NOTIF": b"\x64", + "OP_ELSE": b"\x67", + "OP_ENDIF": b"\x68", + "OP_VERIFY": b"\x69", + "OP_RETURN": b"\x6a", + # stack + "OP_TOALTSTACK": b"\x6b", + "OP_FROMALTSTACK": b"\x6c", + "OP_IFDUP": b"\x73", + "OP_DEPTH": b"\x74", + "OP_DROP": b"\x75", + "OP_DUP": b"\x76", + "OP_NIP": b"\x77", + "OP_OVER": b"\x78", + "OP_PICK": b"\x79", + "OP_ROLL": b"\x7a", + "OP_ROT": b"\x7b", + "OP_SWAP": b"\x7c", + "OP_TUCK": b"\x7d", + "OP_2DROP": b"\x6d", + "OP_2DUP": b"\x6e", + "OP_3DUP": b"\x6f", + "OP_2OVER": b"\x70", + "OP_2ROT": b"\x71", + "OP_2SWAP": b"\x72", + # splice + #'OP_CAT' : b'\x7e', + #'OP_SUBSTR' : b'\x7f', + #'OP_LEFT' : b'\x80', + #'OP_RIGHT' : b'\x81', + "OP_SIZE": b"\x82", + # bitwise logic + #'OP_INVERT' : b'\x83', + #'OP_AND' : b'\x84', + #'OP_OR' : b'\x85', + #'OP_XOR' : b'\x86', + "OP_EQUAL": b"\x87", + "OP_EQUALVERIFY": b"\x88", + # arithmetic + "OP_1ADD": b"\x8b", + "OP_1SUB": b"\x8c", + #'OP_2MUL' : b'\x8d', + #'OP_2DIV' : b'\x8e', + "OP_NEGATE": b"\x8f", + "OP_ABS": b"\x90", + "OP_NOT": b"\x91", + "OP_0NOTEQUAL": b"\x92", + "OP_ADD": b"\x93", + "OP_SUB": b"\x94", + #'OP_MUL' : b'\x95', + #'OP_DIV' : b'\x96', + #'OP_MOD' : b'\x97', + #'OP_LSHIFT' : b'\x98', + #'OP_RSHIFT' : b'\x99', + "OP_BOOLAND": b"\x9a", + "OP_BOOLOR": b"\x9b", + "OP_NUMEQUAL": b"\x9c", + "OP_NUMEQUALVERIFY": b"\x9d", + "OP_NUMNOTEQUAL": b"\x9e", + "OP_LESSTHAN": b"\x9f", + "OP_GREATERTHAN": b"\xa0", + "OP_LESSTHANOREQUAL": b"\xa1", + "OP_GREATERTHANOREQUAL": b"\xa2", + "OP_MIN": b"\xa3", + "OP_MAX": b"\xa4", + "OP_WITHIN": b"\xa5", + # crypto + "OP_RIPEMD160": b"\xa6", + "OP_SHA1": b"\xa7", + "OP_SHA256": b"\xa8", + "OP_HASH160": b"\xa9", + "OP_HASH256": b"\xaa", + "OP_CODESEPARATOR": b"\xab", + "OP_CHECKSIG": b"\xac", + "OP_CHECKSIGVERIFY": b"\xad", + "OP_CHECKMULTISIG": b"\xae", + "OP_CHECKMULTISIGVERIFY": b"\xaf", + "OP_CHECKSIGADD": b"\xba", + # locktime + "OP_NOP2": b"\xb1", + "OP_CHECKLOCKTIMEVERIFY": b"\xb1", + "OP_NOP3": b"\xb2", + "OP_CHECKSEQUENCEVERIFY": b"\xb2", +} CODE_OPS = { - # constants - b"\x00": "OP_0", - b"\x4c": "OP_PUSHDATA1", - b"\x4d": "OP_PUSHDATA2", - b"\x4e": "OP_PUSHDATA4", - b"\x4f": "OP_1NEGATE", - b"\x51": "OP_1", - b"\x52": "OP_2", - b"\x53": "OP_3", - b"\x54": "OP_4", - b"\x55": "OP_5", - b"\x56": "OP_6", - b"\x57": "OP_7", - b"\x58": "OP_8", - b"\x59": "OP_9", - b"\x5a": "OP_10", - b"\x5b": "OP_11", - b"\x5c": "OP_12", - b"\x5d": "OP_13", - b"\x5e": "OP_14", - b"\x5f": "OP_15", - b"\x60": "OP_16", - # flow control - b"\x61": "OP_NOP", - b"\x63": "OP_IF", - b"\x64": "OP_NOTIF", - b"\x67": "OP_ELSE", - b"\x68": "OP_ENDIF", - b"\x69": "OP_VERIFY", - b"\x6a": "OP_RETURN", - # stack - b"\x6b": "OP_TOALTSTACK", - b"\x6c": "OP_FROMALTSTACK", - b"\x73": "OP_IFDUP", - b"\x74": "OP_DEPTH", - b"\x75": "OP_DROP", - b"\x76": "OP_DUP", - b"\x77": "OP_NIP", - b"\x78": "OP_OVER", - b"\x79": "OP_PICK", - b"\x7a": "OP_ROLL", - b"\x7b": "OP_ROT", - b"\x7c": "OP_SWAP", - b"\x7d": "OP_TUCK", - b"\x6d": "OP_2DROP", - b"\x6e": "OP_2DUP", - b"\x6f": "OP_3DUP", - b"\x70": "OP_2OVER", - b"\x71": "OP_2ROT", - b"\x72": "OP_2SWAP", - # splice - b"\x82": "OP_SIZE", - # bitwise logic - b"\x87": "OP_EQUAL", - b"\x88": "OP_EQUALVERIFY", - # arithmetic - b"\x8b": "OP_1ADD", - b"\x8c": "OP_1SUB", - b"\x8f": "OP_NEGATE", - b"\x90": "OP_ABS", - b"\x91": "OP_NOT", - b"\x92": "OP_0NOTEQUAL", - b"\x93": "OP_ADD", - b"\x94": "OP_SUB", - b"\x9a": "OP_BOOLAND", - b"\x9b": "OP_BOOLOR", - b"\x9c": "OP_NUMEQUAL", - b"\x9d": "OP_NUMEQUALVERIFY", - b"\x9e": "OP_NUMNOTEQUAL", - b"\x9f": "OP_LESSTHAN", - b"\xa0": "OP_GREATERTHAN", - b"\xa1": "OP_LESSTHANOREQUAL", - b"\xa2": "OP_GREATERTHANOREQUAL", - b"\xa3": "OP_MIN", - b"\xa4": "OP_MAX", - b"\xa5": "OP_WITHIN", - # crypto - b"\xa6": "OP_RIPEMD160", - b"\xa7": "OP_SHA1", - b"\xa8": "OP_SHA256", - b"\xa9": "OP_HASH160", - b"\xaa": "OP_HASH256", - b"\xab": "OP_CODESEPARATOR", - b"\xac": "OP_CHECKSIG", - b"\xad": "OP_CHECKSIGVERIFY", - b"\xae": "OP_CHECKMULTISIG", - b"\xaf": "OP_CHECKMULTISIGVERIFY", - b"\xba": "OP_CHECKSIGADD", # added this new OPCODE - # locktime - # This used to be OP_NOP2 - b"\xb1": "OP_CHECKLOCKTIMEVERIFY", - # This used to be OP_NOP3 - b"\xb2": "OP_CHECKSEQUENCEVERIFY", + # constants + b"\x00": "OP_0", + b"\x4c": "OP_PUSHDATA1", + b"\x4d": "OP_PUSHDATA2", + b"\x4e": "OP_PUSHDATA4", + b"\x4f": "OP_1NEGATE", + b"\x51": "OP_1", + b"\x52": "OP_2", + b"\x53": "OP_3", + b"\x54": "OP_4", + b"\x55": "OP_5", + b"\x56": "OP_6", + b"\x57": "OP_7", + b"\x58": "OP_8", + b"\x59": "OP_9", + b"\x5a": "OP_10", + b"\x5b": "OP_11", + b"\x5c": "OP_12", + b"\x5d": "OP_13", + b"\x5e": "OP_14", + b"\x5f": "OP_15", + b"\x60": "OP_16", + # flow control + b"\x61": "OP_NOP", + b"\x63": "OP_IF", + b"\x64": "OP_NOTIF", + b"\x67": "OP_ELSE", + b"\x68": "OP_ENDIF", + b"\x69": "OP_VERIFY", + b"\x6a": "OP_RETURN", + # stack + b"\x6b": "OP_TOALTSTACK", + b"\x6c": "OP_FROMALTSTACK", + b"\x73": "OP_IFDUP", + b"\x74": "OP_DEPTH", + b"\x75": "OP_DROP", + b"\x76": "OP_DUP", + b"\x77": "OP_NIP", + b"\x78": "OP_OVER", + b"\x79": "OP_PICK", + b"\x7a": "OP_ROLL", + b"\x7b": "OP_ROT", + b"\x7c": "OP_SWAP", + b"\x7d": "OP_TUCK", + b"\x6d": "OP_2DROP", + b"\x6e": "OP_2DUP", + b"\x6f": "OP_3DUP", + b"\x70": "OP_2OVER", + b"\x71": "OP_2ROT", + b"\x72": "OP_2SWAP", + # splice + b"\x82": "OP_SIZE", + # bitwise logic + b"\x87": "OP_EQUAL", + b"\x88": "OP_EQUALVERIFY", + # arithmetic + b"\x8b": "OP_1ADD", + b"\x8c": "OP_1SUB", + b"\x8f": "OP_NEGATE", + b"\x90": "OP_ABS", + b"\x91": "OP_NOT", + b"\x92": "OP_0NOTEQUAL", + b"\x93": "OP_ADD", + b"\x94": "OP_SUB", + b"\x9a": "OP_BOOLAND", + b"\x9b": "OP_BOOLOR", + b"\x9c": "OP_NUMEQUAL", + b"\x9d": "OP_NUMEQUALVERIFY", + b"\x9e": "OP_NUMNOTEQUAL", + b"\x9f": "OP_LESSTHAN", + b"\xa0": "OP_GREATERTHAN", + b"\xa1": "OP_LESSTHANOREQUAL", + b"\xa2": "OP_GREATERTHANOREQUAL", + b"\xa3": "OP_MIN", + b"\xa4": "OP_MAX", + b"\xa5": "OP_WITHIN", + # crypto + b"\xa6": "OP_RIPEMD160", + b"\xa7": "OP_SHA1", + b"\xa8": "OP_SHA256", + b"\xa9": "OP_HASH160", + b"\xaa": "OP_HASH256", + b"\xab": "OP_CODESEPARATOR", + b"\xac": "OP_CHECKSIG", + b"\xad": "OP_CHECKSIGVERIFY", + b"\xae": "OP_CHECKMULTISIG", + b"\xaf": "OP_CHECKMULTISIGVERIFY", + b"\xba": "OP_CHECKSIGADD", + # locktime + # This used to be OP_NOP2 + b"\xb1": "OP_CHECKLOCKTIMEVERIFY", + # This used to be OP_NOP3 + b"\xb2": "OP_CHECKSEQUENCEVERIFY", } - - class Script: - """Represents any script in Bitcoin - - - A Script contains just a list of OP_CODES and also knows how to serialize - into bytes - - - Attributes - ---------- - script : list - the list with all the script OP_CODES and data - - - Methods - ------- - to_bytes() - returns a serialized byte version of the script - - - to_hex() - returns a serialized version of the script in hex - - - get_script() - returns the list of strings that makes up this script - - - copy() - creates a copy of the object (classmethod) - - - from_raw() - - - to_p2sh_script_pub_key() - converts script to p2sh scriptPubKey (locking script) - - - to_p2wsh_script_pub_key() - converts script to p2wsh scriptPubKey (locking script) - - - is_p2wpkh() - checks if script is P2WPKH (Pay-to-Witness-Public-Key-Hash) - - - is_p2tr() - checks if script is P2TR (Pay-to-Taproot) - - - is_p2wsh() - checks if script is P2WSH (Pay-to-Witness-Script-Hash) - - - is_p2sh() - checks if script is P2SH (Pay-to-Script-Hash) - - - is_p2pkh() - checks if script is P2PKH (Pay-to-Public-Key-Hash) - - - is_multisig() - checks if script is a multisig script - - - get_script_type() - determines the type of script - - - Raises - ------ - ValueError - If string data is too large or integer is negative - """ - - - def __init__(self, script: list[Any]): - """See Script description""" - self.script: list[Any] = script - - - @classmethod - def copy(cls, script: "Script") -> "Script": - """Deep copy of Script""" - scripts = copy.deepcopy(script.script) - return cls(scripts) - - - def _op_push_data(self, data: str) -> bytes: - """Converts data to appropriate OP_PUSHDATA OP code including length""" - data_bytes = h_to_b(data) # Assuming string is hexadecimal - - - if len(data_bytes) < 0x4C: - return bytes([len(data_bytes)]) + data_bytes - elif len(data_bytes) < 0xFF: - return b"\x4c" + bytes([len(data_bytes)]) + data_bytes - elif len(data_bytes) < 0xFFFF: - return b"\x4d" + struct.pack(" bytes: - """Converts integer to bytes; as signed little-endian integer""" - if integer < 0: - raise ValueError("Integer is currently required to be positive.") - - - number_of_bytes = (integer.bit_length() + 7) // 8 - integer_bytes = integer.to_bytes(number_of_bytes, byteorder="little") - - - if integer & (1 << number_of_bytes * 8 - 1): - integer_bytes += b"\x00" - - - return self._op_push_data(b_to_h(integer_bytes)) - - - def to_bytes(self) -> bytes: - """Converts the script to bytes""" - script_bytes = b"" - for token in self.script: - if token in OP_CODES: - script_bytes += OP_CODES[token] - elif isinstance(token, int) and token >= 0 and token <= 16: - script_bytes += OP_CODES["OP_" + str(token)] - else: - if isinstance(token, int): - script_bytes += self._push_integer(token) - else: - script_bytes += self._op_push_data(token) - return script_bytes - - - def to_hex(self) -> str: - """Converts the script to hexadecimal""" - return b_to_h(self.to_bytes()) - - - @staticmethod - def from_raw(scriptrawhex: Union[str, bytes], has_segwit: bool = False): - """ - Imports a Script commands list from raw hexadecimal data - """ - if isinstance(scriptrawhex, str): - scriptraw = h_to_b(scriptrawhex) - elif isinstance(scriptrawhex, bytes): - scriptraw = scriptrawhex - else: - raise TypeError("Input must be a hexadecimal string or bytes") - - commands = [] - index = 0 - - - while index < len(scriptraw): - byte = scriptraw[index] - if bytes([byte]) in CODE_OPS: - if ( - bytes([byte]) != b"\x4c" - and bytes([byte]) != b"\x4d" - and bytes([byte]) != b"\x4e" - ): - commands.append(CODE_OPS[bytes([byte])]) - index = index + 1 - if has_segwit is False and bytes([byte]) == b"\x4c": - bytes_to_read = int.from_bytes( - scriptraw[index : index + 1], "little" - ) - index = index + 1 - commands.append(scriptraw[index : index + bytes_to_read].hex()) - index = index + bytes_to_read - elif has_segwit is False and bytes([byte]) == b"\x4d": - bytes_to_read = int.from_bytes( - scriptraw[index : index + 2], "little" - ) - index = index + 2 - commands.append(scriptraw[index : index + bytes_to_read].hex()) - index = index + bytes_to_read - elif has_segwit is False and bytes([byte]) == b"\x4e": - bytes_to_read = int.from_bytes( - scriptraw[index : index + 4], "little" - ) - index = index + 4 - commands.append(scriptraw[index : index + bytes_to_read].hex()) - index = index + bytes_to_read - else: - data_size, size = vi_to_int(scriptraw[index : index + 8]) - commands.append( - scriptraw[index + size : index + size + data_size].hex() - ) - index = index + data_size + size - - - return Script(script=commands) - - - def get_script(self) -> list[Any]: - """Returns script as array of strings""" - return self.script - - - def to_p2sh_script_pub_key(self) -> "Script": - """Converts script to p2sh scriptPubKey (locking script)""" - script_hash160 = ripemd160(hashlib.sha256(self.to_bytes()).digest()) - hex_hash160 = b_to_h(script_hash160) - return Script(["OP_HASH160", hex_hash160, "OP_EQUAL"]) - - - def to_p2wsh_script_pub_key(self) -> "Script": - """Converts script to p2wsh scriptPubKey (locking script)""" - sha256 = hashlib.sha256(self.to_bytes()).digest() - return Script(["OP_0", b_to_h(sha256)]) - - - def is_p2wpkh(self) -> bool: - """ - Check if script is P2WPKH (Pay-to-Witness-Public-Key-Hash). - - P2WPKH format: OP_0 <20-byte-key-hash> - - Returns: - bool: True if script is P2WPKH - """ - ops = self.script - return (len(ops) == 2 and - (ops[0] == 0 or ops[0] == "OP_0") and - isinstance(ops[1], str) and - len(h_to_b(ops[1])) == 20) - - - def is_p2tr(self) -> bool: - """ - Check if script is P2TR (Pay-to-Taproot). - - P2TR format: OP_1 <32-byte-key> - - Returns: - bool: True if script is P2TR - """ - ops = self.script - return (len(ops) == 2 and - (ops[0] == 1 or ops[0] == "OP_1") and - isinstance(ops[1], str) and - len(h_to_b(ops[1])) == 32) - - - def is_p2wsh(self) -> bool: - """ - Check if script is P2WSH (Pay-to-Witness-Script-Hash). - - P2WSH format: OP_0 <32-byte-script-hash> - - Returns: - bool: True if script is P2WSH - """ - ops = self.script - return (len(ops) == 2 and - (ops[0] == 0 or ops[0] == "OP_0") and - isinstance(ops[1], str) and - len(h_to_b(ops[1])) == 32) - - - def is_p2sh(self) -> bool: - """ - Check if script is P2SH (Pay-to-Script-Hash). - - P2SH format: OP_HASH160 <20-byte-script-hash> OP_EQUAL - - Returns: - bool: True if script is P2SH - """ - ops = self.script - return (len(ops) == 3 and - ops[0] == 'OP_HASH160' and - isinstance(ops[1], str) and - len(h_to_b(ops[1])) == 20 and - ops[2] == 'OP_EQUAL') - - - def is_p2pkh(self) -> bool: - """ - Check if script is P2PKH (Pay-to-Public-Key-Hash). - - P2PKH format: OP_DUP OP_HASH160 <20-byte-key-hash> OP_EQUALVERIFY OP_CHECKSIG - - Returns: - bool: True if script is P2PKH - """ - ops = self.script - return (len(ops) == 5 and - ops[0] == 'OP_DUP' and - ops[1] == 'OP_HASH160' and - isinstance(ops[2], str) and - len(h_to_b(ops[2])) == 20 and - ops[3] == 'OP_EQUALVERIFY' and - ops[4] == 'OP_CHECKSIG') - - - def is_multisig(self) -> tuple[bool, Union[tuple[int, int], None]]: - """ - Check if script is a multisig script. - - Multisig format: OP_M ... OP_N OP_CHECKMULTISIG - - Returns: - tuple: (bool, (M, N) if multisig, None otherwise) - """ - ops = self.script - - if (len(ops) >= 4 and - isinstance(ops[0], int) and - ops[-1] == 'OP_CHECKMULTISIG' and - isinstance(ops[-2], int)): - - m = ops[0] # Required signatures - n = ops[-2] # Total public keys - - # Validate the structure - if len(ops) == n + 3: # M + N pubkeys + N + OP_CHECKMULTISIG - return True, (m, n) - - return False, None - - - def get_script_type(self) -> str: - """ - Determine the type of script. - - Returns: - str: Script type ('p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr', 'multisig', 'unknown') - """ - if self.is_p2pkh(): - return 'p2pkh' - elif self.is_p2sh(): - return 'p2sh' - elif self.is_p2wpkh(): - return 'p2wpkh' - elif self.is_p2wsh(): - return 'p2wsh' - elif self.is_p2tr(): - return 'p2tr' - elif self.is_multisig()[0]: - return 'multisig' - else: - return 'unknown' - - - def __str__(self) -> str: - return str(self.script) - - - def __repr__(self) -> str: - return self.__str__() - - - def __eq__(self, _other: object) -> bool: - if not isinstance(_other, Script): - return False - return self.script == _other.script + """Represents any script in Bitcoin + + A Script contains just a list of OP_CODES and also knows how to serialize + into bytes + + Attributes + ---------- + script : list + the list with all the script OP_CODES and data + + Methods + ------- + to_bytes() + returns a serialized byte version of the script + + to_hex() + returns a serialized version of the script in hex + + get_script() + returns the list of strings that makes up this script + + copy() + creates a copy of the object (classmethod) + + from_raw() + + to_p2sh_script_pub_key() + converts script to p2sh scriptPubKey (locking script) + + to_p2wsh_script_pub_key() + converts script to p2wsh scriptPubKey (locking script) + + is_p2wpkh() + checks if script is P2WPKH (Pay-to-Witness-Public-Key-Hash) + + is_p2tr() + checks if script is P2TR (Pay-to-Taproot) + + is_p2wsh() + checks if script is P2WSH (Pay-to-Witness-Script-Hash) + + is_p2sh() + checks if script is P2SH (Pay-to-Script-Hash) + + is_p2pkh() + checks if script is P2PKH (Pay-to-Public-Key-Hash) + + is_multisig() + checks if script is a multisig script + + get_script_type() + determines the type of script + + Raises + ------ + ValueError + If string data is too large or integer is negative + """ + + def __init__(self, script: list[Any]): + """See Script description""" + + self.script: list[Any] = script + + + @classmethod + def copy(cls, script: "Script") -> "Script": + """Deep copy of Script""" + scripts = copy.deepcopy(script.script) + return cls(scripts) + + + def _op_push_data(self, data): + """Converts data to appropriate OP_PUSHDATA OP code including length""" + + # Handle bytes directly + if isinstance(data, bytes): + data_bytes = data + # Handle hex strings + elif isinstance(data, str): + try: + # Check if it's a valid hex string + if len(data) % 2 == 0 and all(c in '0123456789abcdefABCDEF' for c in data): + data_bytes = bytes.fromhex(data) + else: + data_bytes = data.encode('utf-8') + except ValueError: + data_bytes = data.encode('utf-8') + # Handle ints + elif isinstance(data, int): + # Special handling for small integers that might be opcodes + if data == 0: + return b'\x00' # OP_0 + elif 1 <= data <= 16: + return bytes([0x50 + data]) # OP_1 through OP_16 + elif data == 0x51: + return b'\x51' + elif data == 0x52: + return b'\x52' + elif data == 0x53: + return b'\x53' + elif data == 0xae: + return b'\xae' + else: + # Convert integer to bytes + data_bytes = data.to_bytes((data.bit_length() + 7) // 8 or 1, 'little') + else: + raise TypeError(f"Unsupported data type for push: {type(data)}") + + length = len(data_bytes) + if length < 0x4c: + return bytes([length]) + data_bytes + elif length <= 0xff: + return b'\x4c' + bytes([length]) + data_bytes + elif length <= 0xffff: + return b'\x4d' + length.to_bytes(2, 'little') + data_bytes + else: + return b'\x4e' + length.to_bytes(4, 'little') + data_bytes + + + def _push_integer(self, integer: int) -> bytes: + """Converts integer to bytes; as signed little-endian integer""" + if integer < 0: + raise ValueError("Integer is currently required to be positive.") + + + number_of_bytes = (integer.bit_length() + 7) // 8 + integer_bytes = integer.to_bytes(number_of_bytes, byteorder="little") + + + if integer & (1 << number_of_bytes * 8 - 1): + integer_bytes += b"\x00" + + + return self._op_push_data(b_to_h(integer_bytes)) + + + def to_bytes(self) -> bytes: + """Converts the script to bytes""" + script_bytes = b"" + for token in self.script: + if isinstance(token, str) and token in OP_CODES: + # It's an opcode string like 'OP_CHECKMULTISIG' + script_bytes += OP_CODES[token] + elif isinstance(token, int): + # Handle integer opcodes directly + if token == 0: + script_bytes += b'\x00' # OP_0 + elif 1 <= token <= 16: + script_bytes += bytes([0x50 + token]) # OP_1 through OP_16 + elif token == 0x51: # OP_1 + script_bytes += b'\x51' + elif token == 0x52: # OP_2 + script_bytes += b'\x52' + elif token == 0x53: # OP_3 + script_bytes += b'\x53' + elif token == 0xae: # OP_CHECKMULTISIG + script_bytes += b'\xae' + elif 0x50 <= token <= 0x60: # Other single-byte opcodes + script_bytes += bytes([token]) + else: + # For other integers, push as data + script_bytes += self._push_integer(token) + elif isinstance(token, bytes): + # Raw bytes - push with length prefix + script_bytes += self._op_push_data(token) + elif isinstance(token, str): + # Assume it's hex data if not an opcode + script_bytes += self._op_push_data(token) + else: + raise TypeError(f"Invalid token type in script: {type(token)}") + return script_bytes + + def to_hex(self) -> str: + """Converts the script to hexadecimal""" + return b_to_h(self.to_bytes()) + + @classmethod + def from_bytes(cls, byte_data: bytes) -> "Script": + """ + Creates a Script object from raw bytes. + + Args: + byte_data (bytes): The raw bytes of the script. + + Returns: + Script: A new Script object parsed from the byte data. + """ + return cls.from_raw(byte_data) + + + @staticmethod + def from_raw(scriptrawhex: Union[str, bytes], has_segwit: bool = False): + """ + Imports a Script commands list from raw hexadecimal data + """ + if isinstance(scriptrawhex, str): + scriptraw = h_to_b(scriptrawhex) + elif isinstance(scriptrawhex, bytes): + scriptraw = scriptrawhex + else: + raise TypeError("Input must be a hexadecimal string or bytes") + + commands = [] + index = 0 + + + while index < len(scriptraw): + byte = scriptraw[index] + if bytes([byte]) in CODE_OPS: + if ( + bytes([byte]) != b"\x4c" + and bytes([byte]) != b"\x4d" + and bytes([byte]) != b"\x4e" + ): + commands.append(CODE_OPS[bytes([byte])]) + index = index + 1 + if has_segwit is False and bytes([byte]) == b"\x4c": + bytes_to_read = int.from_bytes( + scriptraw[index : index + 1], "little" + ) + index = index + 1 + commands.append(scriptraw[index : index + bytes_to_read].hex()) + index = index + bytes_to_read + elif has_segwit is False and bytes([byte]) == b"\x4d": + bytes_to_read = int.from_bytes( + scriptraw[index : index + 2], "little" + ) + index = index + 2 + commands.append(scriptraw[index : index + bytes_to_read].hex()) + index = index + bytes_to_read + elif has_segwit is False and bytes([byte]) == b"\x4e": + bytes_to_read = int.from_bytes( + scriptraw[index : index + 4], "little" + ) + index = index + 4 + commands.append(scriptraw[index : index + bytes_to_read].hex()) + index = index + bytes_to_read + else: + data_size, size = vi_to_int(scriptraw[index : index + 8]) + commands.append( + scriptraw[index + size : index + size + data_size].hex() + ) + index = index + data_size + size + + + return Script(script=commands) + + + def get_script(self) -> list[Any]: + """Returns script as array of strings""" + return self.script + + + def to_p2sh_script_pub_key(self) -> "Script": + """Converts script to p2sh scriptPubKey (locking script)""" + script_hash160 = ripemd160(hashlib.sha256(self.to_bytes()).digest()) + hex_hash160 = b_to_h(script_hash160) + return Script(["OP_HASH160", hex_hash160, "OP_EQUAL"]) + + + def to_p2wsh_script_pub_key(self) -> "Script": + """Converts script to p2wsh scriptPubKey (locking script)""" + sha256 = hashlib.sha256(self.to_bytes()).digest() + return Script(["OP_0", b_to_h(sha256)]) + + + def is_p2wpkh(self) -> bool: + """ + Check if script is P2WPKH (Pay-to-Witness-Public-Key-Hash). + + P2WPKH format: OP_0 <20-byte-key-hash> + + Returns: + bool: True if script is P2WPKH + """ + ops = self.script + return (len(ops) == 2 and + (ops[0] == 0 or ops[0] == "OP_0") and + isinstance(ops[1], str) and + len(h_to_b(ops[1])) == 20) + + + def is_p2tr(self) -> bool: + """ + Check if script is P2TR (Pay-to-Taproot). + + P2TR format: OP_1 <32-byte-key> + + Returns: + bool: True if script is P2TR + """ + ops = self.script + return (len(ops) == 2 and + (ops[0] == 1 or ops[0] == "OP_1") and + isinstance(ops[1], str) and + len(h_to_b(ops[1])) == 32) + + + def is_p2wsh(self) -> bool: + """ + Check if script is P2WSH (Pay-to-Witness-Script-Hash). + + P2WSH format: OP_0 <32-byte-script-hash> + + Returns: + bool: True if script is P2WSH + """ + ops = self.script + return (len(ops) == 2 and + (ops[0] == 0 or ops[0] == "OP_0") and + isinstance(ops[1], str) and + len(h_to_b(ops[1])) == 32) + + + def is_p2sh(self) -> bool: + """ + Check if script is P2SH (Pay-to-Script-Hash). + + P2SH format: OP_HASH160 <20-byte-script-hash> OP_EQUAL + + Returns: + bool: True if script is P2SH + """ + ops = self.script + return (len(ops) == 3 and + ops[0] == 'OP_HASH160' and + isinstance(ops[1], str) and + len(h_to_b(ops[1])) == 20 and + ops[2] == 'OP_EQUAL') + + + def is_p2pkh(self) -> bool: + """ + Check if script is P2PKH (Pay-to-Public-Key-Hash). + + P2PKH format: OP_DUP OP_HASH160 <20-byte-key-hash> OP_EQUALVERIFY OP_CHECKSIG + + Returns: + bool: True if script is P2PKH + """ + ops = self.script + return (len(ops) == 5 and + ops[0] == 'OP_DUP' and + ops[1] == 'OP_HASH160' and + isinstance(ops[2], str) and + len(h_to_b(ops[2])) == 20 and + ops[3] == 'OP_EQUALVERIFY' and + ops[4] == 'OP_CHECKSIG') + + + def is_multisig(self) -> tuple[bool, Union[tuple[int, int], None]]: + """ + Check if script is a multisig script. + + Multisig format: OP_M ... OP_N OP_CHECKMULTISIG + + Returns: + tuple: (bool, (M, N) if multisig, None otherwise) + """ + ops = self.script + + if (len(ops) >= 4 and + isinstance(ops[0], int) and + ops[-1] == 'OP_CHECKMULTISIG' and + isinstance(ops[-2], int)): + + m = ops[0] # Required signatures + n = ops[-2] # Total public keys + + # Validate the structure + if len(ops) == n + 3: # M + N pubkeys + N + OP_CHECKMULTISIG + return True, (m, n) + + return False, None + + + def get_script_type(self) -> str: + """ + Determine the type of script. + + Returns: + str: Script type ('p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr', 'multisig', 'unknown') + """ + if self.is_p2pkh(): + return 'p2pkh' + elif self.is_p2sh(): + return 'p2sh' + elif self.is_p2wpkh(): + return 'p2wpkh' + elif self.is_p2wsh(): + return 'p2wsh' + elif self.is_p2tr(): + return 'p2tr' + elif self.is_multisig()[0]: + return 'multisig' + else: + return 'unknown' + + + def __str__(self) -> str: + return str(self.script) + + + def __repr__(self) -> str: + return self.__str__() + + + def __eq__(self, _other: object) -> bool: + if not isinstance(_other, Script): + return False + return self.script == _other.script \ No newline at end of file diff --git a/bitcoinutils/transactions.py b/bitcoinutils/transactions.py index 75892a6e..547988a0 100644 --- a/bitcoinutils/transactions.py +++ b/bitcoinutils/transactions.py @@ -12,6 +12,7 @@ import math import hashlib import struct +from io import BytesIO from typing import Optional, Union from bitcoinutils.constants import ( @@ -42,6 +43,8 @@ parse_compact_size, ) +def i_to_little_endian(value, length): + return value.to_bytes(length, byteorder='little') class TxInput: """Represents a transaction input. @@ -348,7 +351,34 @@ def copy(cls, txout: "TxOutput") -> "TxOutput": """Deep copy of TxOutput""" return cls(txout.amount, txout.script_pubkey) + + @classmethod + def from_bytes(cls, b): + from bitcoinutils.script import Script + import struct + from io import BytesIO + + stream = BytesIO(b) + + # Read 8-byte value (little-endian) + value = struct.unpack(' str: "outputs": self.outputs, "has_segwit": self.has_segwit, "witnesses": self.witnesses, - "locktime": self.locktime.hex(), + "locktime": self.locktime, "version": self.version.hex(), } ) @@ -878,7 +910,7 @@ def get_transaction_segwit_digest( tx_for_signing += hash_outputs # add locktime - tx_for_signing += self.locktime + tx_for_signing += self.locktime.to_bytes(4, byteorder="little") # add sighash type tx_for_signing += struct.pack(" bytes: witnesses_count_bytes = encode_varint(len(witness.stack)) data += witnesses_count_bytes data += witness.to_bytes() - data += self.locktime + data += i_to_little_endian(self.locktime, 4) return data def get_txid(self) -> str: @@ -1120,30 +1178,30 @@ def get_size(self) -> int: return len(self.to_bytes(self.has_segwit)) def get_vsize(self) -> int: - """Gets the virtual size of the transaction. + """ + Gets the virtual size of the transaction. For non-segwit txs this is identical to get_size(). For segwit txs the marker and witnesses length needs to be reduced to 1/4 of its original - length. Thus it is substructed from size and then it is divided by 4 - before added back to size to produce vsize (always rounded up). + length. Thus it is subtracted from size and then it is divided by 4 + before being added back to size to produce vsize (always rounded up). https://en.bitcoin.it/wiki/Weight_units """ - # return size if non segwit + # return size if non-segwit if not self.has_segwit: return self.get_size() marker_size = 2 - - wit_size = 0 data = b"" - # count witnesses data + # count witness data for witness in self.witnesses: - # add witnesses stack count + # add witness stack count witnesses_count_bytes = chr(len(witness.stack)).encode() data += witnesses_count_bytes data += witness.to_bytes() + wit_size = len(data) size = self.get_size() - (marker_size + wit_size) @@ -1152,19 +1210,110 @@ def get_vsize(self) -> int: return int(math.ceil(vsize)) def to_hex(self) -> str: - """Converts object to hexadecimal string""" - + """Converts object to hexadecimal string.""" return b_to_h(self.to_bytes(self.has_segwit)) def serialize(self) -> str: - """Converts object to hexadecimal string""" - + """Alias for to_hex().""" return self.to_hex() + @staticmethod + def from_bytes(tx_bytes): + """ + Minimal inline deserializer for Transaction from bytes. + Only for testing — not full-featured! + """ + stream = BytesIO(tx_bytes) + return Transaction._parse_from_stream(stream) + + @classmethod + def _parse_from_stream(cls, stream): + """ + Parse a Bitcoin transaction from a byte stream (PSBT raw transaction). + """ + def read_varint(s): + i = s.read(1)[0] + if i == 0xfd: + return struct.unpack(' bytes: return i.to_bytes(byte_length, "big") +def read_varint(b: bytes) -> tuple[int, int]: + """ + Reads a Bitcoin varint from the provided bytes. + + Returns: + A tuple (value, size) where: + - value: the decoded integer + - size: the number of bytes consumed + """ + prefix = b[0] + if prefix < 0xfd: + return prefix, 1 + elif prefix == 0xfd: + return int.from_bytes(b[1:3], 'little'), 3 + elif prefix == 0xfe: + return int.from_bytes(b[1:5], 'little'), 5 + elif prefix == 0xff: + return int.from_bytes(b[1:9], 'little'), 9 + else: + raise ValueError("Invalid varint prefix") + + # TODO are these required - maybe bytestoint and inttobytes are only required?!? \ No newline at end of file From 00e86d9d6538fcbc01bb019a419b9b6f72097489 Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Fri, 18 Jul 2025 14:48:35 +0530 Subject: [PATCH 08/12] Update example files: combine_psbt, create_psbt_multisig, finalize_psbt, sign_psbt --- examples/combine_psbt.py | 44 ++-- examples/create_psbt_multisig.py | 420 +++++++------------------------ examples/finalize_psbt.py | 140 ++++------- examples/sign_psbt.py | 93 +++---- 4 files changed, 201 insertions(+), 496 deletions(-) diff --git a/examples/combine_psbt.py b/examples/combine_psbt.py index 72b45cfe..a9bf5f2f 100644 --- a/examples/combine_psbt.py +++ b/examples/combine_psbt.py @@ -1,22 +1,25 @@ #!/usr/bin/env python3 - """ -Combine multiple PSBTs (Partially Signed Bitcoin Transactions) into a single PSBT with merged signatures and metadata. +Example of combining multiple PSBTs into a single PSBT. -This script performs the combiner role defined in BIP-174, allowing multiple signers to contribute signatures separately, -and then merge their PSBTs into one unified transaction. +This example demonstrates how to: +1. Load multiple PSBTs that contain partial signatures +2. Combine them into a single PSBT with all signatures +3. Output the combined PSBT -Features: -- Loads multiple base64-encoded PSBTs -- Merges all inputs, outputs, and partial signatures -- Validates consistency across PSBTs before combining -- Outputs a single combined PSBT in base64 format +This is typically used in multisig scenarios where different +participants sign the same transaction independently and then +combine their signatures. Usage: python combine_psbt.py [ ...] -Returns: - Combined PSBT with merged data from all inputs +Example: + # Combine Alice's and Bob's signed PSBTs + python combine_psbt.py cHNidP8BAH... cHNidP8BAH... + +Note: All PSBTs must be for the same transaction. The combine + operation merges all partial signatures and other data. """ import sys @@ -24,20 +27,23 @@ from bitcoinutils.psbt import PSBT def main(): + """Combine multiple PSBTs into one.""" + + # Always set the network first setup('testnet') if len(sys.argv) < 3: - print("Usage: python combine_psbt.py [psbt3_base64] ...") + print("Usage: python combine_psbt.py [ ...]") return - # Load PSBTs from command line arguments - psbts = [PSBT.from_base64(psbt_base64) for psbt_base64 in sys.argv[1:]] + # Load first PSBT + psbt = PSBT.from_base64(sys.argv[1]) - # Combine all PSBTs using the first one as base - combined_psbt = psbts[0].combine_psbts(psbts[1:]) + # Load and combine with remaining PSBTs + other_psbts = [PSBT.from_base64(base64_str) for base64_str in sys.argv[2:]] + combined = psbt.combine_psbts(other_psbts) - # Output the combined PSBT - print(combined_psbt.to_base64()) + print(combined.to_base64()) if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/examples/create_psbt_multisig.py b/examples/create_psbt_multisig.py index 4d0c72d5..65024aa0 100644 --- a/examples/create_psbt_multisig.py +++ b/examples/create_psbt_multisig.py @@ -1,356 +1,122 @@ -# Copyright (C) 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. - +#!/usr/bin/env python3 """ -Example of creating a 2-of-3 multisig PSBT using REAL TESTNET4 UTXOs. +Example of creating a PSBT for spending from a P2WSH multisig address. + +This example demonstrates how to: +1. Create a 2-of-3 multisig witness script +2. Build a transaction spending from a P2WSH multisig +3. Create a PSBT with witness script information -This example demonstrates: -1. Creating a 2-of-3 multisig P2WSH address (Segwit multisig) -2. Creating a PSBT for spending from that address using real Testnet4 UTXOs -3. Setting up the PSBT with proper input information for signing +The PSBT can then be distributed to signers for partial signing. -IMPORTANT: This uses REAL TESTNET4 transactions that can be verified on: -https://blockstream.info/testnet/ +Run this first to create the initial PSBT, then use sign_psbt.py +to add signatures from each participant. -Note: This script uses bitcoinutils with setup('testnet'), which defaults to Testnet3. - For Testnet4 compatibility, ensure your UTXOs are from Testnet4 and verify - using a Testnet4-compatible explorer or wallet. +Example usage: + python create_psbt_multisig.py -Before running this example: -1. Get Testnet4 coins from a faucet (e.g., https://faucet.testnet4.dev/) -2. Create the multisig address shown below -3. Send Testnet4 coins to that address -4. Update the UTXO details below with your real transaction +Note: Uses testnet. Replace the UTXO with a real funded transaction. """ from bitcoinutils.setup import setup -from bitcoinutils.transactions import Transaction, TxInput, TxOutput, Locktime -from bitcoinutils.keys import PrivateKey, PublicKey +from bitcoinutils.transactions import Transaction, TxInput, TxOutput +from bitcoinutils.keys import PrivateKey, P2wshAddress from bitcoinutils.script import Script -from bitcoinutils.utils import to_satoshis from bitcoinutils.psbt import PSBT -from bitcoinutils.constants import TYPE_RELATIVE_TIMELOCK - - -def get_real_testnet_utxo(): - """ - STEP-BY-STEP GUIDE TO GET REAL TESTNET4 UTXO: - - 1. Visit a Testnet4 block explorer (e.g., https://blockstream.info/testnet/) - 2. Find any recent transaction with outputs - 3. Click on a transaction, copy its TXID - 4. Replace the values below with real Testnet4 data - 5. Verify the TXID works on a Testnet4-compatible explorer - - EXAMPLE OF HOW TO FIND REAL DATA: - - Go to a Testnet4-compatible explorer - - Click "Recent Transactions" - - Pick any transaction (e.g., click on a TXID) - - Copy the TXID from the URL - - Check the outputs for amount and vout index - """ - - # METHOD 1: Use a funding transaction you create yourself - # (Recommended - you control the UTXO) - create_own_funding = True - - if create_own_funding: - # TODO: After running this script once: - # 1. Note the multisig address printed below - # 2. Get Testnet4 coins from faucet - # 3. Send coins to the multisig address - # 4. Update these values with YOUR funding transaction - utxo_details = { - 'txid': 'YOUR_FUNDING_TXID_HERE', # ← Replace with your funding TXID - 'vout': 0, # ← Usually 0, but check the transaction - 'amount': to_satoshis(0.001), # ← Replace with actual amount sent - 'address': None, # Will be set to multisig address - 'is_placeholder': True # Set to False when using real data - } - else: - # METHOD 2: Use any existing Testnet4 UTXO (not recommended for production) - # This is just for demonstration - don't spend other people's UTXOs! - utxo_details = { - 'txid': 'SOME_EXISTING_TESTNET4_TXID', - 'vout': 0, - 'amount': to_satoshis(0.001), - 'address': None, - 'is_placeholder': True - } - - # Validation - if utxo_details['is_placeholder']: - print(" PLACEHOLDER DATA DETECTED!") - print(" This PSBT uses placeholder data and won't work on Testnet4.") - print(" Follow these steps to use real Testnet4 data:") - print() - print(" STEP 1: Get Testnet4 coins") - print(" • Visit: https://faucet.testnet4.dev/") - print(" • Request coins to any address you control") - print() - print(" STEP 2: Fund the multisig (run this script first to get address)") - print(" • Send Testnet4 coins to the multisig address") - print(" • Wait for confirmation") - print() - print(" STEP 3: Update this function") - print(" • Copy the funding transaction TXID") - print(" • Set utxo_details['txid'] = 'your_real_txid'") - print(" • Set utxo_details['amount'] = to_satoshis(your_real_amount)") - print(" • Set utxo_details['is_placeholder'] = False") - print() - print(" STEP 4: Verify") - print(" • Check on a Testnet4-compatible explorer") - print(" • Confirm the UTXO exists and amount is correct") - print() - - return utxo_details - def main(): - # Always call setup() first - using testnet (Testnet3, compatible with Testnet4 with real UTXOs) - setup('testnet') - - print("=" * 70) - print("Creating 2-of-3 Multisig PSBT with REAL TESTNET4 UTXOs") - print("=" * 70) - - # Step 1: Create three private keys (representing Alice, Bob, and Charlie) - print("\n1. Creating private keys for Alice, Bob, and Charlie...") - - # Using deterministic keys for consistency (in production, generate securely) - alice_private_key = PrivateKey.from_wif("cTALNpTpRbbxTCJ2A5Vq88UxT44w1PE2cYqiB3n4hRvzyCev1Wwo") - alice_public_key = alice_private_key.get_public_key() - print(f"Alice's public key: {alice_public_key.to_hex()}") + """Create a PSBT for a 2-of-3 multisig P2WSH transaction.""" - # Bob's key - bob_private_key = PrivateKey.from_wif("cVf3kGh6552jU2rLaKwXTKq5APHPoZqCP4GQzQirWGHFoHQ9rEVt") - bob_public_key = bob_private_key.get_public_key() - print(f"Bob's public key: {bob_public_key.to_hex()}") + # Always set the network first + setup('testnet') - # Charlie's key - charlie_private_key = PrivateKey.from_wif("cQDvVP5VhYsV3dtHQwQ5dCbL54WuJcvsUgr3LXwhf6vD5mPp9nVy") - charlie_public_key = charlie_private_key.get_public_key() - print(f"Charlie's public key: {charlie_public_key.to_hex()}") + print("=" * 60) + print("Create P2WSH Multisig PSBT Example") + print("=" * 60) + + # Example testnet UTXO - replace with your funded transaction + # You can get testnet coins from a faucet and create a P2WSH multisig + # transaction to fund this address + utxo = { + "txid": "6316f4f8dbf842f6982ed09c48df50cef58ee3dbf752eeb73187f2373ef23536", + "vout": 1, + "amount": 500000 # 0.005 BTC in satoshis + } - # Step 2: Create 2-of-3 multisig P2WSH script (Segwit version) - print("\n2. Creating 2-of-3 multisig P2WSH script...") + # Three private keys for the multisig participants + # In practice, each participant would only have their own key + alice_key = PrivateKey.from_wif('cTcFkAJtFvyPKjQhPkijgyv4ZRQTau6wQgd1M87Y221zm1sMTRFT') + bob_key = PrivateKey.from_wif('cUygdGhxnZfjyQZc5ugQY6su6qFgRndqh6JyQK4RN7ry6UUs1Rcj') + charlie_key = PrivateKey.from_wif('cTbY2V12VRvgLxvBJmd3iq3kN8bZHWGzYYHhG9T6bsisgHjjHCgu') - # Create the multisig witness script (2 of 3) - sorted keys for deterministic addresses - public_keys = sorted([alice_public_key, bob_public_key, charlie_public_key], - key=lambda k: k.to_hex()) + # Get public keys and sort them (BIP67 - lexicographic ordering) + pubkeys = sorted( + [alice_key.get_public_key(), bob_key.get_public_key(), charlie_key.get_public_key()], + key=lambda p: p.to_hex() + ) + # Create the 2-of-3 witness script witness_script = Script([ - 2, # Required signatures - public_keys[0].to_hex(), - public_keys[1].to_hex(), - public_keys[2].to_hex(), - 3, # Total public keys + 'OP_2', + pubkeys[0].to_hex(), + pubkeys[1].to_hex(), + pubkeys[2].to_hex(), + 'OP_3', 'OP_CHECKMULTISIG' ]) - print(f"Witness script: {witness_script.to_hex()}") - - # Create P2WSH address from the witness script - p2wsh_address = witness_script.to_p2wsh_script_pub_key().to_address() - print(f"P2WSH Multisig Address: {p2wsh_address}") - print(f" Check this address on a Testnet4-compatible explorer") - - # Step 3: Get real Testnet4 UTXO details - print("\n3. Getting real Testnet4 UTXO details...") - utxo = get_real_testnet_utxo() - utxo['address'] = p2wsh_address + # Create the P2WSH address from the witness script + p2wsh_address = P2wshAddress.from_script(witness_script) + print(f"\n1. Multisig P2WSH Address: {p2wsh_address.to_string()}") + print(f" Witness Script: {witness_script.to_hex()}") - print(f"Using UTXO:") - print(f" TXID: {utxo['txid']}") - print(f" Vout: {utxo['vout']}") - print(f" Amount: {utxo['amount']} satoshis ({utxo['amount'] / 100000000:.8f} BTC)") - print(f" Address: {utxo['address']}") + # Create a simple transaction spending the UTXO + # Send to Alice's individual segwit address (minus fee) + recipient = alice_key.get_public_key().get_segwit_address() + fee = 500 # 500 satoshis fee - if utxo['is_placeholder']: - print(f" PLACEHOLDER: This TXID is not verifiable") - print(f" This TXID won't verify - it's just an example format") - else: - print(f" VERIFY: Check on a Testnet4-compatible explorer") - print(f" This should show a real Testnet4 transaction") + # Build the transaction + tx_input = TxInput(utxo['txid'], utxo['vout']) + tx_output = TxOutput(utxo['amount'] - fee, recipient.to_script_pub_key()) + tx = Transaction([tx_input], [tx_output], has_segwit=True) - # Step 4: Create transaction inputs and outputs - print("\n4. Setting up transaction...") + print(f"\n2. Created Transaction:") + print(f" TXID: {tx.get_txid()}") + print(f" Spending: {utxo['amount']} satoshis") + print(f" Sending: {utxo['amount'] - fee} satoshis to {recipient.to_string()}") - # Input: Real Testnet4 UTXO - txin = TxInput(utxo['txid'], utxo['vout']) - - # Output: Send to Charlie's P2WPKH address (modern Segwit address) - charlie_p2wpkh_address = charlie_public_key.get_segwit_address() - - # Calculate output amount (leaving some for fees) - fee_amount = to_satoshis(0.0001) # 0.0001 BTC fee - send_amount = utxo['amount'] - fee_amount - - if send_amount <= 0: - raise ValueError("UTXO amount too small to cover fees!") - - txout = TxOutput(send_amount, charlie_p2wpkh_address.to_script_pub_key()) - - # Create the transaction - tx = Transaction([txin], [txout], Locktime(0)) - print(f"Unsigned transaction: {tx.serialize()}") - - # Step 5: Create PSBT - print("\n5. Creating PSBT...") - - # Create PSBT from the unsigned transaction + # Create the PSBT psbt = PSBT(tx) - # Add input information needed for signing P2WSH - # For P2WSH inputs, we need the witness script and witness UTXO info - psbt.add_input_witness_script(0, witness_script) - psbt.add_input_witness_utxo(0, utxo['amount'], p2wsh_address.to_script_pub_key()) - - print(f"PSBT created successfully!") - print(f"PSBT base64: {psbt.to_base64()}") - - # Step 6: Display verification information - print("\n6. TESTNET4 VERIFICATION") - print("=" * 50) - - if utxo['is_placeholder']: - print(" USING PLACEHOLDER DATA - NOT VERIFIABLE") - print(" Current TXID is fake and won't verify on explorer") - print(" To fix this:") - print(" 1. Get real Testnet4 coins from faucet") - print(" 2. Send to the multisig address above") - print(" 3. Update get_real_testnet_utxo() with real data") - print() - print(" When ready, verify with a Testnet4-compatible explorer") - else: - print(" REAL TESTNET4 DATA - VERIFIABLE") - print(" Verify input transaction on a Testnet4-compatible explorer") - - print(f" Check multisig address balance on a Testnet4-compatible explorer") - print(f" After signing and broadcasting, check output:") - print(f" Address: {charlie_p2wpkh_address}") - - # Step 7: Display signing workflow - print("\n7. SIGNING WORKFLOW") - print("=" * 50) - print("This PSBT is ready for the 2-of-3 multisig signing process:") - print() - print("1. Alice signs:") - print(" - Import PSBT") - print(" - Sign with Alice's private key") - print(" - Export partial signature") - print() - print("2. Bob signs:") - print(" - Import PSBT (with Alice's signature)") - print(" - Sign with Bob's private key") - print(" - Export complete signature") - print() - print("3. Finalize and broadcast:") - print(" - Combine signatures (2 of 3 threshold met)") - print(" - Finalize PSBT to create broadcastable transaction") - print(" - Broadcast to Testnet4") - print(" - Monitor on a Testnet4-compatible explorer") - - # Step 8: Show the structure for educational purposes - print("\n8. PSBT STRUCTURE ANALYSIS") - print("=" * 50) - print(f"Global data:") - print(f" - Unsigned transaction: {tx.serialize()}") - print(f" - Version: {psbt.version}") - print(f" - Transaction type: P2WSH (Segwit multisig)") - - print(f"\nInput 0 data:") - print(f" - Previous TXID: {utxo['txid']}") - print(f" - Previous Vout: {utxo['vout']}") - print(f" - Witness Script: {witness_script.to_hex()}") - print(f" - Amount: {utxo['amount']} satoshis") - print(f" - Script type: P2WSH") - print(f" - Required signatures: 2 of 3") - - print(f"\nOutput 0 data:") - print(f" - Amount: {send_amount} satoshis") - print(f" - Fee: {fee_amount} satoshis") - print(f" - Recipient: {charlie_p2wpkh_address}") - print(f" - Script type: P2WPKH") - - # Step 9: How to get real Testnet4 coins - print("\n9. HOW TO GET REAL TESTNET4 COINS & TXID") - print("=" * 50) - print("COMPLETE WORKFLOW FOR REAL TESTNET4 DATA:") - print() - print("PHASE 1: Setup") - print("1. Run this script AS-IS to get your multisig address") - print("2. Copy the P2WSH address from the output above") - print() - print("PHASE 2: Get Testnet4 coins") - print("3. Visit Testnet4 faucet:") - print(" • https://faucet.testnet4.dev/") - print(" • Request 0.001+ BTC to any address you control") - print() - print("PHASE 3: Fund multisig") - print("4. Send Testnet4 coins to your multisig address:") - print(f" • Send to: {p2wsh_address}") - print(" • Amount: 0.001 BTC (or whatever you got from faucet)") - print(" • Wait for 1+ confirmations") - print() - print("PHASE 4: Get real TXID") - print("5. Find your funding transaction:") - print(" • Use a Testnet4-compatible explorer") - print(" • Search for your multisig address") - print(" • Click on the funding transaction") - print(" • Copy the TXID") - print() - print("PHASE 5: Update code") - print("6. Edit get_real_testnet_utxo() function:") - print(" • Set txid = 'your_real_txid_here'") - print(" • Set amount = to_satoshis(your_actual_amount)") - print(" • Set is_placeholder = False") - print() - print("PHASE 6: Verify & test") - print("7. Re-run this script") - print(" • Should show REAL TESTNET4 DATA") - print(" • TXID should be verifiable on a Testnet4 explorer") - print(" • PSBT should be ready for signing") - print() - print("EXAMPLE of real Testnet4 TXID format:") - print("b4c1a58d7f8e9a2b3c4d5e6f1234567890abcdef1234567890abcdef12345678") - print("(64 hex characters - yours will look similar)") - print() - print(" Your mentor can then verify:") - print("• Paste your TXID into a Testnet4-compatible explorer") - print("• See real transaction with real UTXOs") - print("• Confirm PSBT references actual Testnet4 blockchain data") - - return psbt, { - 'multisig_address': p2wsh_address, - 'witness_script': witness_script.to_hex(), - 'recipient_address': charlie_p2wpkh_address, - 'utxo': utxo - } - + # Add witness script information for the input + # This tells signers how to sign this P2WSH input + psbt.inputs[0].witness_script = witness_script + + # Add witness UTXO information + # This provides the amount and script needed for signing + psbt.inputs[0].witness_utxo = TxOutput( + utxo['amount'], + p2wsh_address.to_script_pub_key() + ) + + # Serialize to base64 for distribution + psbt_b64 = psbt.to_base64() + + print(f"\n3. PSBT Created Successfully!") + print(f" Base64 PSBT:\n {psbt_b64}") + + print(f"\n4. Next Steps:") + print(f" - Distribute this PSBT to the multisig participants") + print(f" - Each participant signs using: python sign_psbt.py 0") + print(f" - After 2 signatures, combine using: python combine_psbt.py ") + print(f" - Finalize using: python finalize_psbt.py ") + print(f" - Broadcast the final transaction to the network") + + print(f"\n5. Example signing commands:") + print(f" # Alice signs:") + print(f" python sign_psbt.py {psbt_b64[:50]}... cTcFkAJtFvyPKjQhPkijgyv4ZRQTau6wQgd1M87Y221zm1sMTRFT 0") + print(f" # Bob signs:") + print(f" python sign_psbt.py {psbt_b64[:50]}... cUygdGhxnZfjyQZc5ugQY6su6qFgRndqh6JyQK4RN7ry6UUs1Rcj 0") if __name__ == "__main__": - created_psbt, info = main() - - print(f"\n" + "=" * 70) - print(" PSBT CREATION COMPLETED!") - print("=" * 70) - print(f" PSBT (base64): {created_psbt.to_base64()}") - print() - print(" NEXT STEPS:") - print("1. Fund the multisig address with real Testnet4 coins") - print("2. Update the UTXO details in get_real_testnet_utxo()") - print("3. Re-run this script") - print("4. Sign the PSBT with 2 of the 3 private keys") - print("5. Broadcast to Testnet4 and verify on a Testnet4-compatible explorer") - print() - print(f" Multisig address: {info['multisig_address']}") - print(f" Check balance on a Testnet4-compatible explorer") \ No newline at end of file + main() \ No newline at end of file diff --git a/examples/finalize_psbt.py b/examples/finalize_psbt.py index 2cf18627..64506410 100644 --- a/examples/finalize_psbt.py +++ b/examples/finalize_psbt.py @@ -1,111 +1,75 @@ #!/usr/bin/env python3 """ -Finalize a PSBT (Partially Signed Bitcoin Transaction) to produce a broadcastable Bitcoin transaction. +Example of finalizing a PSBT into a complete transaction. -This script serves as the finalizer step (as defined in BIP-174), assembling signatures and scripts -into a complete transaction ready for broadcast. +This example demonstrates how to: +1. Load a PSBT that has all required signatures +2. Finalize it into a complete transaction +3. Optionally validate the transaction +4. Output the hex transaction ready for broadcast -Features: -- Loads a base64-encoded PSBT from string or file -- Finalizes all inputs by constructing scriptSig/scriptWitness -- Optionally validates that all inputs are fully signed before finalization -- Outputs the raw hex-encoded Bitcoin transaction +The finalization process extracts all signatures from the PSBT +and constructs the final scriptSig and/or witness data. Usage: - python finalize_psbt.py - python finalize_psbt.py --file - python finalize_psbt.py --file --validate + python finalize_psbt.py [--validate] -Arguments: - PSBT data as a base64-encoded string - --file Load PSBT from a file - --validate (Optional) Enforce validation before finalizing +Example: + # Basic finalization + python finalize_psbt.py cHNidP8BAH... + + # With validation + python finalize_psbt.py cHNidP8BAH... --validate -Returns: - Hex-encoded, fully signed Bitcoin transaction ready for broadcast +Note: The PSBT must have all required signatures before finalization. + Use --validate to perform additional transaction validation. """ - -import argparse -import base64 import sys from bitcoinutils.setup import setup from bitcoinutils.psbt import PSBT - def main(): - """ - Main function for PSBT finalization. + """Finalize a PSBT into a complete transaction.""" - Usage: - python finalize_psbt.py - python finalize_psbt.py --file - python finalize_psbt.py --file --validate - """ - parser = argparse.ArgumentParser(description='Finalize a PSBT and create a transaction.') - parser.add_argument('psbt', nargs='?', help='Base64-encoded PSBT string') - parser.add_argument('--file', help='Text file containing base64 PSBT') - parser.add_argument('--validate', action='store_true', help='Validate finalized transaction') - parser.add_argument('--network', choices=['mainnet', 'testnet'], default='testnet', - help='Bitcoin network (default: testnet)') + # Always set the network first + setup('testnet') - args = parser.parse_args() + if len(sys.argv) < 2: + print("Usage: python finalize_psbt.py [--validate]") + return - # Setup the library for specified network - setup(args.network) + # Load PSBT + psbt = PSBT.from_base64(sys.argv[1]) - try: - # Load PSBT from input - if args.file: - with open(args.file, 'r') as f: - psbt_b64 = f.read().strip() - elif args.psbt: - psbt_b64 = args.psbt - else: - print("Error: Provide either base64 string or --file option.") - print("Use --help for usage information.") - return 1 - - # Create PSBT object - psbt = PSBT.from_base64(psbt_b64) - - # Finalize the PSBT - if args.validate: - final_tx, validation = psbt.finalize(validate=True) - - print("Finalized Transaction (Hex):") - print(final_tx.serialize()) - - print("\nValidation Report:") - print(f"Valid: {validation['valid']}") - print(f"Transaction ID: {validation['txid']}") - print(f"Size: {validation['size']} bytes") - print(f"Virtual Size: {validation['vsize']} vbytes") - - if validation['errors']: - print("Errors:") - for error in validation['errors']: - print(f" - {error}") + # Check if validation requested + validate = '--validate' in sys.argv + + # Finalize the PSBT + if validate: + final_tx, validation_info = psbt.finalize(validate=True) + if validation_info['valid']: + print(f"\nFinalized Transaction (hex):") + print(final_tx.to_hex()) + print(f"\nTransaction ID: {final_tx.get_txid()}") + print(f"Size: {validation_info['size']} bytes") + print(f"Virtual Size: {validation_info['vsize']} vbytes") - if validation['warnings']: - print("Warnings:") - for warning in validation['warnings']: - print(f" - {warning}") - + print(f"\nTo broadcast:") + print(f" bitcoin-cli -testnet sendrawtransaction {final_tx.to_hex()[:50]}...") else: - final_tx = psbt.finalize(validate=False) - print("Finalized Transaction (Hex):") - print(final_tx.serialize()) - - print(f"\nTransaction ready to broadcast!") - print(f"Use 'bitcoin-cli sendrawtransaction {final_tx.serialize()}' to broadcast") - - return 0 - - except Exception as e: - print(f"Error: {str(e)}") - return 1 - + print("Finalization failed - validation errors found") + sys.exit(1) + else: + final_tx = psbt.finalize(validate=False) + if final_tx: + print(final_tx.to_hex()) + print(f"\nTransaction ID: {final_tx.get_txid()}") + print(f"\nTo broadcast:") + print(f" bitcoin-cli -testnet sendrawtransaction {final_tx.to_hex()[:50]}...") + else: + print("Finalization failed - missing signatures or invalid PSBT") + sys.exit(1) if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + main() \ No newline at end of file diff --git a/examples/sign_psbt.py b/examples/sign_psbt.py index 61c9a1bb..44fc5956 100644 --- a/examples/sign_psbt.py +++ b/examples/sign_psbt.py @@ -1,85 +1,54 @@ -# Copyright (C) 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. - +#!/usr/bin/env python3 """ -Sign a specific input of a PSBT (Partially Signed Bitcoin Transaction) using a WIF private key. +Example of signing a PSBT with a private key. -This script allows targeted signing of one input in a PSBT, which is useful for multisig setups, -hardware wallet integrations, or step-by-step signing processes. +This example demonstrates how to: +1. Load a PSBT from base64 +2. Sign a specific input with a private key +3. Output the signed PSBT -Features: -- Loads a PSBT from a base64-encoded string -- Signs a specified input using a provided WIF-formatted private key -- Supports multiple script types: P2PKH, P2SH, P2WPKH, P2WSH, P2TR -- Allows optional SIGHASH type customization (default: SIGHASH_ALL) +The PSBT signing logic automatically detects the script type +(P2PKH, P2WPKH, P2SH, P2WSH, etc.) and signs appropriately. Usage: - python sign_psbt.py [sighash_type] + python sign_psbt.py -Arguments: - psbt_base64 The PSBT in base64 encoding - private_key_wif The private key in Wallet Import Format (WIF) - input_index Index of the input to sign (0-based) - sighash_type (Optional) Bitcoin SIGHASH flag (e.g., SIGHASH_ALL, SIGHASH_SINGLE) +Example: + # Alice signs input 0 + python sign_psbt.py cHNidP8B... cTcFkAJtFvyPKjQh... 0 + + # Bob signs input 0 + python sign_psbt.py cHNidP8B... cUygdGhxnZfjyQZ... 0 -Returns: - Updated PSBT with the input partially signed and printed as base64 +Note: The input_index parameter is required to specify which input to sign. + In a multisig scenario, multiple parties sign the same input. """ - import sys from bitcoinutils.setup import setup from bitcoinutils.keys import PrivateKey from bitcoinutils.psbt import PSBT -from bitcoinutils.constants import SIGHASH_ALL - def main(): - """Main function for signing PSBT example.""" - # Always call setup() first + """Sign a PSBT input with a private key.""" + + # Always set the network first setup('testnet') - # Parse command line arguments - if len(sys.argv) < 4: - print("Usage: python sign_psbt.py [sighash_type]") - print("\nExample:") - print("python sign_psbt.py cTALNpTpRbbxTCJ2A5Vq88UxT44w1PE2cYqiB3n4hRvzyCev1Wwo 0") - sys.exit(1) + if len(sys.argv) != 4: + print("Usage: python sign_psbt.py ") + return - psbt_base64 = sys.argv[1] - private_key_wif = sys.argv[2] + # Load PSBT and private key + psbt = PSBT.from_base64(sys.argv[1]) + private_key = PrivateKey.from_wif(sys.argv[2]) input_index = int(sys.argv[3]) - sighash_type = int(sys.argv[4]) if len(sys.argv) > 4 else SIGHASH_ALL - try: - # Load PSBT from base64 - psbt = PSBT.from_base64(psbt_base64) - - # Load private key - private_key = PrivateKey.from_wif(private_key_wif) - - # Sign the specified input - success = psbt.sign_input(input_index, private_key, sighash_type) - - if success: - # Output the updated PSBT - print(psbt.to_base64()) - else: - print("Failed to sign input", file=sys.stderr) - sys.exit(1) - - except Exception as e: - print(f"Error: {str(e)}", file=sys.stderr) + # Sign the specified input + if psbt.sign_input(input_index, private_key): + print(psbt.to_base64()) + else: sys.exit(1) - if __name__ == "__main__": - main() + main() \ No newline at end of file From f2c301f295b4587efd3c7eead1e25fe293941886 Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Sat, 19 Jul 2025 00:26:52 +0530 Subject: [PATCH 09/12] Added docstrings --- bitcoinutils/psbt.py | 614 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 586 insertions(+), 28 deletions(-) diff --git a/bitcoinutils/psbt.py b/bitcoinutils/psbt.py index 0ddbcf48..88e2c55b 100644 --- a/bitcoinutils/psbt.py +++ b/bitcoinutils/psbt.py @@ -1,3 +1,26 @@ +""" +Partially Signed Bitcoin Transaction (PSBT) implementation following +BIP-174. + +This module provides the PSBT class which represents a partially signed +bitcoin transaction that can be shared between multiple parties for signing +before broadcasting to the network. + +A PSBT contains: +- The unsigned transaction +- Input metadata needed for signing (UTXOs, scripts, keys, etc.) +- Output metadata for validation +- Partial signatures from different signers + +The PSBT workflow typically involves: +1. Creator: Creates the PSBT with the unsigned transaction +2. Updater: Adds input/output metadata needed for signing +3. Signer: Signs inputs they can sign (sign_input() handles all script types automatically) +4. Combiner: Combines multiple PSBTs with different signatures +5. Finalizer: Finalizes the PSBT by adding final scriptSig/witness +6. Extractor: Extracts the final signed transaction +""" + import struct from io import BytesIO import ecdsa @@ -11,6 +34,28 @@ from bitcoinutils.utils import read_varint class PSBTInput: + """Represents a single input in a PSBT with all associated metadata. + + This class holds all the data associated with a single input in a PSBT, + including UTXO information, partial signatures, scripts, and derivation paths. + + Attributes: + non_witness_utxo: The complete previous transaction for non-witness inputs + witness_utxo: The previous transaction output for witness inputs + partial_sigs: Dictionary mapping public keys to their signatures + sighash_type: The signature hash type to use for this input + redeem_script: The redeem script for P2SH inputs + witness_script: The witness script for P2WSH or P2SH-P2WSH inputs + bip32_derivs: Dictionary mapping public keys to their BIP32 derivation paths + final_scriptsig: The finalized scriptSig for this input + final_scriptwitness: The finalized witness stack for this input + ripemd160_preimages: Preimages for RIPEMD160 hashes + sha256_preimages: Preimages for SHA256 hashes + hash160_preimages: Preimages for HASH160 hashes + hash256_preimages: Preimages for HASH256 hashes + proprietary: Proprietary key-value pairs + unknown: Unknown key-value pairs + """ def __init__(self): # BIP-174 defined fields @@ -35,6 +80,18 @@ def __init__(self): self.unknown: Dict[bytes, bytes] = {} class PSBTOutput: + """Represents a single output in a PSBT with all associated metadata. + + This class holds all the data associated with a single output in a PSBT, + including scripts and derivation paths. + + Attributes: + redeem_script: The redeem script for P2SH outputs + witness_script: The witness script for P2WSH or P2SH-P2WSH outputs + bip32_derivs: Dictionary mapping public keys to their BIP32 derivation paths + proprietary: Proprietary key-value pairs + unknown: Unknown key-value pairs + """ def __init__(self): # BIP-174 defined fields @@ -47,18 +104,39 @@ def __init__(self): self.unknown: Dict[bytes, bytes] = {} class PSBT: + """Bitcoin Partially Signed Bitcoin Transaction (PSBT) implementation as per BIP-174. + + This class provides a complete implementation of the PSBT format, allowing for + the creation, parsing, signing, combining, and finalization of PSBTs. + + PSBTs are useful for constructing transactions in a collaborative manner, + where multiple parties may need to provide signatures or where signing + happens on offline/hardware devices. + + Attributes: + tx: The unsigned transaction + inputs: List of PSBTInput objects containing input-specific data + outputs: List of PSBTOutput objects containing output-specific data + version: PSBT version number + xpubs: Dictionary mapping extended public keys to derivation paths + proprietary: Global proprietary key-value pairs + unknown: Global unknown key-value pairs + """ + # PSBT magic bytes and version MAGIC = b'psbt' VERSION = 0 # Key types as defined in BIP-174 class GlobalTypes: + """Global key types for PSBT as defined in BIP-174.""" UNSIGNED_TX = 0x00 XPUB = 0x01 VERSION = 0xFB PROPRIETARY = 0xFC class InputTypes: + """Input key types for PSBT as defined in BIP-174.""" NON_WITNESS_UTXO = 0x00 WITNESS_UTXO = 0x01 PARTIAL_SIG = 0x02 @@ -75,12 +153,24 @@ class InputTypes: PROPRIETARY = 0xFC class OutputTypes: + """Output key types for PSBT as defined in BIP-174.""" REDEEM_SCRIPT = 0x00 WITNESS_SCRIPT = 0x01 BIP32_DERIVATION = 0x02 PROPRIETARY = 0xFC def _safe_to_bytes(self, obj): + """Safely convert various object types to bytes. + + Args: + obj: Object to convert to bytes + + Returns: + bytes: The object converted to bytes + + Raises: + TypeError: If the object cannot be converted to bytes + """ if isinstance(obj, Script): return obj.to_bytes() elif hasattr(obj, 'to_bytes'): @@ -94,7 +184,14 @@ def _safe_to_bytes(self, obj): def _safe_serialize_transaction(self, tx) -> bytes: - """Safely serialize a transaction to bytes.""" + """Safely serialize a transaction to bytes. + + Args: + tx: Transaction to serialize + + Returns: + bytes: Serialized transaction + """ if isinstance(tx, bytes): return tx @@ -104,6 +201,12 @@ def _safe_serialize_transaction(self, tx) -> bytes: return serialized def __init__(self, unsigned_tx: Optional[Transaction] = None): + """ + Initialize a new PSBT. + + Args: + unsigned_tx: The unsigned transaction. If None, an empty transaction is created. + """ if unsigned_tx is None: # Create empty transaction self.tx = Transaction([], []) @@ -129,12 +232,34 @@ def __init__(self, unsigned_tx: Optional[Transaction] = None): @classmethod def from_base64(cls, psbt_str: str) -> 'PSBT': + """Create a PSBT from a base64-encoded string. + + Args: + psbt_str: Base64-encoded PSBT string + + Returns: + PSBT: Decoded PSBT object + + Raises: + ValueError: If the string is not a valid base64-encoded PSBT + """ import base64 psbt_bytes = base64.b64decode(psbt_str) return cls.from_bytes(psbt_bytes) @classmethod def from_bytes(cls, psbt_bytes: bytes) -> 'PSBT': + """Create a PSBT from raw bytes. + + Args: + psbt_bytes: Raw PSBT bytes + + Returns: + PSBT: Parsed PSBT object + + Raises: + ValueError: If the bytes do not represent a valid PSBT + """ stream = BytesIO(psbt_bytes) # Read and verify magic @@ -162,10 +287,20 @@ def from_bytes(cls, psbt_bytes: bytes) -> 'PSBT': return psbt def to_base64(self) -> str: + """Encode the PSBT as a base64 string. + + Returns: + str: Base64-encoded PSBT + """ import base64 return base64.b64encode(self.to_bytes()).decode('ascii') def to_bytes(self) -> bytes: + """Serialize the PSBT to raw bytes. + + Returns: + bytes: Serialized PSBT + """ result = BytesIO() # Write magic and separator @@ -186,6 +321,13 @@ def to_bytes(self) -> bytes: return result.getvalue() def add_input(self, tx_input: TxInput, psbt_input: Optional[PSBTInput] = None) -> None: + """Add an input to the PSBT. + + Args: + tx_input: Transaction input to add + psbt_input: Optional PSBTInput with metadata. If not provided, + an empty PSBTInput will be created. + """ # Create clean input without scriptSig clean_input = TxInput(tx_input.txid, tx_input.txout_index) self.tx.inputs.append(clean_input) @@ -195,6 +337,13 @@ def add_input(self, tx_input: TxInput, psbt_input: Optional[PSBTInput] = None) - self.inputs.append(psbt_input) def add_output(self, tx_output: TxOutput, psbt_output: Optional[PSBTOutput] = None) -> None: + """Add an output to the PSBT. + + Args: + tx_output: Transaction output to add + psbt_output: Optional PSBTOutput with metadata. If not provided, + an empty PSBTOutput will be created. + """ self.tx.outputs.append(tx_output) if psbt_output is None: @@ -202,9 +351,35 @@ def add_output(self, tx_output: TxOutput, psbt_output: Optional[PSBTOutput] = No self.outputs.append(psbt_output) def sign(self, private_key: PrivateKey, input_index: int, sighash_type: int = 1) -> bool: + """Sign a specific input in the PSBT. + + This is a convenience method that calls sign_input. + + Args: + private_key: Private key to sign with + input_index: Index of the input to sign + sighash_type: Signature hash type (default: SIGHASH_ALL = 1) + + Returns: + bool: True if signing was successful, False otherwise + """ return self.sign_input(input_index, private_key, sighash_type) def sign_input(self, input_index: int, private_key: PrivateKey, sighash_type: int = 1) -> bool: + """Sign a specific input in the PSBT. + + This method adds a partial signature for the specified input using the + provided private key. It automatically detects the script type and + handles both legacy and SegWit inputs. + + Args: + input_index: Index of the input to sign + private_key: Private key to sign with + sighash_type: Signature hash type (default: SIGHASH_ALL = 1) + + Returns: + bool: True if signing was successful, False otherwise + """ try: psbt_input = self.inputs[input_index] @@ -271,6 +446,19 @@ def sign_input(self, input_index: int, private_key: PrivateKey, sighash_type: in return False def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, sighash_type: int) -> bytes: + """Get a signature for a specific input. + + This internal method generates a signature for the specified input, + handling different script types appropriately. + + Args: + input_index: Index of the input to sign + private_key: Private key to sign with + sighash_type: Signature hash type + + Returns: + bytes: The signature, or None if signing failed + """ input_data = self.inputs[input_index] tx_input = self.tx.inputs[input_index] @@ -399,21 +587,42 @@ def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, si def _is_p2pkh_script(self, script) -> bool: - """Check if script is P2PKH (OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG).""" + """Check if script is P2PKH (OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG). + + Args: + script: Script to check + + Returns: + bool: True if script is P2PKH, False otherwise + """ try: return script.is_p2pkh() if hasattr(script, 'is_p2pkh') else False except: return False def _is_p2wpkh_script(self, script) -> bool: - """Check if script is P2WPKH (OP_0 <20-byte-pubkeyhash>).""" + """Check if script is P2WPKH (OP_0 <20-byte-pubkeyhash>). + + Args: + script: Script to check + + Returns: + bool: True if script is P2WPKH, False otherwise + """ try: return script.is_p2wpkh() if hasattr(script, 'is_p2wpkh') else False except: return False def _is_p2tr_script(self, script_pubkey: Script) -> bool: - """Check if script is P2TR (Taproot).""" + """Check if script is P2TR (Taproot). + + Args: + script_pubkey: Script to check + + Returns: + bool: True if script is P2TR, False otherwise + """ if not hasattr(script_pubkey, 'script'): return False @@ -425,18 +634,43 @@ def _is_p2tr_script(self, script_pubkey: Script) -> bool: len(script_ops[1]) == 32) def _is_input_finalized(self, psbt_input: PSBTInput) -> bool: - """Check if an input is already finalized.""" + """Check if an input is already finalized. + + Args: + psbt_input: Input to check + + Returns: + bool: True if input is finalized, False otherwise + """ return (psbt_input.final_scriptsig is not None or psbt_input.final_scriptwitness is not None) def _apply_final_fields(self, tx_input: TxInput, input_data: PSBTInput) -> None: + """Apply finalized fields to a transaction input. + + Args: + tx_input: Transaction input to update + input_data: PSBT input data containing finalized fields + """ if input_data.final_scriptsig: tx_input.script_sig = input_data.final_scriptsig else: tx_input.script_sig = Script([]) def _validate_final_tx(self, final_tx) -> dict: - """Validate the finalized transaction.""" + """Validate the finalized transaction. + + Args: + final_tx: Transaction to validate + + Returns: + dict: Validation information including: + - valid: Whether the transaction is valid + - errors: List of error messages + - warnings: List of warning messages + - size: Transaction size in bytes + - vsize: Virtual size for fee calculation + """ validation_info = { 'valid': True, 'errors': [], @@ -495,7 +729,14 @@ def _validate_final_tx(self, final_tx) -> dict: return validation_info def _serialize_transaction_without_witness(self, tx) -> bytes: - """Serialize transaction without witness data for vsize calculation.""" + """Serialize transaction without witness data for vsize calculation. + + Args: + tx: Transaction to serialize + + Returns: + bytes: Serialized transaction without witness data + """ try: # Create a copy of the transaction without witness data from bitcoinutils.transactions import Transaction @@ -534,6 +775,21 @@ def _serialize_transaction_without_witness(self, tx) -> bytes: return serialized def combine(self, other: 'PSBT') -> 'PSBT': + """Combine this PSBT with another PSBT. + + PSBTs can be combined when they represent the same unsigned transaction. + The resulting PSBT will contain all the data from both PSBTs, with data + from 'other' taking precedence in case of conflicts. + + Args: + other: Another PSBT to combine with this one + + Returns: + PSBT: A new PSBT containing combined data from both PSBTs + + Raises: + ValueError: If the PSBTs have different unsigned transactions + """ # Ensure both PSBTs have the same unsigned transaction if self.tx.serialize() != other.tx.serialize(): raise ValueError("Cannot combine PSBTs with different transactions") @@ -610,12 +866,69 @@ def combine(self, other: 'PSBT') -> 'PSBT': return combined def combine_psbts(self, other_psbts: List['PSBT']) -> 'PSBT': + """Combines this PSBT with multiple other PSBTs. + + Wraps the pairwise `combine()` method in a loop for batch combining. + All PSBTs must have the same unsigned transaction. + + Parameters + ---------- + other_psbts : List[PSBT] + A list of PSBTs to combine with this one + + Returns + ------- + PSBT + The final combined PSBT containing all partial signatures and + metadata from all input PSBTs + + Raises + ------ + ValueError + If any PSBT has a different unsigned transaction + """ combined = self for other in other_psbts: combined = combined.combine(other) return combined def finalize(self, validate: bool = False) -> Union[Transaction, Tuple[Transaction, Dict]]: + """Finalizes all inputs and creates the final broadcastable transaction. + + Attempts to finalize all inputs by converting partial signatures into + final scriptSig/scriptWitness fields. If successful, produces a fully + signed transaction ready for broadcast. + + Parameters + ---------- + validate : bool, optional + If True, validates the final transaction and returns validation + info along with the transaction (default: False) + + Returns + ------- + Transaction or Tuple[Transaction, Dict] + If validate=False: Transaction object ready for broadcast, or None + if not all inputs could be finalized + If validate=True: Tuple of (Transaction, validation_info dict) + containing the transaction and validation details + + Raises + ------ + ValueError + If not all inputs can be finalized when validate=True + + Notes + ----- + The validation_info dict contains: + - 'valid': bool indicating if transaction is valid + - 'errors': List of error messages + - 'warnings': List of warning messages + - 'txid': Transaction ID if computable + - 'size': Transaction size in bytes + - 'vsize': Virtual size for fee calculation + """ + # Count successfully finalized inputs finalized_count = 0 for i in range(len(self.inputs)): @@ -675,6 +988,26 @@ def finalize(self, validate: bool = False) -> Union[Transaction, Tuple[Transacti return final_tx def finalize_input(self, input_index: int) -> bool: + """Finalizes a specific input. + + Converts partial signatures for a single input into final + scriptSig/scriptWitness fields. + + Parameters + ---------- + input_index : int + The index of the input to finalize + + Returns + ------- + bool + True if the input was successfully finalized, False otherwise + + Raises + ------ + ValueError + If input_index is out of range + """ if input_index >= len(self.inputs): raise ValueError(f"Input index {input_index} out of range") @@ -682,6 +1015,21 @@ def finalize_input(self, input_index: int) -> bool: return self._finalize_input(input_index) def _finalize_input(self, input_index: int) -> bool: + """Internal method for finalizing a single input. + + Handles the actual finalization logic for different script types + including P2PKH, P2WPKH, P2SH, P2WSH, and P2TR. + + Parameters + ---------- + input_index : int + The index of the input to finalize + + Returns + ------- + bool + True if the input was successfully finalized + """ psbt_input = self.inputs[input_index] @@ -722,7 +1070,19 @@ def _finalize_input(self, input_index: int) -> bool: return False def _finalize_p2pkh(self, psbt_input: PSBTInput) -> bool: - """Finalize P2PKH input.""" + """Finalizes a P2PKH (Pay-to-PubKey-Hash) input. + + Parameters + ---------- + psbt_input : PSBTInput + The PSBT input to finalize + + Returns + ------- + bool + True if finalization was successful + """ + if len(psbt_input.partial_sigs) != 1: return False @@ -731,7 +1091,19 @@ def _finalize_p2pkh(self, psbt_input: PSBTInput) -> bool: return True def _finalize_p2wpkh(self, psbt_input: PSBTInput) -> bool: - """Finalize P2WPKH input.""" + """Finalizes a P2WPKH (Pay-to-Witness-PubKey-Hash) input. + + Parameters + ---------- + psbt_input : PSBTInput + The PSBT input to finalize + + Returns + ------- + bool + True if finalization was successful + """ + if len(psbt_input.partial_sigs) != 1: return False @@ -741,7 +1113,20 @@ def _finalize_p2wpkh(self, psbt_input: PSBTInput) -> bool: return True def _finalize_p2sh(self, psbt_input: PSBTInput) -> bool: - """Finalize P2SH input with support for nested SegWit.""" + """Finalizes a P2SH (Pay-to-Script-Hash) input. + + Handles both regular P2SH and P2SH-wrapped SegWit scripts. + + Parameters + ---------- + psbt_input : PSBTInput + The PSBT input to finalize + + Returns + ------- + bool + True if finalization was successful + """ if not psbt_input.redeem_script: return False @@ -762,7 +1147,18 @@ def _finalize_p2sh(self, psbt_input: PSBTInput) -> bool: return success def _finalize_p2sh_p2wpkh(self, psbt_input: PSBTInput) -> bool: - """Finalize P2SH-wrapped P2WPKH input.""" + """Finalizes a P2SH-wrapped P2WPKH input. + + Parameters + ---------- + psbt_input : PSBTInput + The PSBT input to finalize + + Returns + ------- + bool + True if finalization was successful + """ if len(psbt_input.partial_sigs) != 1: return False @@ -779,7 +1175,18 @@ def _finalize_p2sh_p2wpkh(self, psbt_input: PSBTInput) -> bool: return True def _finalize_p2sh_p2wsh(self, psbt_input: PSBTInput) -> bool: - """Finalize P2SH-wrapped P2WSH input.""" + """Finalizes a P2SH-wrapped P2WSH input. + + Parameters + ---------- + psbt_input : PSBTInput + The PSBT input to finalize + + Returns + ------- + bool + True if finalization was successful + """ if not psbt_input.witness_script: return False @@ -795,14 +1202,38 @@ def _finalize_p2sh_p2wsh(self, psbt_input: PSBTInput) -> bool: return success def _finalize_p2wsh(self, psbt_input: PSBTInput) -> bool: - """Finalize P2WSH input.""" + """Finalizes a P2WSH (Pay-to-Witness-Script-Hash) input. + + Parameters + ---------- + psbt_input : PSBTInput + The PSBT input to finalize + + Returns + ------- + bool + True if finalization was successful + """ if not psbt_input.witness_script: return False return self._finalize_script(psbt_input, psbt_input.witness_script, is_witness=True) def _finalize_p2tr(self, psbt_input: PSBTInput) -> bool: - """Finalize P2TR (Taproot) input.""" + """Finalizes a P2TR (Pay-to-Taproot) input. + + Currently supports only key-path spending. + + Parameters + ---------- + psbt_input : PSBTInput + The PSBT input to finalize + + Returns + ------- + bool + True if finalization was successful + """ if len(psbt_input.partial_sigs) != 1: return False @@ -813,8 +1244,30 @@ def _finalize_p2tr(self, psbt_input: PSBTInput) -> bool: return True def _finalize_script(self, psbt_input: PSBTInput, script: Script, is_witness: bool) -> bool: - """ - Finalize a script by constructing the appropriate scriptSig or witness. + """Finalizes a script with enhanced multisig support. + + Handles the finalization of complex scripts, particularly multisig + scripts, by properly ordering signatures according to the public + keys in the script. + + Parameters + ---------- + psbt_input : PSBTInput + The PSBT input containing partial signatures + script : Script + The script to finalize against + is_witness : bool + Whether this is a witness script (affects output format) + + Returns + ------- + bool + True if finalization was successful + + Notes + ----- + For multisig scripts, signatures must be provided in the same order + as their corresponding public keys appear in the script. """ script_ops = script.script if hasattr(script, "script") else [] @@ -896,7 +1349,21 @@ def _finalize_script(self, psbt_input: PSBTInput, script: Script, is_witness: bo return False def _parse_global_section(self, stream: BytesIO) -> None: - """Parse the global section of a PSBT.""" + """Parses the global section of a PSBT from a byte stream. + + Reads and processes global key-value pairs including the unsigned + transaction, extended public keys, version, and proprietary data. + + Parameters + ---------- + stream : BytesIO + The byte stream positioned at the start of the global section + + Raises + ------ + ValueError + If required fields are missing or malformed + """ while True: # Read key-value pair key_data = self._read_key_value_pair(stream) @@ -921,7 +1388,23 @@ def _parse_global_section(self, stream: BytesIO) -> None: self.unknown[key_data] = value_data def _parse_input_section(self, stream: BytesIO, input_index: int) -> None: - """Parse an input section of a PSBT.""" + """Parses an input section of a PSBT from a byte stream. + + Reads and processes all key-value pairs for a specific input including + UTXOs, partial signatures, scripts, and derivation paths. + + Parameters + ---------- + stream : BytesIO + The byte stream positioned at the start of the input section + input_index : int + The index of the input being parsed + + Raises + ------ + ValueError + If the stream ends unexpectedly or data is malformed + """ psbt_input = self.inputs[input_index] while True: @@ -973,7 +1456,23 @@ def _parse_input_section(self, stream: BytesIO, input_index: int) -> None: psbt_input.unknown[key_data] = value_data def _parse_output_section(self, stream: BytesIO, output_index: int) -> None: - """Parse an output section of a PSBT.""" + """Parses an output section of a PSBT from a byte stream. + + Reads and processes all key-value pairs for a specific output including + scripts and derivation paths. + + Parameters + ---------- + stream : BytesIO + The byte stream positioned at the start of the output section + output_index : int + The index of the output being parsed + + Raises + ------ + ValueError + If the stream ends unexpectedly or data is malformed + """ psbt_output = self.outputs[output_index] while True: @@ -997,7 +1496,16 @@ def _parse_output_section(self, stream: BytesIO, output_index: int) -> None: psbt_output.unknown[key_data] = value_data def _serialize_global_section(self, result: BytesIO) -> None: - """Serialize the global section of a PSBT.""" + """Serializes the global section of a PSBT to a byte stream. + + Writes all global key-value pairs including the unsigned transaction, + extended public keys, version, and proprietary data. + + Parameters + ---------- + result : BytesIO + The byte stream to write the serialized data to + """ # Unsigned transaction (required) if self.tx: @@ -1029,11 +1537,23 @@ def _serialize_global_section(self, result: BytesIO) -> None: result.write(b'\x00') def _read_key_value_pair(self, stream: BytesIO) -> Optional[Tuple[int, bytes, bytes]]: - """ - Read a key-value pair from the stream. - - Returns: - Tuple of (key_type, key_data, value_data) or None if separator found + """Reads a single key-value pair from a PSBT byte stream. + + Parameters + ---------- + stream : BytesIO + The byte stream to read from + + Returns + ------- + Optional[Tuple[int, bytes, bytes]] + Tuple of (key_type, key_data, value_data) or None if a separator + (0x00) is encountered + + Raises + ------ + ValueError + If the stream ends unexpectedly while reading """ # Read key length key_len_bytes = stream.read(1) @@ -1068,7 +1588,18 @@ def _read_key_value_pair(self, stream: BytesIO) -> Optional[Tuple[int, bytes, by return key_type, key_data, value def _serialize_input_section(self, result: BytesIO, input_index: int) -> None: - """Serialize an input section.""" + """Serializes an input section of a PSBT to a byte stream. + + Writes all key-value pairs for a specific input including UTXOs, + partial signatures, scripts, derivation paths, and finalized scripts. + + Parameters + ---------- + result : BytesIO + The byte stream to write the serialized data to + input_index : int + The index of the input to serialize + """ psbt_input = self.inputs[input_index] # Ensure scripts are Script objects, not bytes @@ -1181,7 +1712,19 @@ def _serialize_input_section(self, result: BytesIO, input_index: int) -> None: result.write(b'\x00') def _serialize_output_section(self, result: BytesIO, output_index: int) -> None: - """Serialize an output section.""" + """Serializes an output section of a PSBT to a byte stream. + + Writes all key-value pairs for a specific output including scripts + and derivation paths. + + Parameters + ---------- + result : BytesIO + The byte stream to write the serialized data to + output_index : int + The index of the output to serialize + """ + psbt_output = self.outputs[output_index] # Ensure scripts are Script objects, not bytes @@ -1220,7 +1763,22 @@ def _serialize_output_section(self, result: BytesIO, output_index: int) -> None: result.write(b'\x00') def _write_key_value_pair(self, result: BytesIO, key_type: int, key_data: bytes, value_data: bytes) -> None: - """Write a key-value pair to the stream.""" + """Writes a key-value pair to a PSBT byte stream. + + Formats and writes a single key-value pair according to the PSBT + specification, including proper length encoding. + + Parameters + ---------- + result : BytesIO + The byte stream to write to + key_type : int + The type identifier for this key-value pair + key_data : bytes + Additional key data (may be empty) + value_data : bytes + The value data to write + """ key = bytes([key_type]) + key_data result.write(encode_varint(len(key))) result.write(key) From b665df8f9cd5a644bc749d646e828e27c094302d Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Thu, 24 Jul 2025 12:22:10 +0530 Subject: [PATCH 10/12] enhanced_files --- bitcoinutils/psbt.py | 836 ++++++++----------------------- bitcoinutils/script.py | 241 +++++---- bitcoinutils/transactions.py | 33 +- bitcoinutils/utils.py | 21 +- examples/create_psbt_multisig.py | 215 ++++---- examples/finalize_psbt.py | 245 ++++++--- examples/sign_psbt.py | 135 +++-- 7 files changed, 768 insertions(+), 958 deletions(-) diff --git a/bitcoinutils/psbt.py b/bitcoinutils/psbt.py index 88e2c55b..36eec371 100644 --- a/bitcoinutils/psbt.py +++ b/bitcoinutils/psbt.py @@ -1,26 +1,3 @@ -""" -Partially Signed Bitcoin Transaction (PSBT) implementation following -BIP-174. - -This module provides the PSBT class which represents a partially signed -bitcoin transaction that can be shared between multiple parties for signing -before broadcasting to the network. - -A PSBT contains: -- The unsigned transaction -- Input metadata needed for signing (UTXOs, scripts, keys, etc.) -- Output metadata for validation -- Partial signatures from different signers - -The PSBT workflow typically involves: -1. Creator: Creates the PSBT with the unsigned transaction -2. Updater: Adds input/output metadata needed for signing -3. Signer: Signs inputs they can sign (sign_input() handles all script types automatically) -4. Combiner: Combines multiple PSBTs with different signatures -5. Finalizer: Finalizes the PSBT by adding final scriptSig/witness -6. Extractor: Extracts the final signed transaction -""" - import struct from io import BytesIO import ecdsa @@ -34,28 +11,6 @@ from bitcoinutils.utils import read_varint class PSBTInput: - """Represents a single input in a PSBT with all associated metadata. - - This class holds all the data associated with a single input in a PSBT, - including UTXO information, partial signatures, scripts, and derivation paths. - - Attributes: - non_witness_utxo: The complete previous transaction for non-witness inputs - witness_utxo: The previous transaction output for witness inputs - partial_sigs: Dictionary mapping public keys to their signatures - sighash_type: The signature hash type to use for this input - redeem_script: The redeem script for P2SH inputs - witness_script: The witness script for P2WSH or P2SH-P2WSH inputs - bip32_derivs: Dictionary mapping public keys to their BIP32 derivation paths - final_scriptsig: The finalized scriptSig for this input - final_scriptwitness: The finalized witness stack for this input - ripemd160_preimages: Preimages for RIPEMD160 hashes - sha256_preimages: Preimages for SHA256 hashes - hash160_preimages: Preimages for HASH160 hashes - hash256_preimages: Preimages for HASH256 hashes - proprietary: Proprietary key-value pairs - unknown: Unknown key-value pairs - """ def __init__(self): # BIP-174 defined fields @@ -80,18 +35,6 @@ def __init__(self): self.unknown: Dict[bytes, bytes] = {} class PSBTOutput: - """Represents a single output in a PSBT with all associated metadata. - - This class holds all the data associated with a single output in a PSBT, - including scripts and derivation paths. - - Attributes: - redeem_script: The redeem script for P2SH outputs - witness_script: The witness script for P2WSH or P2SH-P2WSH outputs - bip32_derivs: Dictionary mapping public keys to their BIP32 derivation paths - proprietary: Proprietary key-value pairs - unknown: Unknown key-value pairs - """ def __init__(self): # BIP-174 defined fields @@ -104,39 +47,18 @@ def __init__(self): self.unknown: Dict[bytes, bytes] = {} class PSBT: - """Bitcoin Partially Signed Bitcoin Transaction (PSBT) implementation as per BIP-174. - - This class provides a complete implementation of the PSBT format, allowing for - the creation, parsing, signing, combining, and finalization of PSBTs. - - PSBTs are useful for constructing transactions in a collaborative manner, - where multiple parties may need to provide signatures or where signing - happens on offline/hardware devices. - - Attributes: - tx: The unsigned transaction - inputs: List of PSBTInput objects containing input-specific data - outputs: List of PSBTOutput objects containing output-specific data - version: PSBT version number - xpubs: Dictionary mapping extended public keys to derivation paths - proprietary: Global proprietary key-value pairs - unknown: Global unknown key-value pairs - """ - # PSBT magic bytes and version MAGIC = b'psbt' VERSION = 0 # Key types as defined in BIP-174 class GlobalTypes: - """Global key types for PSBT as defined in BIP-174.""" UNSIGNED_TX = 0x00 XPUB = 0x01 VERSION = 0xFB PROPRIETARY = 0xFC class InputTypes: - """Input key types for PSBT as defined in BIP-174.""" NON_WITNESS_UTXO = 0x00 WITNESS_UTXO = 0x01 PARTIAL_SIG = 0x02 @@ -153,24 +75,12 @@ class InputTypes: PROPRIETARY = 0xFC class OutputTypes: - """Output key types for PSBT as defined in BIP-174.""" REDEEM_SCRIPT = 0x00 WITNESS_SCRIPT = 0x01 BIP32_DERIVATION = 0x02 PROPRIETARY = 0xFC def _safe_to_bytes(self, obj): - """Safely convert various object types to bytes. - - Args: - obj: Object to convert to bytes - - Returns: - bytes: The object converted to bytes - - Raises: - TypeError: If the object cannot be converted to bytes - """ if isinstance(obj, Script): return obj.to_bytes() elif hasattr(obj, 'to_bytes'): @@ -184,14 +94,7 @@ def _safe_to_bytes(self, obj): def _safe_serialize_transaction(self, tx) -> bytes: - """Safely serialize a transaction to bytes. - - Args: - tx: Transaction to serialize - - Returns: - bytes: Serialized transaction - """ + """Safely serialize a transaction to bytes.""" if isinstance(tx, bytes): return tx @@ -201,12 +104,6 @@ def _safe_serialize_transaction(self, tx) -> bytes: return serialized def __init__(self, unsigned_tx: Optional[Transaction] = None): - """ - Initialize a new PSBT. - - Args: - unsigned_tx: The unsigned transaction. If None, an empty transaction is created. - """ if unsigned_tx is None: # Create empty transaction self.tx = Transaction([], []) @@ -232,34 +129,12 @@ def __init__(self, unsigned_tx: Optional[Transaction] = None): @classmethod def from_base64(cls, psbt_str: str) -> 'PSBT': - """Create a PSBT from a base64-encoded string. - - Args: - psbt_str: Base64-encoded PSBT string - - Returns: - PSBT: Decoded PSBT object - - Raises: - ValueError: If the string is not a valid base64-encoded PSBT - """ import base64 psbt_bytes = base64.b64decode(psbt_str) return cls.from_bytes(psbt_bytes) @classmethod def from_bytes(cls, psbt_bytes: bytes) -> 'PSBT': - """Create a PSBT from raw bytes. - - Args: - psbt_bytes: Raw PSBT bytes - - Returns: - PSBT: Parsed PSBT object - - Raises: - ValueError: If the bytes do not represent a valid PSBT - """ stream = BytesIO(psbt_bytes) # Read and verify magic @@ -287,20 +162,10 @@ def from_bytes(cls, psbt_bytes: bytes) -> 'PSBT': return psbt def to_base64(self) -> str: - """Encode the PSBT as a base64 string. - - Returns: - str: Base64-encoded PSBT - """ import base64 return base64.b64encode(self.to_bytes()).decode('ascii') def to_bytes(self) -> bytes: - """Serialize the PSBT to raw bytes. - - Returns: - bytes: Serialized PSBT - """ result = BytesIO() # Write magic and separator @@ -321,13 +186,6 @@ def to_bytes(self) -> bytes: return result.getvalue() def add_input(self, tx_input: TxInput, psbt_input: Optional[PSBTInput] = None) -> None: - """Add an input to the PSBT. - - Args: - tx_input: Transaction input to add - psbt_input: Optional PSBTInput with metadata. If not provided, - an empty PSBTInput will be created. - """ # Create clean input without scriptSig clean_input = TxInput(tx_input.txid, tx_input.txout_index) self.tx.inputs.append(clean_input) @@ -337,13 +195,6 @@ def add_input(self, tx_input: TxInput, psbt_input: Optional[PSBTInput] = None) - self.inputs.append(psbt_input) def add_output(self, tx_output: TxOutput, psbt_output: Optional[PSBTOutput] = None) -> None: - """Add an output to the PSBT. - - Args: - tx_output: Transaction output to add - psbt_output: Optional PSBTOutput with metadata. If not provided, - an empty PSBTOutput will be created. - """ self.tx.outputs.append(tx_output) if psbt_output is None: @@ -351,35 +202,9 @@ def add_output(self, tx_output: TxOutput, psbt_output: Optional[PSBTOutput] = No self.outputs.append(psbt_output) def sign(self, private_key: PrivateKey, input_index: int, sighash_type: int = 1) -> bool: - """Sign a specific input in the PSBT. - - This is a convenience method that calls sign_input. - - Args: - private_key: Private key to sign with - input_index: Index of the input to sign - sighash_type: Signature hash type (default: SIGHASH_ALL = 1) - - Returns: - bool: True if signing was successful, False otherwise - """ return self.sign_input(input_index, private_key, sighash_type) def sign_input(self, input_index: int, private_key: PrivateKey, sighash_type: int = 1) -> bool: - """Sign a specific input in the PSBT. - - This method adds a partial signature for the specified input using the - provided private key. It automatically detects the script type and - handles both legacy and SegWit inputs. - - Args: - input_index: Index of the input to sign - private_key: Private key to sign with - sighash_type: Signature hash type (default: SIGHASH_ALL = 1) - - Returns: - bool: True if signing was successful, False otherwise - """ try: psbt_input = self.inputs[input_index] @@ -390,6 +215,7 @@ def sign_input(self, input_index: int, private_key: PrivateKey, sighash_type: in if not is_segwit: prev_tx = psbt_input.non_witness_utxo if prev_tx is None: + print(f"Input {input_index} missing both witness_utxo and non_witness_utxo; cannot sign.") return False prev_txout = prev_tx.outputs[self.tx.inputs[input_index].tx_out_index] @@ -428,37 +254,31 @@ def sign_input(self, input_index: int, private_key: PrivateKey, sighash_type: in # Create DER signature + sighash type byte sig = sk.sign_digest(sighash, sigencode=ecdsa.util.sigencode_der_canonize) + bytes([sighash_type]) - # Get compressed pubkey bytes + # Get compressed pubkey bytes - MUST include the prefix byte pubkey_obj = private_key.get_public_key() - if hasattr(pubkey_obj, 'to_bytes'): - pubkey_bytes = pubkey_obj.to_bytes() - else: - pubkey_bytes = pubkey_obj.key.to_string('compressed') - # Add to partial_sigs + # The to_hex() method should give us the full compressed pubkey with prefix + pubkey_hex = pubkey_obj.to_hex() + pubkey_bytes = bytes.fromhex(pubkey_hex) + + # Verify we have a proper compressed pubkey (33 bytes starting with 02 or 03) + if len(pubkey_bytes) != 33 or pubkey_bytes[0] not in [0x02, 0x03]: + raise ValueError(f"Invalid compressed public key: {pubkey_hex}") + + # Ensure we store only the compressed pubkey (33 bytes) + if len(pubkey_bytes) > 33: + pubkey_bytes = pubkey_bytes[:33] psbt_input.partial_sigs[pubkey_bytes] = sig return True except Exception as e: + print(f"Error in sign_input: {e}") import traceback traceback.print_exc() return False def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, sighash_type: int) -> bytes: - """Get a signature for a specific input. - - This internal method generates a signature for the specified input, - handling different script types appropriately. - - Args: - input_index: Index of the input to sign - private_key: Private key to sign with - sighash_type: Signature hash type - - Returns: - bytes: The signature, or None if signing failed - """ input_data = self.inputs[input_index] tx_input = self.tx.inputs[input_index] @@ -519,6 +339,11 @@ def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, si witness_script = input_data.witness_script if input_data.witness_utxo: amount = input_data.witness_utxo.amount + print(f"Input Index: {input_index}") + print(f"Witness Script: {witness_script.to_hex()}") + print(f"Amount: {amount}") + print(f"PubKey: {private_key.get_public_key().to_hex()}") + return private_key.sign_segwit_input( self.tx, input_index, witness_script, amount, sighash_type ) @@ -527,6 +352,9 @@ def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, si # P2SH-P2WPKH (Script Hash wrapping Witness PubKey Hash) if input_data.witness_utxo: amount = input_data.witness_utxo.amount + print(f"Input Index: {input_index}") + print(f"Amount: {amount}") + # For P2WPKH, we need the P2PKH script of the public key p2pkh_script = private_key.get_public_key().get_address().to_script_pub_key() return private_key.sign_segwit_input( @@ -535,6 +363,9 @@ def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, si else: # Regular P2SH (Script Hash) + print(f"Input Index: {input_index}") + print(f"Redeem Script: {redeem_script.to_hex()}") + return private_key.sign_input( self.tx, input_index, redeem_script, sighash_type ) @@ -544,6 +375,10 @@ def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, si witness_script = input_data.witness_script if input_data.witness_utxo: amount = input_data.witness_utxo.amount + print(f"Input Index: {input_index}") + print(f"Witness Script: {witness_script.to_hex()}") + print(f"Amount: {amount}") + return private_key.sign_segwit_input( self.tx, input_index, witness_script, amount, sighash_type ) @@ -555,6 +390,9 @@ def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, si if self._is_p2wpkh_script(script_pubkey): # P2WPKH (Witness PubKey Hash) + print(f"Input Index: {input_index}") + print(f"Amount: {amount}") + # For P2WPKH, we sign with the P2PKH script of our public key p2pkh_script = private_key.get_public_key().get_address().to_script_pub_key() return private_key.sign_segwit_input( @@ -563,6 +401,9 @@ def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, si elif self._is_p2tr_script(script_pubkey): # P2TR (Taproot) + print(f"Input Index: {input_index}") + print(f"Amount: {amount}") + return private_key.sign_taproot_input( self.tx, input_index, amount, sighash_type ) @@ -574,9 +415,28 @@ def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, si if self._is_p2pkh_script(script_pubkey): # P2PKH (Pay to PubKey Hash) + print(f"Input Index: {input_index}") + print(f"Script PubKey: {script_pubkey.to_hex()}") + return private_key.sign_input( self.tx, input_index, script_pubkey, sighash_type ) + + print(f"⚠️ No suitable script type matched for input {input_index}") + + # Debug information + print(f"Debug info:") + print(f" - Has redeem_script: {bool(input_data.redeem_script)}") + print(f" - Has witness_script: {bool(input_data.witness_script)}") + print(f" - Has witness_utxo: {bool(input_data.witness_utxo)}") + print(f" - Has non_witness_utxo: {bool(input_data.non_witness_utxo)}") + + if input_data.witness_utxo: + spk = input_data.witness_utxo.script_pubkey + print(f" - Script pubkey type checks:") + print(f" - is_p2wpkh: {self._is_p2wpkh_script(spk)}") + print(f" - is_p2tr: {self._is_p2tr_script(spk)}") + print(f" - Script hex: {spk.to_hex() if hasattr(spk, 'to_hex') else 'N/A'}") return None @@ -587,42 +447,21 @@ def _get_signature_for_input(self, input_index: int, private_key: PrivateKey, si def _is_p2pkh_script(self, script) -> bool: - """Check if script is P2PKH (OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG). - - Args: - script: Script to check - - Returns: - bool: True if script is P2PKH, False otherwise - """ + """Check if script is P2PKH (OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG).""" try: return script.is_p2pkh() if hasattr(script, 'is_p2pkh') else False except: return False def _is_p2wpkh_script(self, script) -> bool: - """Check if script is P2WPKH (OP_0 <20-byte-pubkeyhash>). - - Args: - script: Script to check - - Returns: - bool: True if script is P2WPKH, False otherwise - """ + """Check if script is P2WPKH (OP_0 <20-byte-pubkeyhash>).""" try: return script.is_p2wpkh() if hasattr(script, 'is_p2wpkh') else False except: return False def _is_p2tr_script(self, script_pubkey: Script) -> bool: - """Check if script is P2TR (Taproot). - - Args: - script_pubkey: Script to check - - Returns: - bool: True if script is P2TR, False otherwise - """ + """Check if script is P2TR (Taproot).""" if not hasattr(script_pubkey, 'script'): return False @@ -634,43 +473,18 @@ def _is_p2tr_script(self, script_pubkey: Script) -> bool: len(script_ops[1]) == 32) def _is_input_finalized(self, psbt_input: PSBTInput) -> bool: - """Check if an input is already finalized. - - Args: - psbt_input: Input to check - - Returns: - bool: True if input is finalized, False otherwise - """ + """Check if an input is already finalized.""" return (psbt_input.final_scriptsig is not None or psbt_input.final_scriptwitness is not None) def _apply_final_fields(self, tx_input: TxInput, input_data: PSBTInput) -> None: - """Apply finalized fields to a transaction input. - - Args: - tx_input: Transaction input to update - input_data: PSBT input data containing finalized fields - """ if input_data.final_scriptsig: tx_input.script_sig = input_data.final_scriptsig else: tx_input.script_sig = Script([]) def _validate_final_tx(self, final_tx) -> dict: - """Validate the finalized transaction. - - Args: - final_tx: Transaction to validate - - Returns: - dict: Validation information including: - - valid: Whether the transaction is valid - - errors: List of error messages - - warnings: List of warning messages - - size: Transaction size in bytes - - vsize: Virtual size for fee calculation - """ + """Validate the finalized transaction.""" validation_info = { 'valid': True, 'errors': [], @@ -729,14 +543,7 @@ def _validate_final_tx(self, final_tx) -> dict: return validation_info def _serialize_transaction_without_witness(self, tx) -> bytes: - """Serialize transaction without witness data for vsize calculation. - - Args: - tx: Transaction to serialize - - Returns: - bytes: Serialized transaction without witness data - """ + """Serialize transaction without witness data for vsize calculation.""" try: # Create a copy of the transaction without witness data from bitcoinutils.transactions import Transaction @@ -775,21 +582,6 @@ def _serialize_transaction_without_witness(self, tx) -> bytes: return serialized def combine(self, other: 'PSBT') -> 'PSBT': - """Combine this PSBT with another PSBT. - - PSBTs can be combined when they represent the same unsigned transaction. - The resulting PSBT will contain all the data from both PSBTs, with data - from 'other' taking precedence in case of conflicts. - - Args: - other: Another PSBT to combine with this one - - Returns: - PSBT: A new PSBT containing combined data from both PSBTs - - Raises: - ValueError: If the PSBTs have different unsigned transactions - """ # Ensure both PSBTs have the same unsigned transaction if self.tx.serialize() != other.tx.serialize(): raise ValueError("Cannot combine PSBTs with different transactions") @@ -866,76 +658,19 @@ def combine(self, other: 'PSBT') -> 'PSBT': return combined def combine_psbts(self, other_psbts: List['PSBT']) -> 'PSBT': - """Combines this PSBT with multiple other PSBTs. - - Wraps the pairwise `combine()` method in a loop for batch combining. - All PSBTs must have the same unsigned transaction. - - Parameters - ---------- - other_psbts : List[PSBT] - A list of PSBTs to combine with this one - - Returns - ------- - PSBT - The final combined PSBT containing all partial signatures and - metadata from all input PSBTs - - Raises - ------ - ValueError - If any PSBT has a different unsigned transaction - """ combined = self for other in other_psbts: combined = combined.combine(other) return combined - def finalize(self, validate: bool = False) -> Union[Transaction, Tuple[Transaction, Dict]]: - """Finalizes all inputs and creates the final broadcastable transaction. - - Attempts to finalize all inputs by converting partial signatures into - final scriptSig/scriptWitness fields. If successful, produces a fully - signed transaction ready for broadcast. - - Parameters - ---------- - validate : bool, optional - If True, validates the final transaction and returns validation - info along with the transaction (default: False) - - Returns - ------- - Transaction or Tuple[Transaction, Dict] - If validate=False: Transaction object ready for broadcast, or None - if not all inputs could be finalized - If validate=True: Tuple of (Transaction, validation_info dict) - containing the transaction and validation details - - Raises - ------ - ValueError - If not all inputs can be finalized when validate=True - - Notes - ----- - The validation_info dict contains: - - 'valid': bool indicating if transaction is valid - - 'errors': List of error messages - - 'warnings': List of warning messages - - 'txid': Transaction ID if computable - - 'size': Transaction size in bytes - - 'vsize': Virtual size for fee calculation - """ - + def finalize(self, validate: bool = False) -> Union[Transaction, Tuple[Transaction, Dict], bool]: # Count successfully finalized inputs finalized_count = 0 for i in range(len(self.inputs)): if self._finalize_input(i): finalized_count += 1 - # If not all inputs could be finalized, return None + # If not all inputs could be finalized, return False if finalized_count != len(self.inputs): if validate: # Return a validation dict with error info @@ -947,7 +682,7 @@ def finalize(self, validate: bool = False) -> Union[Transaction, Tuple[Transacti # Return a dummy transaction and validation info return self.tx, validation_info else: - return None + return False # All inputs finalized - build final transaction final_inputs = [] @@ -988,26 +723,6 @@ def finalize(self, validate: bool = False) -> Union[Transaction, Tuple[Transacti return final_tx def finalize_input(self, input_index: int) -> bool: - """Finalizes a specific input. - - Converts partial signatures for a single input into final - scriptSig/scriptWitness fields. - - Parameters - ---------- - input_index : int - The index of the input to finalize - - Returns - ------- - bool - True if the input was successfully finalized, False otherwise - - Raises - ------ - ValueError - If input_index is out of range - """ if input_index >= len(self.inputs): raise ValueError(f"Input index {input_index} out of range") @@ -1015,21 +730,6 @@ def finalize_input(self, input_index: int) -> bool: return self._finalize_input(input_index) def _finalize_input(self, input_index: int) -> bool: - """Internal method for finalizing a single input. - - Handles the actual finalization logic for different script types - including P2PKH, P2WPKH, P2SH, P2WSH, and P2TR. - - Parameters - ---------- - input_index : int - The index of the input to finalize - - Returns - ------- - bool - True if the input was successfully finalized - """ psbt_input = self.inputs[input_index] @@ -1070,19 +770,7 @@ def _finalize_input(self, input_index: int) -> bool: return False def _finalize_p2pkh(self, psbt_input: PSBTInput) -> bool: - """Finalizes a P2PKH (Pay-to-PubKey-Hash) input. - - Parameters - ---------- - psbt_input : PSBTInput - The PSBT input to finalize - - Returns - ------- - bool - True if finalization was successful - """ - + """Finalize P2PKH input.""" if len(psbt_input.partial_sigs) != 1: return False @@ -1091,19 +779,7 @@ def _finalize_p2pkh(self, psbt_input: PSBTInput) -> bool: return True def _finalize_p2wpkh(self, psbt_input: PSBTInput) -> bool: - """Finalizes a P2WPKH (Pay-to-Witness-PubKey-Hash) input. - - Parameters - ---------- - psbt_input : PSBTInput - The PSBT input to finalize - - Returns - ------- - bool - True if finalization was successful - """ - + """Finalize P2WPKH input.""" if len(psbt_input.partial_sigs) != 1: return False @@ -1113,20 +789,7 @@ def _finalize_p2wpkh(self, psbt_input: PSBTInput) -> bool: return True def _finalize_p2sh(self, psbt_input: PSBTInput) -> bool: - """Finalizes a P2SH (Pay-to-Script-Hash) input. - - Handles both regular P2SH and P2SH-wrapped SegWit scripts. - - Parameters - ---------- - psbt_input : PSBTInput - The PSBT input to finalize - - Returns - ------- - bool - True if finalization was successful - """ + """Finalize P2SH input with support for nested SegWit.""" if not psbt_input.redeem_script: return False @@ -1147,18 +810,7 @@ def _finalize_p2sh(self, psbt_input: PSBTInput) -> bool: return success def _finalize_p2sh_p2wpkh(self, psbt_input: PSBTInput) -> bool: - """Finalizes a P2SH-wrapped P2WPKH input. - - Parameters - ---------- - psbt_input : PSBTInput - The PSBT input to finalize - - Returns - ------- - bool - True if finalization was successful - """ + """Finalize P2SH-wrapped P2WPKH input.""" if len(psbt_input.partial_sigs) != 1: return False @@ -1175,18 +827,7 @@ def _finalize_p2sh_p2wpkh(self, psbt_input: PSBTInput) -> bool: return True def _finalize_p2sh_p2wsh(self, psbt_input: PSBTInput) -> bool: - """Finalizes a P2SH-wrapped P2WSH input. - - Parameters - ---------- - psbt_input : PSBTInput - The PSBT input to finalize - - Returns - ------- - bool - True if finalization was successful - """ + """Finalize P2SH-wrapped P2WSH input.""" if not psbt_input.witness_script: return False @@ -1202,38 +843,74 @@ def _finalize_p2sh_p2wsh(self, psbt_input: PSBTInput) -> bool: return success def _finalize_p2wsh(self, psbt_input: PSBTInput) -> bool: - """Finalizes a P2WSH (Pay-to-Witness-Script-Hash) input. - - Parameters - ---------- - psbt_input : PSBTInput - The PSBT input to finalize - - Returns - ------- - bool - True if finalization was successful - """ + """Finalize P2WSH input.""" if not psbt_input.witness_script: return False - return self._finalize_script(psbt_input, psbt_input.witness_script, is_witness=True) - - def _finalize_p2tr(self, psbt_input: PSBTInput) -> bool: - """Finalizes a P2TR (Pay-to-Taproot) input. - - Currently supports only key-path spending. + witness_script = psbt_input.witness_script + script_ops = witness_script.script - Parameters - ---------- - psbt_input : PSBTInput - The PSBT input to finalize + # Check if it's a multisig script + if (len(script_ops) >= 4 and + script_ops[0] in ['OP_1', 'OP_2', 'OP_3'] and + script_ops[-1] == 'OP_CHECKMULTISIG'): + + # Extract m value + if script_ops[0] == 'OP_1': + m = 1 + elif script_ops[0] == 'OP_2': + m = 2 + elif script_ops[0] == 'OP_3': + m = 3 + else: + return False + + # Extract pubkeys from the script + pubkeys_in_script = [] + for i in range(1, len(script_ops) - 2): + op = script_ops[i] + if isinstance(op, str) and not op.startswith('OP_'): + pubkeys_in_script.append(op) + + # Match signatures to pubkeys in script order + ordered_sigs = [] + for script_pubkey in pubkeys_in_script: + for partial_pubkey, sig in psbt_input.partial_sigs.items(): + # Convert to hex for comparison + partial_pubkey_hex = partial_pubkey.hex() if isinstance(partial_pubkey, bytes) else partial_pubkey + + # Compare the compressed pubkey (33 bytes = 66 hex chars) + if partial_pubkey_hex[:66] == script_pubkey: + ordered_sigs.append(sig) + break + + if len(ordered_sigs) < m: + return False + + # Build witness stack: [OP_0, sig1, sig2, ..., sigM, witness_script] + psbt_input.final_scriptsig = Script([]) + psbt_input.final_scriptwitness = [] + + # Add OP_0 (empty element) - required for CHECKMULTISIG bug + psbt_input.final_scriptwitness.append(b'') + + # Add the first m signatures in correct order + for sig in ordered_sigs[:m]: + if isinstance(sig, bytes): + psbt_input.final_scriptwitness.append(sig) + else: + psbt_input.final_scriptwitness.append(bytes.fromhex(sig)) + + # Add the witness script as bytes + psbt_input.final_scriptwitness.append(witness_script.to_bytes()) + + return True + else: + # Non-multisig P2WSH + return self._finalize_script(psbt_input, witness_script, is_witness=True) - Returns - ------- - bool - True if finalization was successful - """ + def _finalize_p2tr(self, psbt_input: PSBTInput) -> bool: + """Finalize P2TR (Taproot) input.""" if len(psbt_input.partial_sigs) != 1: return False @@ -1244,30 +921,8 @@ def _finalize_p2tr(self, psbt_input: PSBTInput) -> bool: return True def _finalize_script(self, psbt_input: PSBTInput, script: Script, is_witness: bool) -> bool: - """Finalizes a script with enhanced multisig support. - - Handles the finalization of complex scripts, particularly multisig - scripts, by properly ordering signatures according to the public - keys in the script. - - Parameters - ---------- - psbt_input : PSBTInput - The PSBT input containing partial signatures - script : Script - The script to finalize against - is_witness : bool - Whether this is a witness script (affects output format) - - Returns - ------- - bool - True if finalization was successful - - Notes - ----- - For multisig scripts, signatures must be provided in the same order - as their corresponding public keys appear in the script. + """ + Finalize a script by constructing the appropriate scriptSig or witness. """ script_ops = script.script if hasattr(script, "script") else [] @@ -1281,42 +936,46 @@ def _finalize_script(self, psbt_input: PSBTInput, script: Script, is_witness: bo n = 3 # From OP_3 # Extract public keys from script (they're between m and n) - pubkeys = [] + pubkeys_in_script = [] for i in range(1, 4): # indices 1, 2, 3 for the three pubkeys if i < len(script_ops): pk = script_ops[i] if isinstance(pk, str): - pubkeys.append(bytes.fromhex(pk)) + pubkeys_in_script.append(bytes.fromhex(pk)) elif isinstance(pk, bytes): - pubkeys.append(pk) + pubkeys_in_script.append(pk) - if len(pubkeys) != n: + if len(pubkeys_in_script) != n: return False - # IMPORTANT: For Bitcoin multisig, we need to match signatures to their pubkeys - # and provide them in the order the pubkeys appear in the script + # Collect signatures in the order of pubkeys in the script ordered_sigs = [] - sig_pubkey_map = {} - - # First, normalize all pubkeys from partial_sigs to bytes - for partial_pubkey, sig in psbt_input.partial_sigs.items(): - if isinstance(partial_pubkey, str): - partial_pubkey_bytes = bytes.fromhex(partial_pubkey) - else: - partial_pubkey_bytes = partial_pubkey - sig_pubkey_map[partial_pubkey_bytes] = sig - # Now collect signatures in script pubkey order - for pubkey in pubkeys: - if pubkey in sig_pubkey_map: - ordered_sigs.append(sig_pubkey_map[pubkey]) + for script_pubkey in pubkeys_in_script: + # Check each pubkey from partial_sigs to see if it matches + for partial_pubkey, sig in psbt_input.partial_sigs.items(): + # Normalize to bytes for comparison + if isinstance(partial_pubkey, str): + partial_pubkey_bytes = bytes.fromhex(partial_pubkey) + else: + partial_pubkey_bytes = partial_pubkey + + # Compare the actual pubkey bytes (not including any extra data) + script_pubkey_hex = script_pubkey.hex() if isinstance(script_pubkey, bytes) else script_pubkey + partial_pubkey_hex = partial_pubkey_bytes.hex() + + # Only compare the first 33 bytes (compressed pubkey length) + if script_pubkey_hex[:66] == partial_pubkey_hex[:66]: + ordered_sigs.append((script_pubkey, sig)) + break # Check if we have enough signatures if len(ordered_sigs) < m: + print(f"Not enough signatures: have {len(ordered_sigs)}, need {m}") return False - # Use only the first m signatures (in case we have more) - signatures_to_use = ordered_sigs[:m] + # Use only the first m signatures + signatures_to_use = [sig for _, sig in ordered_sigs[:m]] # Build the final script if is_witness: @@ -1349,21 +1008,7 @@ def _finalize_script(self, psbt_input: PSBTInput, script: Script, is_witness: bo return False def _parse_global_section(self, stream: BytesIO) -> None: - """Parses the global section of a PSBT from a byte stream. - - Reads and processes global key-value pairs including the unsigned - transaction, extended public keys, version, and proprietary data. - - Parameters - ---------- - stream : BytesIO - The byte stream positioned at the start of the global section - - Raises - ------ - ValueError - If required fields are missing or malformed - """ + """Parse the global section of a PSBT.""" while True: # Read key-value pair key_data = self._read_key_value_pair(stream) @@ -1388,23 +1033,7 @@ def _parse_global_section(self, stream: BytesIO) -> None: self.unknown[key_data] = value_data def _parse_input_section(self, stream: BytesIO, input_index: int) -> None: - """Parses an input section of a PSBT from a byte stream. - - Reads and processes all key-value pairs for a specific input including - UTXOs, partial signatures, scripts, and derivation paths. - - Parameters - ---------- - stream : BytesIO - The byte stream positioned at the start of the input section - input_index : int - The index of the input being parsed - - Raises - ------ - ValueError - If the stream ends unexpectedly or data is malformed - """ + """Parse an input section of a PSBT.""" psbt_input = self.inputs[input_index] while True: @@ -1417,8 +1046,26 @@ def _parse_input_section(self, stream: BytesIO, input_index: int) -> None: if key_type == self.InputTypes.NON_WITNESS_UTXO: psbt_input.non_witness_utxo = Transaction.from_bytes(value_data) elif key_type == self.InputTypes.WITNESS_UTXO: - psbt_input.witness_utxo = TxOutput.from_bytes(value_data) + # Parse witness UTXO manually to ensure correctness + import struct + from bitcoinutils.utils import read_varint + + # Read amount (8 bytes, little-endian) + amount = struct.unpack(' 33: + key_data = key_data[:33] psbt_input.partial_sigs[key_data] = value_data elif key_type == self.InputTypes.SIGHASH_TYPE: psbt_input.sighash_type = struct.unpack(' None: psbt_input.unknown[key_data] = value_data def _parse_output_section(self, stream: BytesIO, output_index: int) -> None: - """Parses an output section of a PSBT from a byte stream. - - Reads and processes all key-value pairs for a specific output including - scripts and derivation paths. - - Parameters - ---------- - stream : BytesIO - The byte stream positioned at the start of the output section - output_index : int - The index of the output being parsed - - Raises - ------ - ValueError - If the stream ends unexpectedly or data is malformed - """ + """Parse an output section of a PSBT.""" psbt_output = self.outputs[output_index] while True: @@ -1496,16 +1127,7 @@ def _parse_output_section(self, stream: BytesIO, output_index: int) -> None: psbt_output.unknown[key_data] = value_data def _serialize_global_section(self, result: BytesIO) -> None: - """Serializes the global section of a PSBT to a byte stream. - - Writes all global key-value pairs including the unsigned transaction, - extended public keys, version, and proprietary data. - - Parameters - ---------- - result : BytesIO - The byte stream to write the serialized data to - """ + """Serialize the global section of a PSBT.""" # Unsigned transaction (required) if self.tx: @@ -1537,23 +1159,11 @@ def _serialize_global_section(self, result: BytesIO) -> None: result.write(b'\x00') def _read_key_value_pair(self, stream: BytesIO) -> Optional[Tuple[int, bytes, bytes]]: - """Reads a single key-value pair from a PSBT byte stream. - - Parameters - ---------- - stream : BytesIO - The byte stream to read from - - Returns - ------- - Optional[Tuple[int, bytes, bytes]] - Tuple of (key_type, key_data, value_data) or None if a separator - (0x00) is encountered - - Raises - ------ - ValueError - If the stream ends unexpectedly while reading + """ + Read a key-value pair from the stream. + + Returns: + Tuple of (key_type, key_data, value_data) or None if separator found """ # Read key length key_len_bytes = stream.read(1) @@ -1588,18 +1198,7 @@ def _read_key_value_pair(self, stream: BytesIO) -> Optional[Tuple[int, bytes, by return key_type, key_data, value def _serialize_input_section(self, result: BytesIO, input_index: int) -> None: - """Serializes an input section of a PSBT to a byte stream. - - Writes all key-value pairs for a specific input including UTXOs, - partial signatures, scripts, derivation paths, and finalized scripts. - - Parameters - ---------- - result : BytesIO - The byte stream to write the serialized data to - input_index : int - The index of the input to serialize - """ + """Serialize an input section.""" psbt_input = self.inputs[input_index] # Ensure scripts are Script objects, not bytes @@ -1624,8 +1223,12 @@ def _serialize_input_section(self, result: BytesIO, input_index: int) -> None: witness_utxo = psbt_input.witness_utxo + # Ensure we have a proper TxOutput object + if not hasattr(witness_utxo, 'amount') or not hasattr(witness_utxo, 'script_pubkey'): + raise ValueError("Invalid witness UTXO structure") + # Serialize amount (8 bytes, little-endian) - amount_bytes = struct.pack(" None: witness_data = amount_bytes + script_len_bytes + script_bytes except Exception as e: - # Fallback - try to use existing method if available - if hasattr(psbt_input.witness_utxo, 'to_bytes'): - witness_data = psbt_input.witness_utxo.to_bytes() - else: - raise + print(f"Error serializing witness UTXO: {e}") + raise self._write_key_value_pair(result, self.InputTypes.WITNESS_UTXO, b'', witness_data) @@ -1678,11 +1278,16 @@ def _serialize_input_section(self, result: BytesIO, input_index: int) -> None: # Final script witness if psbt_input.final_scriptwitness: - witness_data = b''.join( - encode_varint(len(item)) + - (item if isinstance(item, bytes) else self._safe_to_bytes(item)) - for item in psbt_input.final_scriptwitness - ) + witness_data = b'' + for item in psbt_input.final_scriptwitness: + if isinstance(item, str): + # Handle hex strings - empty string means OP_0 + item_bytes = bytes.fromhex(item) if item else b'' + elif isinstance(item, bytes): + item_bytes = item + else: + item_bytes = self._safe_to_bytes(item) + witness_data += encode_varint(len(item_bytes)) + item_bytes self._write_key_value_pair(result, self.InputTypes.FINAL_SCRIPTWITNESS, b'', witness_data) # Hash preimages (these should already be bytes) @@ -1712,19 +1317,7 @@ def _serialize_input_section(self, result: BytesIO, input_index: int) -> None: result.write(b'\x00') def _serialize_output_section(self, result: BytesIO, output_index: int) -> None: - """Serializes an output section of a PSBT to a byte stream. - - Writes all key-value pairs for a specific output including scripts - and derivation paths. - - Parameters - ---------- - result : BytesIO - The byte stream to write the serialized data to - output_index : int - The index of the output to serialize - """ - + """Serialize an output section.""" psbt_output = self.outputs[output_index] # Ensure scripts are Script objects, not bytes @@ -1763,22 +1356,7 @@ def _serialize_output_section(self, result: BytesIO, output_index: int) -> None: result.write(b'\x00') def _write_key_value_pair(self, result: BytesIO, key_type: int, key_data: bytes, value_data: bytes) -> None: - """Writes a key-value pair to a PSBT byte stream. - - Formats and writes a single key-value pair according to the PSBT - specification, including proper length encoding. - - Parameters - ---------- - result : BytesIO - The byte stream to write to - key_type : int - The type identifier for this key-value pair - key_data : bytes - Additional key data (may be empty) - value_data : bytes - The value data to write - """ + """Write a key-value pair to the stream.""" key = bytes([key_type]) + key_data result.write(encode_varint(len(key))) result.write(key) diff --git a/bitcoinutils/script.py b/bitcoinutils/script.py index ef0b6895..d45c08ea 100644 --- a/bitcoinutils/script.py +++ b/bitcoinutils/script.py @@ -9,17 +9,22 @@ # propagated, or distributed except according to the terms contained in the # LICENSE file. + import copy import hashlib import struct from typing import Any, Union + from bitcoinutils.ripemd160 import ripemd160 from bitcoinutils.utils import b_to_h, h_to_b, vi_to_int + # import bitcoinutils.keys + + # Bitcoin's op codes. Complete list at: https://en.bitcoin.it/wiki/Script OP_CODES = { # constants @@ -126,14 +131,15 @@ "OP_CHECKSIGVERIFY": b"\xad", "OP_CHECKMULTISIG": b"\xae", "OP_CHECKMULTISIGVERIFY": b"\xaf", - "OP_CHECKSIGADD": b"\xba", + "OP_CHECKSIGADD": b"\xba", # added this new OPCODE # locktime "OP_NOP2": b"\xb1", "OP_CHECKLOCKTIMEVERIFY": b"\xb1", "OP_NOP3": b"\xb2", "OP_CHECKSEQUENCEVERIFY": b"\xb2", + + } -} CODE_OPS = { # constants @@ -223,78 +229,97 @@ b"\xad": "OP_CHECKSIGVERIFY", b"\xae": "OP_CHECKMULTISIG", b"\xaf": "OP_CHECKMULTISIGVERIFY", - b"\xba": "OP_CHECKSIGADD", + b"\xba": "OP_CHECKSIGADD", # added this new OPCODE # locktime # This used to be OP_NOP2 - b"\xb1": "OP_CHECKLOCKTIMEVERIFY", + b"\xb1": "OP_CHECKLOCKTIMEVERIFY", # This used to be OP_NOP3 - b"\xb2": "OP_CHECKSEQUENCEVERIFY", -} + b"\xb2": "OP_CHECKSEQUENCEVERIFY", + } + + class Script: """Represents any script in Bitcoin + A Script contains just a list of OP_CODES and also knows how to serialize into bytes + Attributes ---------- script : list the list with all the script OP_CODES and data + Methods ------- to_bytes() returns a serialized byte version of the script + to_hex() returns a serialized version of the script in hex + get_script() returns the list of strings that makes up this script + copy() creates a copy of the object (classmethod) + from_raw() + to_p2sh_script_pub_key() converts script to p2sh scriptPubKey (locking script) + to_p2wsh_script_pub_key() converts script to p2wsh scriptPubKey (locking script) + is_p2wpkh() checks if script is P2WPKH (Pay-to-Witness-Public-Key-Hash) + is_p2tr() checks if script is P2TR (Pay-to-Taproot) + is_p2wsh() checks if script is P2WSH (Pay-to-Witness-Script-Hash) + is_p2sh() checks if script is P2SH (Pay-to-Script-Hash) + is_p2pkh() checks if script is P2PKH (Pay-to-Public-Key-Hash) + is_multisig() checks if script is a multisig script + get_script_type() determines the type of script + Raises ------ ValueError If string data is too large or integer is negative """ + def __init__(self, script: list[Any]): """See Script description""" - self.script: list[Any] = script @@ -306,51 +331,51 @@ def copy(cls, script: "Script") -> "Script": def _op_push_data(self, data): - """Converts data to appropriate OP_PUSHDATA OP code including length""" - - # Handle bytes directly - if isinstance(data, bytes): - data_bytes = data - # Handle hex strings - elif isinstance(data, str): - try: - # Check if it's a valid hex string - if len(data) % 2 == 0 and all(c in '0123456789abcdefABCDEF' for c in data): - data_bytes = bytes.fromhex(data) - else: + """Converts data to appropriate OP_PUSHDATA OP code including length""" + + # Handle bytes directly + if isinstance(data, bytes): + data_bytes = data + # Handle hex strings + elif isinstance(data, str): + try: + # Check if it's a valid hex string + if len(data) % 2 == 0 and all(c in '0123456789abcdefABCDEF' for c in data): + data_bytes = bytes.fromhex(data) + else: + data_bytes = data.encode('utf-8') + except ValueError: data_bytes = data.encode('utf-8') - except ValueError: - data_bytes = data.encode('utf-8') - # Handle ints - elif isinstance(data, int): - # Special handling for small integers that might be opcodes - if data == 0: - return b'\x00' # OP_0 - elif 1 <= data <= 16: - return bytes([0x50 + data]) # OP_1 through OP_16 - elif data == 0x51: - return b'\x51' - elif data == 0x52: - return b'\x52' - elif data == 0x53: - return b'\x53' - elif data == 0xae: - return b'\xae' + # Handle ints + elif isinstance(data, int): + # Special handling for small integers that might be opcodes + if data == 0: + return b'\x00' # OP_0 + elif 1 <= data <= 16: + return bytes([0x50 + data]) # OP_1 through OP_16 + elif data == 0x51: + return b'\x51' + elif data == 0x52: + return b'\x52' + elif data == 0x53: + return b'\x53' + elif data == 0xae: + return b'\xae' + else: + # Convert integer to bytes + data_bytes = data.to_bytes((data.bit_length() + 7) // 8 or 1, 'little') else: - # Convert integer to bytes - data_bytes = data.to_bytes((data.bit_length() + 7) // 8 or 1, 'little') - else: - raise TypeError(f"Unsupported data type for push: {type(data)}") - - length = len(data_bytes) - if length < 0x4c: - return bytes([length]) + data_bytes - elif length <= 0xff: - return b'\x4c' + bytes([length]) + data_bytes - elif length <= 0xffff: - return b'\x4d' + length.to_bytes(2, 'little') + data_bytes - else: - return b'\x4e' + length.to_bytes(4, 'little') + data_bytes + raise TypeError(f"Unsupported data type for push: {type(data)}") + + length = len(data_bytes) + if length < 0x4c: + return bytes([length]) + data_bytes + elif length <= 0xff: + return b'\x4c' + bytes([length]) + data_bytes + elif length <= 0xffff: + return b'\x4d' + length.to_bytes(2, 'little') + data_bytes + else: + return b'\x4e' + length.to_bytes(4, 'little') + data_bytes def _push_integer(self, integer: int) -> bytes: @@ -371,40 +396,40 @@ def _push_integer(self, integer: int) -> bytes: def to_bytes(self) -> bytes: - """Converts the script to bytes""" - script_bytes = b"" - for token in self.script: - if isinstance(token, str) and token in OP_CODES: - # It's an opcode string like 'OP_CHECKMULTISIG' - script_bytes += OP_CODES[token] - elif isinstance(token, int): - # Handle integer opcodes directly - if token == 0: - script_bytes += b'\x00' # OP_0 - elif 1 <= token <= 16: - script_bytes += bytes([0x50 + token]) # OP_1 through OP_16 - elif token == 0x51: # OP_1 - script_bytes += b'\x51' - elif token == 0x52: # OP_2 - script_bytes += b'\x52' - elif token == 0x53: # OP_3 - script_bytes += b'\x53' - elif token == 0xae: # OP_CHECKMULTISIG - script_bytes += b'\xae' - elif 0x50 <= token <= 0x60: # Other single-byte opcodes - script_bytes += bytes([token]) + """Converts the script to bytes""" + script_bytes = b"" + for token in self.script: + if isinstance(token, str) and token in OP_CODES: + # It's an opcode string like 'OP_CHECKMULTISIG' + script_bytes += OP_CODES[token] + elif isinstance(token, int): + # Handle integer opcodes directly + if token == 0: + script_bytes += b'\x00' # OP_0 + elif 1 <= token <= 16: + script_bytes += bytes([0x50 + token]) # OP_1 through OP_16 + elif token == 0x51: # OP_1 + script_bytes += b'\x51' + elif token == 0x52: # OP_2 + script_bytes += b'\x52' + elif token == 0x53: # OP_3 + script_bytes += b'\x53' + elif token == 0xae: # OP_CHECKMULTISIG + script_bytes += b'\xae' + elif 0x50 <= token <= 0x60: # Other single-byte opcodes + script_bytes += bytes([token]) + else: + # For other integers, push as data + script_bytes += self._push_integer(token) + elif isinstance(token, bytes): + # Raw bytes - push with length prefix + script_bytes += self._op_push_data(token) + elif isinstance(token, str): + # Assume it's hex data if not an opcode + script_bytes += self._op_push_data(token) else: - # For other integers, push as data - script_bytes += self._push_integer(token) - elif isinstance(token, bytes): - # Raw bytes - push with length prefix - script_bytes += self._op_push_data(token) - elif isinstance(token, str): - # Assume it's hex data if not an opcode - script_bytes += self._op_push_data(token) - else: - raise TypeError(f"Invalid token type in script: {type(token)}") - return script_bytes + raise TypeError(f"Invalid token type in script: {type(token)}") + return script_bytes def to_hex(self) -> str: """Converts the script to hexadecimal""" @@ -412,16 +437,16 @@ def to_hex(self) -> str: @classmethod def from_bytes(cls, byte_data: bytes) -> "Script": - """ - Creates a Script object from raw bytes. + """ + Creates a Script object from raw bytes. - Args: - byte_data (bytes): The raw bytes of the script. + Args: + byte_data (bytes): The raw bytes of the script. - Returns: + Returns: Script: A new Script object parsed from the byte data. - """ - return cls.from_raw(byte_data) + """ + return cls.from_raw(byte_data) @staticmethod @@ -435,7 +460,7 @@ def from_raw(scriptrawhex: Union[str, bytes], has_segwit: bool = False): scriptraw = scriptrawhex else: raise TypeError("Input must be a hexadecimal string or bytes") - + commands = [] index = 0 @@ -503,9 +528,9 @@ def to_p2wsh_script_pub_key(self) -> "Script": def is_p2wpkh(self) -> bool: """ Check if script is P2WPKH (Pay-to-Witness-Public-Key-Hash). - + P2WPKH format: OP_0 <20-byte-key-hash> - + Returns: bool: True if script is P2WPKH """ @@ -519,9 +544,9 @@ def is_p2wpkh(self) -> bool: def is_p2tr(self) -> bool: """ Check if script is P2TR (Pay-to-Taproot). - + P2TR format: OP_1 <32-byte-key> - + Returns: bool: True if script is P2TR """ @@ -535,9 +560,9 @@ def is_p2tr(self) -> bool: def is_p2wsh(self) -> bool: """ Check if script is P2WSH (Pay-to-Witness-Script-Hash). - + P2WSH format: OP_0 <32-byte-script-hash> - + Returns: bool: True if script is P2WSH """ @@ -551,9 +576,9 @@ def is_p2wsh(self) -> bool: def is_p2sh(self) -> bool: """ Check if script is P2SH (Pay-to-Script-Hash). - + P2SH format: OP_HASH160 <20-byte-script-hash> OP_EQUAL - + Returns: bool: True if script is P2SH """ @@ -568,9 +593,9 @@ def is_p2sh(self) -> bool: def is_p2pkh(self) -> bool: """ Check if script is P2PKH (Pay-to-Public-Key-Hash). - + P2PKH format: OP_DUP OP_HASH160 <20-byte-key-hash> OP_EQUALVERIFY OP_CHECKSIG - + Returns: bool: True if script is P2PKH """ @@ -587,33 +612,33 @@ def is_p2pkh(self) -> bool: def is_multisig(self) -> tuple[bool, Union[tuple[int, int], None]]: """ Check if script is a multisig script. - + Multisig format: OP_M ... OP_N OP_CHECKMULTISIG - + Returns: tuple: (bool, (M, N) if multisig, None otherwise) """ ops = self.script - + if (len(ops) >= 4 and isinstance(ops[0], int) and ops[-1] == 'OP_CHECKMULTISIG' and isinstance(ops[-2], int)): - + m = ops[0] # Required signatures n = ops[-2] # Total public keys - + # Validate the structure if len(ops) == n + 3: # M + N pubkeys + N + OP_CHECKMULTISIG return True, (m, n) - + return False, None def get_script_type(self) -> str: """ Determine the type of script. - + Returns: str: Script type ('p2pkh', 'p2sh', 'p2wpkh', 'p2wsh', 'p2tr', 'multisig', 'unknown') """ diff --git a/bitcoinutils/transactions.py b/bitcoinutils/transactions.py index 547988a0..5b9cc6cc 100644 --- a/bitcoinutils/transactions.py +++ b/bitcoinutils/transactions.py @@ -8,7 +8,7 @@ # 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. - +from typing import Union import math import hashlib import struct @@ -79,7 +79,7 @@ def __init__( txid: str, txout_index: int, script_sig=Script([]), - sequence: str | bytes = DEFAULT_TX_SEQUENCE, + sequence: Union[str, bytes] = DEFAULT_TX_SEQUENCE, ) -> None: """See TxInput description""" @@ -354,31 +354,28 @@ def copy(cls, txout: "TxOutput") -> "TxOutput": @classmethod def from_bytes(cls, b): + """Deserialize a TxOutput from bytes.""" from bitcoinutils.script import Script + from bitcoinutils.utils import read_varint import struct from io import BytesIO stream = BytesIO(b) - # Read 8-byte value (little-endian) + # Read 8-byte value (little-endian) value = struct.unpack(' bytes: byte_length = (i.bit_length() + 7) // 8 return i.to_bytes(byte_length, "big") - -def read_varint(b: bytes) -> tuple[int, int]: +def read_varint(b: bytes) -> tuple: """ Reads a Bitcoin varint from the provided bytes. @@ -603,17 +602,23 @@ def read_varint(b: bytes) -> tuple[int, int]: - value: the decoded integer - size: the number of bytes consumed """ + if not b: + raise ValueError("Empty bytes for varint") + prefix = b[0] if prefix < 0xfd: return prefix, 1 elif prefix == 0xfd: - return int.from_bytes(b[1:3], 'little'), 3 + if len(b) < 3: + raise ValueError("Insufficient bytes for 0xfd varint") + return struct.unpack(' 0") - print(f" - After 2 signatures, combine using: python combine_psbt.py ") - print(f" - Finalize using: python finalize_psbt.py ") - print(f" - Broadcast the final transaction to the network") - - print(f"\n5. Example signing commands:") - print(f" # Alice signs:") - print(f" python sign_psbt.py {psbt_b64[:50]}... cTcFkAJtFvyPKjQhPkijgyv4ZRQTau6wQgd1M87Y221zm1sMTRFT 0") - print(f" # Bob signs:") - print(f" python sign_psbt.py {psbt_b64[:50]}... cUygdGhxnZfjyQZc5ugQY6su6qFgRndqh6JyQK4RN7ry6UUs1Rcj 0") + # Create the witness UTXO (the output we're spending) + witness_utxo = TxOutput(utxo['amount'], p2wsh_address.to_script_pub_key()) + print(f"Debug - Witness UTXO amount: {witness_utxo.amount}") + print(f"Debug - Witness UTXO script: {witness_utxo.script_pubkey.to_hex()}") + psbt.inputs[0].witness_utxo = witness_utxo + + print("\n PSBT for P2WSH Spend Created Successfully") + + try: + base64_psbt = psbt.to_base64() + print("\nBase64 PSBT:\n", base64_psbt) + + # Save to file for convenience + with open('p2wsh_unsigned.psbt', 'w') as f: + f.write(base64_psbt) + print("\n PSBT saved to: p2wsh_unsigned.psbt") + + print("\n Transaction Summary:") + print(f" Input: {utxo['txid']}:{utxo['vout']} ({utxo['amount']} sats)") + print(f" Output: {recipient_address.to_string()} ({sending_amount} sats)") + print(f" Fee: {fee} satoshis") + print(f" Witness Script Type: 2-of-3 multisig") + + print("\n Next steps:") + print(" 1. Sign with at least 2 keys: python examples/sign_psbt.py p2wsh_unsigned.psbt 0") + print(" 2. Sign with the 2nd key: python examples/sign_psbt.py p2wsh_signed_1.psbt 0") + print(" 3. Finalize: python examples/finalize_psbt.py p2wsh_signed_2.psbt") + + except Exception as e: + print(f" Error creating PSBT: {str(e)}") + import traceback + traceback.print_exc() if __name__ == "__main__": main() \ No newline at end of file diff --git a/examples/finalize_psbt.py b/examples/finalize_psbt.py index 64506410..45bd7b9e 100644 --- a/examples/finalize_psbt.py +++ b/examples/finalize_psbt.py @@ -1,75 +1,198 @@ #!/usr/bin/env python3 """ -Example of finalizing a PSBT into a complete transaction. - -This example demonstrates how to: -1. Load a PSBT that has all required signatures -2. Finalize it into a complete transaction -3. Optionally validate the transaction -4. Output the hex transaction ready for broadcast - -The finalization process extracts all signatures from the PSBT -and constructs the final scriptSig and/or witness data. - -Usage: - python finalize_psbt.py [--validate] - -Example: - # Basic finalization - python finalize_psbt.py cHNidP8BAH... - - # With validation - python finalize_psbt.py cHNidP8BAH... --validate - -Note: The PSBT must have all required signatures before finalization. - Use --validate to perform additional transaction validation. +Fixed P2WSH PSBT finalizer with proper witness serialization """ - import sys from bitcoinutils.setup import setup from bitcoinutils.psbt import PSBT +from bitcoinutils.transactions import Transaction, TxWitnessInput +from bitcoinutils.script import Script def main(): - """Finalize a PSBT into a complete transaction.""" - - # Always set the network first - setup('testnet') - if len(sys.argv) < 2: - print("Usage: python finalize_psbt.py [--validate]") - return + print("Usage: python finalize_psbt.py ") + sys.exit(1) - # Load PSBT - psbt = PSBT.from_base64(sys.argv[1]) + setup('testnet') - # Check if validation requested - validate = '--validate' in sys.argv + psbt_file = sys.argv[1] - # Finalize the PSBT - if validate: - final_tx, validation_info = psbt.finalize(validate=True) - if validation_info['valid']: - print(f"\nFinalized Transaction (hex):") - print(final_tx.to_hex()) - print(f"\nTransaction ID: {final_tx.get_txid()}") - print(f"Size: {validation_info['size']} bytes") - print(f"Virtual Size: {validation_info['vsize']} vbytes") - - print(f"\nTo broadcast:") - print(f" bitcoin-cli -testnet sendrawtransaction {final_tx.to_hex()[:50]}...") - else: - print("Finalization failed - validation errors found") - sys.exit(1) - else: - final_tx = psbt.finalize(validate=False) - if final_tx: - print(final_tx.to_hex()) - print(f"\nTransaction ID: {final_tx.get_txid()}") - print(f"\nTo broadcast:") - print(f" bitcoin-cli -testnet sendrawtransaction {final_tx.to_hex()[:50]}...") - else: - print("Finalization failed - missing signatures or invalid PSBT") - sys.exit(1) + try: + # Load PSBT + with open(psbt_file, 'r') as f: + psbt_b64 = f.read().strip() + + psbt = PSBT.from_base64(psbt_b64) + print(f" Loaded PSBT: {len(psbt.inputs)} inputs") + + all_finalized = True + + for i, psbt_input in enumerate(psbt.inputs): + print(f"\n Processing input {i}:") + + # Skip if already finalized + if psbt_input.final_scriptwitness: + print(f" Already finalized") + continue + + # Validate P2WSH + if not psbt_input.witness_script: + print(f" No witness script") + all_finalized = False + continue + + if not psbt_input.witness_utxo: + print(f" No witness UTXO") + all_finalized = False + continue + + if not psbt_input.partial_sigs: + print(f" No partial signatures") + all_finalized = False + continue + + witness_script_hex = psbt_input.witness_script.to_hex() + print(f" Witness script: {len(witness_script_hex)//2} bytes") + print(f" UTXO amount: {psbt_input.witness_utxo.amount:,} sats") + print(f" Partial signatures: {len(psbt_input.partial_sigs)}") + + # For a 2-of-3 multisig, we need at least 2 signatures + required_sigs = 2 # This is m in m-of-n + + if len(psbt_input.partial_sigs) < required_sigs: + print(f" Need {required_sigs} signatures, only have {len(psbt_input.partial_sigs)}") + all_finalized = False + continue + + # Get all available signatures and their corresponding pubkeys + sig_items = list(psbt_input.partial_sigs.items()) + print(f"\n Available signatures:") + for pk, sig in sig_items: + print(f" - Pubkey: {pk.hex()}") + print(f" Sig: {sig.hex()[:32]}...") + + # Sort signatures by public key to ensure consistent ordering + sorted_sig_items = sorted(sig_items, key=lambda x: x[0]) + + # Build witness stack for P2WSH multisig + # Format: [OP_0, sig1, sig2, witness_script] + witness_stack = [] + + # OP_0 for CHECKMULTISIG bug (empty hex string) + witness_stack.append("") # This will be an empty witness element + + # Add first m signatures (we need 2 for 2-of-3) + sigs_added = 0 + for pk, sig in sorted_sig_items: + if sigs_added < required_sigs: + # Convert signature bytes to hex string + witness_stack.append(sig.hex()) + print(f" Added signature {sigs_added + 1}") + sigs_added += 1 + + # Add the witness script as hex string + witness_stack.append(witness_script_hex) + + # Set final witness - store as list of hex strings + psbt_input.final_scriptwitness = witness_stack + + # Clear partial sigs and witness script (they're not needed after finalization) + psbt_input.partial_sigs = {} + psbt_input.witness_script = None + + print(f" Finalized with {required_sigs} signatures") + print(f" Witness stack has {len(witness_stack)} items:") + print(f" [0] OP_0 (empty)") + for j in range(1, required_sigs + 1): + print(f" [{j}] Signature {j}") + print(f" [{required_sigs + 1}] Witness script") + + if not all_finalized: + print("\n Not all inputs could be finalized") + return 1 + + # Extract final transaction + print("\n Extracting final transaction...") + + try: + # Create a new transaction with the same structure + final_tx = Transaction( + psbt.tx.inputs[:], + psbt.tx.outputs[:], + psbt.tx.locktime, + psbt.tx.version, + has_segwit=True # IMPORTANT: Must be True for witness transactions + ) + + # Set up witnesses + final_tx.witnesses = [] + for psbt_input in psbt.inputs: + if psbt_input.final_scriptwitness: + # Create TxWitnessInput from the witness stack + witness = TxWitnessInput(psbt_input.final_scriptwitness) + final_tx.witnesses.append(witness) + else: + # Empty witness for non-witness inputs + final_tx.witnesses.append(TxWitnessInput([])) + + # Serialize the transaction WITH witness data + tx_hex = final_tx.to_hex() + + # Get transaction IDs + txid = final_tx.get_txid() + wtxid = final_tx.get_wtxid() + + print(f"\n Transaction Finalized Successfully!") + print(f" TXID: {txid}") + print(f" WTXID: {wtxid}") + print(f" Size: {len(tx_hex)//2} bytes") + print(f" vSize: {final_tx.get_vsize()} vbytes") + + # Verify witness data is present + if "0001" in tx_hex[8:12]: # Check for witness marker + print(f" Witness data present") + else: + print(f" WARNING: No witness marker found!") + + print(f"\n Transaction hex preview:") + print(f" {tx_hex[:100]}...") + if len(tx_hex) > 200: + print(f" ...{tx_hex[-100:]}") + + # Save transaction hex + output_file = 'finalized_p2wsh_tx.hex' + with open(output_file, 'w') as f: + f.write(tx_hex) + + print(f"\n Transaction saved to: {output_file}") + print(f"\n To broadcast on testnet:") + print(f" bitcoin-cli -testnet sendrawtransaction {tx_hex}") + print(f"\n Or paste the hex at:") + print(f" https://blockstream.info/testnet/tx/push") + + # Save finalized PSBT + try: + finalized_psbt_b64 = psbt.to_base64() + with open('finalized.psbt', 'w') as f: + f.write(finalized_psbt_b64) + print(f"\n Finalized PSBT saved to: finalized.psbt") + except Exception as e: + print(f"\n Warning: Could not save finalized PSBT: {e}") + print(" This is OK - the transaction hex has been saved successfully!") + + except Exception as e: + print(f" Error during extraction: {e}") + import traceback + traceback.print_exc() + return 1 + + return 0 + + except Exception as e: + print(f"\n Error: {e}") + import traceback + traceback.print_exc() + return 1 if __name__ == "__main__": - main() \ No newline at end of file + sys.exit(main()) \ No newline at end of file diff --git a/examples/sign_psbt.py b/examples/sign_psbt.py index 44fc5956..6d003549 100644 --- a/examples/sign_psbt.py +++ b/examples/sign_psbt.py @@ -1,54 +1,111 @@ #!/usr/bin/env python3 """ -Example of signing a PSBT with a private key. - -This example demonstrates how to: -1. Load a PSBT from base64 -2. Sign a specific input with a private key -3. Output the signed PSBT - -The PSBT signing logic automatically detects the script type -(P2PKH, P2WPKH, P2SH, P2WSH, etc.) and signs appropriately. - -Usage: - python sign_psbt.py - -Example: - # Alice signs input 0 - python sign_psbt.py cHNidP8B... cTcFkAJtFvyPKjQh... 0 - - # Bob signs input 0 - python sign_psbt.py cHNidP8B... cUygdGhxnZfjyQZ... 0 - -Note: The input_index parameter is required to specify which input to sign. - In a multisig scenario, multiple parties sign the same input. +Simple P2WSH PSBT signer that relies on bitcoinutils' native functionality. """ - import sys from bitcoinutils.setup import setup from bitcoinutils.keys import PrivateKey from bitcoinutils.psbt import PSBT +from bitcoinutils.constants import SIGHASH_ALL def main(): - """Sign a PSBT input with a private key.""" + if len(sys.argv) < 4: + print("Usage: python examples/sign_psbt.py ") + print("\nExample:") + print(" python examples/sign_psbt.py p2wsh_unsigned.psbt cVVJMEAz... 0") + sys.exit(1) - # Always set the network first - setup('testnet') + psbt_input = sys.argv[1] + private_key_wif = sys.argv[2] + input_index = int(sys.argv[3]) - if len(sys.argv) != 4: - print("Usage: python sign_psbt.py ") - return + setup('testnet') - # Load PSBT and private key - psbt = PSBT.from_base64(sys.argv[1]) - private_key = PrivateKey.from_wif(sys.argv[2]) - input_index = int(sys.argv[3]) + try: + # Load PSBT + try: + with open(psbt_input, 'r') as f: + psbt_base64 = f.read().strip() + print(f" Loaded PSBT from: {psbt_input}") + except: + psbt_base64 = psbt_input + print(f" Using PSBT from command line") + + psbt = PSBT.from_base64(psbt_base64) + + # Load private key + private_key = PrivateKey.from_wif(private_key_wif) + pubkey = private_key.get_public_key() + + print(f"\n Signing Details:") + print(f" Private Key: {private_key_wif[:8]}...") + print(f" Public Key: {pubkey.to_hex()}") + print(f" Input Index: {input_index}") + + # Validate input + if input_index >= len(psbt.inputs): + raise ValueError(f"Input index {input_index} out of range") + + psbt_input_data = psbt.inputs[input_index] + + # Basic validation + if not psbt_input_data.witness_script: + raise ValueError("No witness script found - not a P2WSH input") + + if not psbt_input_data.witness_utxo: + raise ValueError("No witness UTXO found") + + print(f"\n Input Validation:") + print(f" Witness script found ({len(psbt_input_data.witness_script.to_hex())//2} bytes)") + print(f" Witness UTXO found ({psbt_input_data.witness_utxo.amount:,} sats)") + + # Check existing signatures + existing_sigs = len(psbt_input_data.partial_sigs) if psbt_input_data.partial_sigs else 0 + print(f" Existing signatures: {existing_sigs}") + + # Check if already signed by this key + pubkey_bytes = pubkey.to_bytes() + if psbt_input_data.partial_sigs and pubkey_bytes in psbt_input_data.partial_sigs: + print(f" This key has already signed this input") + return 0 + + # Sign using bitcoinutils native method + print(f"\n Signing input {input_index}...") + + # Try signing + success = psbt.sign_input(input_index, private_key, SIGHASH_ALL) + + if success: + new_sig_count = len(psbt.inputs[input_index].partial_sigs) + print(f" Successfully signed! Signatures: {new_sig_count}") + + # Output signed PSBT + signed_psbt = psbt.to_base64() + output_file = f'p2wsh_signed_{new_sig_count}.psbt' + + with open(output_file, 'w') as f: + f.write(signed_psbt) + + print(f"\n Saved to: {output_file}") + print(f" First 100 chars: {signed_psbt[:100]}...") + + # Show next steps + print(f"\n Next Steps:") + print(f" - To sign with another key:") + print(f" python examples/sign_psbt.py {output_file} {input_index}") + print(f" - When you have enough signatures, finalize:") + print(f" python examples/finalize_psbt.py {output_file}") + + return 0 + else: + print(f" Failed to sign input {input_index}") + return 1 - # Sign the specified input - if psbt.sign_input(input_index, private_key): - print(psbt.to_base64()) - else: - sys.exit(1) + except Exception as e: + print(f"\n Error: {str(e)}") + import traceback + traceback.print_exc() + return 1 if __name__ == "__main__": - main() \ No newline at end of file + sys.exit(main()) \ No newline at end of file From 713d6995bfc006ae2cf15d98de74c12b0e18a895 Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Thu, 24 Jul 2025 15:32:47 +0530 Subject: [PATCH 11/12] modified_combine_psbt --- examples/combine_psbt.py | 150 ++++++++++++++++++++++++++++++--------- 1 file changed, 118 insertions(+), 32 deletions(-) diff --git a/examples/combine_psbt.py b/examples/combine_psbt.py index a9bf5f2f..16cb6777 100644 --- a/examples/combine_psbt.py +++ b/examples/combine_psbt.py @@ -1,49 +1,135 @@ #!/usr/bin/env python3 """ -Example of combining multiple PSBTs into a single PSBT. +Combine multiple partially signed PSBTs into a single PSBT. -This example demonstrates how to: -1. Load multiple PSBTs that contain partial signatures -2. Combine them into a single PSBT with all signatures -3. Output the combined PSBT - -This is typically used in multisig scenarios where different -participants sign the same transaction independently and then -combine their signatures. +This is useful in multisig scenarios where different signers create separate +PSBTs with their signatures, which then need to be combined before finalizing. Usage: - python combine_psbt.py [ ...] - + python examples/combine_psbt.py [ ...] + Example: - # Combine Alice's and Bob's signed PSBTs - python combine_psbt.py cHNidP8BAH... cHNidP8BAH... - -Note: All PSBTs must be for the same transaction. The combine - operation merges all partial signatures and other data. + python examples/combine_psbt.py p2wsh_signed_1.psbt p2wsh_signed_2.psbt """ - import sys from bitcoinutils.setup import setup from bitcoinutils.psbt import PSBT def main(): - """Combine multiple PSBTs into one.""" - - # Always set the network first - setup('testnet') - if len(sys.argv) < 3: - print("Usage: python combine_psbt.py [ ...]") - return + print("Usage: python examples/combine_psbt.py [ ...]") + print("\nExample:") + print(" python examples/combine_psbt.py p2wsh_signed_1.psbt p2wsh_signed_2.psbt") + print("\nThis combines multiple PSBTs that have different signatures for the same transaction.") + sys.exit(1) - # Load first PSBT - psbt = PSBT.from_base64(sys.argv[1]) - - # Load and combine with remaining PSBTs - other_psbts = [PSBT.from_base64(base64_str) for base64_str in sys.argv[2:]] - combined = psbt.combine_psbts(other_psbts) + setup('testnet') - print(combined.to_base64()) + try: + # Load all PSBTs + psbts = [] + for i, psbt_file in enumerate(sys.argv[1:], 1): + try: + # Try to load from file first + with open(psbt_file, 'r') as f: + psbt_b64 = f.read().strip() + print(f" Loaded PSBT {i} from: {psbt_file}") + except FileNotFoundError: + # If not a file, treat as base64 string + psbt_b64 = psbt_file + print(f" Using PSBT {i} from command line") + + psbt = PSBT.from_base64(psbt_b64) + psbts.append(psbt) + + # Show info about this PSBT + total_sigs = sum(len(inp.partial_sigs) if inp.partial_sigs else 0 + for inp in psbt.inputs) + print(f" - Inputs: {len(psbt.inputs)}") + print(f" - Total signatures: {total_sigs}") + + print(f"\n Combining {len(psbts)} PSBTs...") + + # Take the first PSBT as base + combined = psbts[0] + + # Combine with the rest + for i, other_psbt in enumerate(psbts[1:], 2): + print(f" Merging PSBT {i}...") + combined = combined.combine(other_psbt) + + # Show combined result info + print("\n Successfully combined PSBTs!") + + # Count signatures per input + print("\n Combined PSBT Summary:") + print(f" Total inputs: {len(combined.inputs)}") + + for i, inp in enumerate(combined.inputs): + sig_count = len(inp.partial_sigs) if inp.partial_sigs else 0 + print(f" Input {i}: {sig_count} signature(s)") + + # Show which pubkeys have signed + if inp.partial_sigs: + for pubkey in inp.partial_sigs: + print(f" - Signed by: {pubkey.hex()[:16]}...") + + # Check if ready to finalize + ready_to_finalize = True + for i, inp in enumerate(combined.inputs): + if inp.witness_script: + # For multisig, check if we have enough signatures + witness_hex = inp.witness_script.to_hex() + # Simple check for 2-of-3 multisig (you might need to adjust this) + if "52" in witness_hex[:4] and "53ae" in witness_hex[-4:]: # OP_2...OP_3 OP_CHECKMULTISIG + required_sigs = 2 + current_sigs = len(inp.partial_sigs) if inp.partial_sigs else 0 + if current_sigs < required_sigs: + ready_to_finalize = False + print(f"\n Input {i} needs {required_sigs - current_sigs} more signature(s)") + + if ready_to_finalize: + print("\n This PSBT has enough signatures and is ready to finalize!") + else: + print("\n This PSBT needs more signatures before it can be finalized.") + + # Save combined PSBT + combined_b64 = combined.to_base64() + + # Generate output filename + total_sigs = sum(len(inp.partial_sigs) if inp.partial_sigs else 0 + for inp in combined.inputs) + output_file = f'p2wsh_combined_{total_sigs}sigs.psbt' + + with open(output_file, 'w') as f: + f.write(combined_b64) + + print(f"\n Saved combined PSBT to: {output_file}") + print(f" Base64 preview: {combined_b64[:80]}...") + + # Show next steps + print("\n Next Steps:") + if ready_to_finalize: + print(f" python examples/finalize_psbt.py {output_file}") + else: + print(" 1. Get more signatures:") + print(f" python examples/sign_psbt.py {output_file} ") + print(" 2. Once you have enough signatures, finalize:") + print(f" python examples/finalize_psbt.py {output_file}") + + return 0 + + except ValueError as e: + print(f"\n Error: {e}") + if "different transactions" in str(e).lower(): + print(" PSBTs must be for the same transaction to be combined.") + print(" The PSBTs you're trying to combine appear to be for different transactions.") + return 1 + except Exception as e: + print(f"\n Error: {e}") + import traceback + traceback.print_exc() + return 1 if __name__ == "__main__": - main() \ No newline at end of file + sys.exit(main()) \ No newline at end of file From e41c452414cc3093877a83331fce49512d077b78 Mon Sep 17 00:00:00 2001 From: JagadishSunilPednekar Date: Fri, 25 Jul 2025 16:42:25 +0530 Subject: [PATCH 12/12] Handling of P2WPKH --- bitcoinutils/psbt.py | 57 +++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/bitcoinutils/psbt.py b/bitcoinutils/psbt.py index 36eec371..f414891d 100644 --- a/bitcoinutils/psbt.py +++ b/bitcoinutils/psbt.py @@ -219,57 +219,60 @@ def sign_input(self, input_index: int, private_key: PrivateKey, sighash_type: in return False prev_txout = prev_tx.outputs[self.tx.inputs[input_index].tx_out_index] - # Determine the correct script to use - script_to_use = ( - psbt_input.witness_script - if psbt_input.witness_script is not None - else psbt_input.redeem_script - if psbt_input.redeem_script is not None - else prev_txout.script_pubkey - ) - - # Compute sighash correctly - if is_segwit: + # For P2WPKH, we need special handling + if is_segwit and self._is_p2wpkh_script(prev_txout.script_pubkey): + # Get the P2PKH script from the public key + pubkey_obj = private_key.get_public_key() + p2pkh_script = pubkey_obj.get_address().to_script_pub_key() + sighash = self.tx.get_transaction_segwit_digest( input_index, - script_to_use, + p2pkh_script, # This is the key fix! prev_txout.amount, sighash_type ) else: - sighash = self.tx.get_transaction_digest( - input_index, - script_to_use, - sighash_type + # Determine the correct script to use for other types + script_to_use = ( + psbt_input.witness_script + if psbt_input.witness_script is not None + else psbt_input.redeem_script + if psbt_input.redeem_script is not None + else prev_txout.script_pubkey ) - # Prepare SigningKey correctly + # Compute sighash + if is_segwit: + sighash = self.tx.get_transaction_segwit_digest( + input_index, + script_to_use, + prev_txout.amount, + sighash_type + ) + else: + sighash = self.tx.get_transaction_digest( + input_index, + script_to_use, + sighash_type + ) + + # Rest of your signing code remains the same... if hasattr(private_key, 'key'): raw_private_key = private_key.key.privkey.secret_multiplier.to_bytes(32, 'big') else: raw_private_key = private_key.to_bytes() sk = SigningKey.from_string(raw_private_key, curve=SECP256k1) - - # Create DER signature + sighash type byte sig = sk.sign_digest(sighash, sigencode=ecdsa.util.sigencode_der_canonize) + bytes([sighash_type]) - # Get compressed pubkey bytes - MUST include the prefix byte pubkey_obj = private_key.get_public_key() - - # The to_hex() method should give us the full compressed pubkey with prefix pubkey_hex = pubkey_obj.to_hex() pubkey_bytes = bytes.fromhex(pubkey_hex) - # Verify we have a proper compressed pubkey (33 bytes starting with 02 or 03) if len(pubkey_bytes) != 33 or pubkey_bytes[0] not in [0x02, 0x03]: raise ValueError(f"Invalid compressed public key: {pubkey_hex}") - # Ensure we store only the compressed pubkey (33 bytes) - if len(pubkey_bytes) > 33: - pubkey_bytes = pubkey_bytes[:33] psbt_input.partial_sigs[pubkey_bytes] = sig - return True except Exception as e: