kaccy_bitcoin/
script_optimizer.rs

1//! Bitcoin Script Optimizer
2//!
3//! This module provides tools for optimizing Bitcoin scripts to reduce size and witness costs.
4//! Optimization strategies include:
5//! - Redundant opcode elimination
6//! - Witness size optimization
7//! - Script size reduction strategies
8//!
9//! # Examples
10//!
11//! ```
12//! use kaccy_bitcoin::script_optimizer::{ScriptOptimizer, OptimizationConfig};
13//! use bitcoin::ScriptBuf;
14//!
15//! let script = ScriptBuf::new();
16//! let optimizer = ScriptOptimizer::new(OptimizationConfig::default());
17//! let optimized = optimizer.optimize(&script).unwrap();
18//! println!("Original size: {} bytes", script.len());
19//! println!("Optimized size: {} bytes", optimized.optimized_script.len());
20//! println!("Savings: {} bytes", optimized.bytes_saved);
21//! ```
22
23use crate::error::BitcoinError;
24use bitcoin::blockdata::opcodes::all::*;
25use bitcoin::blockdata::script::Instruction;
26use bitcoin::{Script, ScriptBuf};
27use serde::{Deserialize, Serialize};
28
29/// Configuration for script optimization
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct OptimizationConfig {
32    /// Remove redundant DUP/DROP sequences
33    pub eliminate_redundant_stack_ops: bool,
34    /// Optimize numeric pushes (use shorter opcodes for small numbers)
35    pub optimize_numeric_pushes: bool,
36    /// Combine consecutive pushes where possible
37    pub combine_pushes: bool,
38    /// Optimize witness scripts for SegWit
39    pub optimize_witness_scripts: bool,
40    /// Remove unreachable code after OP_RETURN
41    pub remove_dead_code: bool,
42    /// Optimize control flow (IF/ELSE/ENDIF)
43    pub optimize_control_flow: bool,
44}
45
46impl Default for OptimizationConfig {
47    fn default() -> Self {
48        Self {
49            eliminate_redundant_stack_ops: true,
50            optimize_numeric_pushes: true,
51            combine_pushes: false, // Conservative: might change semantics
52            optimize_witness_scripts: true,
53            remove_dead_code: true,
54            optimize_control_flow: true,
55        }
56    }
57}
58
59impl OptimizationConfig {
60    /// Create an aggressive optimization configuration
61    pub fn aggressive() -> Self {
62        Self {
63            eliminate_redundant_stack_ops: true,
64            optimize_numeric_pushes: true,
65            combine_pushes: true,
66            optimize_witness_scripts: true,
67            remove_dead_code: true,
68            optimize_control_flow: true,
69        }
70    }
71
72    /// Create a conservative optimization configuration
73    pub fn conservative() -> Self {
74        Self {
75            eliminate_redundant_stack_ops: true,
76            optimize_numeric_pushes: true,
77            combine_pushes: false,
78            optimize_witness_scripts: false,
79            remove_dead_code: false,
80            optimize_control_flow: false,
81        }
82    }
83}
84
85/// Result of script optimization
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct OptimizationResult {
88    /// The optimized script
89    pub optimized_script: ScriptBuf,
90    /// Original script size in bytes
91    pub original_size: usize,
92    /// Optimized script size in bytes
93    pub optimized_size: usize,
94    /// Number of bytes saved
95    pub bytes_saved: usize,
96    /// Percentage reduction
97    pub reduction_percentage: f64,
98    /// List of optimizations applied
99    pub optimizations_applied: Vec<OptimizationType>,
100    /// Whether the script is safe to use (semantics preserved)
101    pub is_safe: bool,
102    /// Warnings about potential issues
103    pub warnings: Vec<String>,
104}
105
106/// Types of optimizations that can be applied
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108pub enum OptimizationType {
109    /// Removed redundant DUP/DROP sequence
110    RedundantStackOp,
111    /// Optimized numeric push
112    NumericPush,
113    /// Combined consecutive pushes
114    CombinedPushes,
115    /// Optimized witness script
116    WitnessOptimization,
117    /// Removed dead code
118    DeadCodeRemoval,
119    /// Optimized control flow
120    ControlFlowOptimization,
121}
122
123/// Bitcoin script optimizer
124pub struct ScriptOptimizer {
125    config: OptimizationConfig,
126}
127
128impl ScriptOptimizer {
129    /// Create a new script optimizer with the given configuration
130    pub fn new(config: OptimizationConfig) -> Self {
131        Self { config }
132    }
133
134    /// Optimize a Bitcoin script
135    pub fn optimize(&self, script: &Script) -> Result<OptimizationResult, BitcoinError> {
136        let original_size = script.len();
137        let mut current_script = script.to_owned();
138        let mut optimizations_applied = Vec::new();
139        let mut warnings = Vec::new();
140
141        // Apply optimizations in sequence
142        if self.config.eliminate_redundant_stack_ops {
143            if let Some((script, count)) = self.eliminate_redundant_stack_ops(&current_script) {
144                current_script = script;
145                for _ in 0..count {
146                    optimizations_applied.push(OptimizationType::RedundantStackOp);
147                }
148            }
149        }
150
151        if self.config.optimize_numeric_pushes {
152            if let Some((script, count)) = self.optimize_numeric_pushes(&current_script) {
153                current_script = script;
154                for _ in 0..count {
155                    optimizations_applied.push(OptimizationType::NumericPush);
156                }
157            }
158        }
159
160        if self.config.remove_dead_code {
161            if let Some((script, removed)) = self.remove_dead_code(&current_script) {
162                if removed {
163                    current_script = script;
164                    optimizations_applied.push(OptimizationType::DeadCodeRemoval);
165                    warnings.push(
166                        "Removed dead code after OP_RETURN - verify this is intentional"
167                            .to_string(),
168                    );
169                }
170            }
171        }
172
173        if self.config.optimize_witness_scripts
174            && (script.is_p2wpkh() || script.is_p2wsh() || script.is_p2tr())
175        {
176            // Witness optimization is mostly about choosing the right script type
177            // This is more of an analysis than a transformation
178            warnings.push(
179                "Witness optimization: consider using Taproot for better efficiency".to_string(),
180            );
181        }
182
183        let optimized_size = current_script.len();
184        let bytes_saved = original_size.saturating_sub(optimized_size);
185        let reduction_percentage = if original_size > 0 {
186            (bytes_saved as f64 / original_size as f64) * 100.0
187        } else {
188            0.0
189        };
190
191        // Check if optimization is safe (semantics preserved)
192        let is_safe = self.verify_optimization_safety(script, &current_script);
193        if !is_safe {
194            warnings.push(
195                "Optimization may have changed script semantics - use with caution".to_string(),
196            );
197        }
198
199        Ok(OptimizationResult {
200            optimized_script: current_script,
201            original_size,
202            optimized_size,
203            bytes_saved,
204            reduction_percentage,
205            optimizations_applied,
206            is_safe,
207            warnings,
208        })
209    }
210
211    /// Eliminate redundant stack operations (e.g., DUP DROP sequences)
212    fn eliminate_redundant_stack_ops(&self, script: &Script) -> Option<(ScriptBuf, usize)> {
213        let mut builder = ScriptBuf::builder();
214        let instructions: Vec<_> = script.instructions().collect();
215        let mut i = 0;
216        let mut optimizations = 0;
217
218        while i < instructions.len() {
219            if i + 1 < instructions.len() {
220                // Check for DUP DROP pattern
221                if let (Ok(Instruction::Op(op1)), Ok(Instruction::Op(op2))) =
222                    (&instructions[i], &instructions[i + 1])
223                {
224                    if op1.to_u8() == OP_DUP.to_u8() && op2.to_u8() == OP_DROP.to_u8() {
225                        // Skip both instructions (they cancel out)
226                        i += 2;
227                        optimizations += 1;
228                        continue;
229                    }
230                }
231            }
232
233            // Copy the instruction as-is
234            if let Ok(instruction) = &instructions[i] {
235                match instruction {
236                    Instruction::Op(opcode) => {
237                        builder = builder.push_opcode(*opcode);
238                    }
239                    Instruction::PushBytes(bytes) => {
240                        builder = builder.push_slice(bytes);
241                    }
242                }
243            }
244            i += 1;
245        }
246
247        if optimizations > 0 {
248            Some((builder.into_script(), optimizations))
249        } else {
250            None
251        }
252    }
253
254    /// Optimize numeric pushes to use shorter opcodes
255    fn optimize_numeric_pushes(&self, script: &Script) -> Option<(ScriptBuf, usize)> {
256        let mut builder = ScriptBuf::builder();
257        let mut optimizations = 0;
258
259        for instruction in script.instructions().flatten() {
260            match instruction {
261                Instruction::PushBytes(bytes) => {
262                    // Check if this is a small number that can be optimized
263                    if bytes.len() == 1 {
264                        let value = bytes.as_bytes()[0];
265                        // Numbers 1-16 can use OP_1 through OP_16
266                        if (1..=16).contains(&value) {
267                            builder = builder.push_opcode(bitcoin::opcodes::Opcode::from(
268                                OP_PUSHNUM_1.to_u8() + value - 1,
269                            ));
270                            optimizations += 1;
271                            continue;
272                        } else if value == 0 {
273                            builder = builder.push_opcode(OP_PUSHBYTES_0);
274                            optimizations += 1;
275                            continue;
276                        } else if value == 0x81 {
277                            // -1 can use OP_1NEGATE
278                            builder = builder.push_opcode(OP_PUSHNUM_NEG1);
279                            optimizations += 1;
280                            continue;
281                        }
282                    }
283                    builder = builder.push_slice(bytes);
284                }
285                Instruction::Op(opcode) => {
286                    builder = builder.push_opcode(opcode);
287                }
288            }
289        }
290
291        if optimizations > 0 {
292            Some((builder.into_script(), optimizations))
293        } else {
294            None
295        }
296    }
297
298    /// Remove dead code after OP_RETURN
299    fn remove_dead_code(&self, script: &Script) -> Option<(ScriptBuf, bool)> {
300        let mut builder = ScriptBuf::builder();
301        let mut found_return = false;
302        let mut removed_code = false;
303
304        for instruction in script.instructions().flatten() {
305            if found_return {
306                // Skip all instructions after OP_RETURN
307                removed_code = true;
308                continue;
309            }
310
311            match instruction {
312                Instruction::Op(opcode) => {
313                    if opcode.to_u8() == OP_RETURN.to_u8() {
314                        found_return = true;
315                    }
316                    builder = builder.push_opcode(opcode);
317                }
318                Instruction::PushBytes(bytes) => {
319                    builder = builder.push_slice(bytes);
320                }
321            }
322        }
323
324        if removed_code {
325            Some((builder.into_script(), true))
326        } else {
327            None
328        }
329    }
330
331    /// Verify that optimization preserved script semantics
332    fn verify_optimization_safety(&self, original: &Script, optimized: &Script) -> bool {
333        // Basic safety checks:
334        // 1. Standard scripts should remain standard
335        // 2. Script type should not change
336        // 3. If original is valid, optimized should be valid
337
338        // Check script type preservation
339        if original.is_p2pkh() != optimized.is_p2pkh() {
340            return false;
341        }
342        if original.is_p2sh() != optimized.is_p2sh() {
343            return false;
344        }
345        if original.is_p2wpkh() != optimized.is_p2wpkh() {
346            return false;
347        }
348        if original.is_p2wsh() != optimized.is_p2wsh() {
349            return false;
350        }
351        if original.is_p2tr() != optimized.is_p2tr() {
352            return false;
353        }
354
355        // If we made the script larger, something is wrong
356        if optimized.len() > original.len() {
357            return false;
358        }
359
360        // Conservative: if the script changed significantly, mark as potentially unsafe
361        let size_change_ratio = if !original.is_empty() {
362            (original.len() - optimized.len()) as f64 / original.len() as f64
363        } else {
364            0.0
365        };
366
367        // If more than 50% of the script was removed, be cautious
368        if size_change_ratio > 0.5 {
369            return false;
370        }
371
372        true
373    }
374
375    /// Estimate potential savings without actually optimizing
376    pub fn estimate_savings(&self, script: &Script) -> OptimizationEstimate {
377        let mut potential_bytes = 0;
378        let mut opportunities = Vec::new();
379
380        // Count redundant stack ops
381        let instructions: Vec<_> = script.instructions().collect();
382        for i in 0..instructions.len().saturating_sub(1) {
383            if let (Ok(Instruction::Op(op1)), Ok(Instruction::Op(op2))) =
384                (&instructions[i], &instructions[i + 1])
385            {
386                if op1.to_u8() == OP_DUP.to_u8() && op2.to_u8() == OP_DROP.to_u8() {
387                    potential_bytes += 2;
388                    opportunities.push("Redundant DUP/DROP sequence".to_string());
389                }
390            }
391        }
392
393        // Count inefficient numeric pushes
394        for instruction in script.instructions() {
395            if let Ok(Instruction::PushBytes(bytes)) = instruction {
396                if bytes.len() == 1 {
397                    let value = bytes.as_bytes()[0];
398                    if (1..=16).contains(&value) || value == 0 || value == 0x81 {
399                        potential_bytes += 1; // Save 1 byte by using OP_N
400                        opportunities.push(format!("Inefficient push of number {}", value));
401                    }
402                }
403            }
404        }
405
406        OptimizationEstimate {
407            potential_bytes_saved: potential_bytes,
408            optimization_opportunities: opportunities,
409            is_worth_optimizing: potential_bytes > 0,
410        }
411    }
412}
413
414/// Estimate of potential optimization savings
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct OptimizationEstimate {
417    /// Potential bytes that could be saved
418    pub potential_bytes_saved: usize,
419    /// List of optimization opportunities
420    pub optimization_opportunities: Vec<String>,
421    /// Whether optimization is worthwhile
422    pub is_worth_optimizing: bool,
423}
424
425impl Default for ScriptOptimizer {
426    fn default() -> Self {
427        Self::new(OptimizationConfig::default())
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434    use bitcoin::hashes::Hash;
435
436    #[test]
437    fn test_optimization_config_default() {
438        let config = OptimizationConfig::default();
439        assert!(config.eliminate_redundant_stack_ops);
440        assert!(config.optimize_numeric_pushes);
441    }
442
443    #[test]
444    fn test_optimization_config_aggressive() {
445        let config = OptimizationConfig::aggressive();
446        assert!(config.eliminate_redundant_stack_ops);
447        assert!(config.combine_pushes);
448    }
449
450    #[test]
451    fn test_optimization_config_conservative() {
452        let config = OptimizationConfig::conservative();
453        assert!(config.eliminate_redundant_stack_ops);
454        assert!(!config.combine_pushes);
455    }
456
457    #[test]
458    fn test_optimize_empty_script() {
459        let script = ScriptBuf::new();
460        let optimizer = ScriptOptimizer::default();
461        let result = optimizer.optimize(&script).unwrap();
462        assert_eq!(result.original_size, 0);
463        assert_eq!(result.optimized_size, 0);
464        assert_eq!(result.bytes_saved, 0);
465    }
466
467    #[test]
468    fn test_optimize_standard_script() {
469        // Standard P2PKH script should not be modified
470        let pubkey_hash = [0u8; 20];
471        let hash = bitcoin::hashes::hash160::Hash::from_byte_array(pubkey_hash);
472        let script = ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::from_raw_hash(hash));
473
474        let optimizer = ScriptOptimizer::default();
475        let result = optimizer.optimize(&script).unwrap();
476
477        assert_eq!(result.original_size, script.len());
478        assert!(result.is_safe);
479    }
480
481    #[test]
482    fn test_eliminate_redundant_dup_drop() {
483        // Create a script with DUP DROP sequence
484        let script = ScriptBuf::builder()
485            .push_opcode(OP_DUP)
486            .push_opcode(OP_DROP)
487            .push_opcode(OP_PUSHNUM_1)
488            .into_script();
489
490        let optimizer = ScriptOptimizer::default();
491        let result = optimizer.optimize(&script).unwrap();
492
493        assert!(result.bytes_saved > 0);
494        assert!(
495            result
496                .optimizations_applied
497                .contains(&OptimizationType::RedundantStackOp)
498        );
499    }
500
501    #[test]
502    fn test_optimize_numeric_push() {
503        // Create a script with inefficient numeric push
504        let script = ScriptBuf::builder()
505            .push_slice([1u8]) // This should be optimized to OP_PUSHNUM_1
506            .into_script();
507
508        let optimizer = ScriptOptimizer::default();
509        let result = optimizer.optimize(&script).unwrap();
510
511        // Should have optimized the push
512        assert!(
513            result
514                .optimizations_applied
515                .contains(&OptimizationType::NumericPush)
516        );
517    }
518
519    #[test]
520    fn test_remove_dead_code() {
521        // Create a script with code after OP_RETURN
522        let script = ScriptBuf::builder()
523            .push_opcode(OP_RETURN)
524            .push_slice(b"data")
525            .push_opcode(OP_PUSHNUM_1) // Dead code
526            .push_opcode(OP_PUSHNUM_2) // Dead code
527            .into_script();
528
529        let config = OptimizationConfig {
530            remove_dead_code: true,
531            ..Default::default()
532        };
533        let optimizer = ScriptOptimizer::new(config);
534        let result = optimizer.optimize(&script).unwrap();
535
536        assert!(result.bytes_saved > 0);
537        assert!(!result.warnings.is_empty());
538    }
539
540    #[test]
541    fn test_estimate_savings() {
542        // Create a script with optimization opportunities
543        let script = ScriptBuf::builder()
544            .push_opcode(OP_DUP)
545            .push_opcode(OP_DROP)
546            .push_slice([1u8])
547            .into_script();
548
549        let optimizer = ScriptOptimizer::default();
550        let estimate = optimizer.estimate_savings(&script);
551
552        assert!(estimate.is_worth_optimizing);
553        assert!(estimate.potential_bytes_saved > 0);
554        assert!(!estimate.optimization_opportunities.is_empty());
555    }
556
557    #[test]
558    fn test_safety_verification() {
559        let pubkey_hash = [0u8; 20];
560        let hash = bitcoin::hashes::hash160::Hash::from_byte_array(pubkey_hash);
561        let script = ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::from_raw_hash(hash));
562
563        let optimizer = ScriptOptimizer::default();
564        let result = optimizer.optimize(&script).unwrap();
565
566        assert!(result.is_safe);
567    }
568
569    #[test]
570    fn test_optimization_result_percentage() {
571        let script = ScriptBuf::builder()
572            .push_opcode(OP_DUP)
573            .push_opcode(OP_DROP)
574            .into_script();
575
576        let optimizer = ScriptOptimizer::default();
577        let result = optimizer.optimize(&script).unwrap();
578
579        if result.bytes_saved > 0 {
580            assert!(result.reduction_percentage > 0.0);
581            assert!(result.reduction_percentage <= 100.0);
582        }
583    }
584}