#!/usr/bin/env python3 """ Bitcoin Transaction Size Estimator Supports legacy, SegWit, and Taproot output types with accurate size calculations """ import json import math from typing import Dict, List, Optional, Any, Tuple from dataclasses import dataclass from datetime import datetime @dataclass class InputConfig: """Input configuration for size estimation""" type: str # 'p2pkh', 'p2wpkh', 'p2sh', 'p2tr' count: int redeem_script_size: int = 0 # For P2SH inputs @dataclass class OutputConfig: """Output configuration for size estimation""" type: str # 'p2pkh', 'p2wpkh', 'p2sh', 'p2tr' count: int class TxSizeEstimator: """Accurate Bitcoin transaction size estimator""" # Size constants in bytes INPUT_SIZES = { 'p2pkh': { 'script_sig': 107, # ~107 bytes 'witness': 0, }, 'p2wpkh': { 'script_sig': 0, 'witness': 108, # 108 bytes witness }, 'p2sh-p2wpkh': { 'script_sig': 51, # Redeem script + signature 'witness': 108, }, 'p2tr': { 'script_sig': 0, 'witness': 64, # Schnorr signature + key } } OUTPUT_SIZES = { 'p2pkh': 25, # 25 bytes scriptPubKey 'p2wpkh': 22, # 22 bytes (native segwit) 'p2sh': 23, # 23 bytes 'p2tr': 34, # 34 bytes } # Transaction overhead BASE_TX_SIZE = 10 # 4 bytes version + 4 bytes locktime + 1 byte flag + 1 byte input/output counts def __init__(self): self.size_cache = {} def calculate_input_size(self, input_config: InputConfig) -> Tuple[int, int]: """Calculate input size (base, witness)""" sizes = self.INPUT_SIZES.get(input_config.type, self.INPUT_SIZES['p2pkh']) # Base size includes script_sig base_size = 32 + 4 + 1 + len(sizes['script_sig'].to_bytes(1, 'big')) + sizes['script_sig'] # 32 bytes outpoint + 4 bytes sequence + 1 byte script_sig length + script_sig # Witness size witness_size = sizes['witness'] return base_size, witness_size def calculate_output_size(self, output_config: OutputConfig) -> int: """Calculate output size""" script_size = self.OUTPUT_SIZES.get(output_config.type, self.OUTPUT_SIZES['p2pkh']) # 8 bytes value + 1 byte script length + script return 8 + 1 + script_size def estimate_transaction_size(self, inputs: List[InputConfig], outputs: List[OutputConfig]) -> Dict[str, int]: """Estimate transaction size with witness data""" # Calculate base size base_size = self.BASE_TX_SIZE # Add input sizes total_input_base = 0 total_witness = 0 for input_config in inputs: input_base, witness = self.calculate_input_size(input_config) total_input_base += input_base total_witness += witness # Add output sizes total_output_size = sum(self.calculate_output_size(out) for out in outputs) # Calculate final sizes base_total = base_size + total_input_base + total_output_size # For SegWit transactions, apply witness discount if total_witness > 0: # vsize = (base_size * 3 + total_size) / 4 total_size = base_total + total_witness vsize = math.ceil((base_total * 3 + total_size) / 4) weight = base_total * 3 + total_size else: total_size = base_total vsize = base_total weight = base_total * 4 return { 'base_size': base_total, 'total_size': total_size, 'vsize': vsize, 'weight': weight, 'witness_size': total_witness } def generate_size_matrix(self) -> Dict[str, Any]: """Generate comprehensive size matrix for different configurations""" matrix = { 'timestamp': datetime.now().isoformat(), 'configurations': {} } # Test different input/output combinations input_types = ['p2pkh', 'p2wpkh', 'p2sh-p2wpkh', 'p2tr'] output_types = ['p2pkh', 'p2wpkh', 'p2sh', 'p2tr'] for input_type in input_types: for output_type in output_types: config_key = f"{input_type}_to_{output_type}" config_data = { 'input_type': input_type, 'output_type': output_type, 'size_matrix': [] } # Test 1-10 inputs and 1-5 outputs for num_inputs in range(1, 11): for num_outputs in range(1, 6): inputs = [InputConfig(input_type, 1)] * num_inputs outputs = [OutputConfig(output_type, 1)] * num_outputs size_info = self.estimate_transaction_size(inputs, outputs) config_data['size_matrix'].append({ 'inputs': num_inputs, 'outputs': num_outputs, 'vsize': size_info['vsize'], 'weight': size_info['weight'], 'base_size': size_info['base_size'] }) matrix['configurations'][config_key] = config_data return matrix def calculate_fee_for_config(self, inputs: List[InputConfig], outputs: List[OutputConfig], fee_rate: float) -> Dict[str, Any]: """Calculate fee for specific configuration""" size_info = self.estimate_transaction_size(inputs, outputs) fee_vsize = math.ceil(size_info['vsize'] * fee_rate) fee_weight = math.ceil(size_info['weight'] * fee_rate / 4) return { 'size_info': size_info, 'fee_rate': fee_rate, 'fee_vsize': fee_vsize, 'fee_weight': fee_weight, 'recommended_fee': fee_vsize } def optimize_for_fee(self, target_inputs: int, target_outputs: int, input_types: List[str], output_types: List[str], fee_rate: float) -> Dict[str, Any]: """Find optimal input/output types for minimum fee""" best_config = None min_fee = float('inf') for input_type in input_types: for output_type in output_types: inputs = [InputConfig(input_type, 1)] * target_inputs outputs = [OutputConfig(output_type, 1)] * target_outputs fee_info = self.calculate_fee_for_config(inputs, outputs, fee_rate) if fee_info['recommended_fee'] < min_fee: min_fee = fee_info['recommended_fee'] best_config = { 'input_type': input_type, 'output_type': output_type, 'fee_info': fee_info } return best_config def main(): """Run transaction size analysis""" estimator = TxSizeEstimator() # Generate size matrix size_matrix = estimator.generate_size_matrix() # Test specific configurations test_configs = [ ([InputConfig('p2wpkh', 2)], [OutputConfig('p2wpkh', 1)], "2-to-1 SegWit"), ([InputConfig('p2tr', 1)], [OutputConfig('p2tr', 2)], "1-to-2 Taproot"), ([InputConfig('p2pkh', 3)], [OutputConfig('p2pkh', 1)], "3-to-1 Legacy"), ] test_results = [] for inputs, outputs, description in test_configs: fee_info = estimator.calculate_fee_for_config(inputs, outputs, 5.0) test_results.append({ 'description': description, 'fee_info': fee_info }) # Find optimal configuration optimal = estimator.optimize_for_fee(2, 2, ['p2wpkh', 'p2tr'], ['p2wpkh', 'p2tr'], 5.0) results = { 'size_matrix': size_matrix, 'test_configurations': test_results, 'optimal_configuration': optimal } # Save results with open('tx_size_analysis.json', 'w') as f: json.dump(results, f, indent=2) print("✅ Transaction size analysis complete") print(f"📊 Analyzed {len(size_matrix['configurations'])} configurations") print(f"🎯 Optimal config: {optimal['input_type']} → {optimal['output_type']}") return results if __name__ == "__main__": main()