1use crate::error::BitcoinError;
24use bitcoin::blockdata::opcodes::all::*;
25use bitcoin::blockdata::script::Instruction;
26use bitcoin::{Script, ScriptBuf};
27use serde::{Deserialize, Serialize};
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct OptimizationConfig {
32 pub eliminate_redundant_stack_ops: bool,
34 pub optimize_numeric_pushes: bool,
36 pub combine_pushes: bool,
38 pub optimize_witness_scripts: bool,
40 pub remove_dead_code: bool,
42 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, optimize_witness_scripts: true,
53 remove_dead_code: true,
54 optimize_control_flow: true,
55 }
56 }
57}
58
59impl OptimizationConfig {
60 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct OptimizationResult {
88 pub optimized_script: ScriptBuf,
90 pub original_size: usize,
92 pub optimized_size: usize,
94 pub bytes_saved: usize,
96 pub reduction_percentage: f64,
98 pub optimizations_applied: Vec<OptimizationType>,
100 pub is_safe: bool,
102 pub warnings: Vec<String>,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108pub enum OptimizationType {
109 RedundantStackOp,
111 NumericPush,
113 CombinedPushes,
115 WitnessOptimization,
117 DeadCodeRemoval,
119 ControlFlowOptimization,
121}
122
123pub struct ScriptOptimizer {
125 config: OptimizationConfig,
126}
127
128impl ScriptOptimizer {
129 pub fn new(config: OptimizationConfig) -> Self {
131 Self { config }
132 }
133
134 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 if self.config.eliminate_redundant_stack_ops {
143 if let Some((script, count)) = self.eliminate_redundant_stack_ops(¤t_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(¤t_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(¤t_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 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 let is_safe = self.verify_optimization_safety(script, ¤t_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 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 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 i += 2;
227 optimizations += 1;
228 continue;
229 }
230 }
231 }
232
233 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 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 if bytes.len() == 1 {
264 let value = bytes.as_bytes()[0];
265 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 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 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 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 fn verify_optimization_safety(&self, original: &Script, optimized: &Script) -> bool {
333 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 optimized.len() > original.len() {
357 return false;
358 }
359
360 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 size_change_ratio > 0.5 {
369 return false;
370 }
371
372 true
373 }
374
375 pub fn estimate_savings(&self, script: &Script) -> OptimizationEstimate {
377 let mut potential_bytes = 0;
378 let mut opportunities = Vec::new();
379
380 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 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; 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#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct OptimizationEstimate {
417 pub potential_bytes_saved: usize,
419 pub optimization_opportunities: Vec<String>,
421 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 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 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 let script = ScriptBuf::builder()
505 .push_slice([1u8]) .into_script();
507
508 let optimizer = ScriptOptimizer::default();
509 let result = optimizer.optimize(&script).unwrap();
510
511 assert!(
513 result
514 .optimizations_applied
515 .contains(&OptimizationType::NumericPush)
516 );
517 }
518
519 #[test]
520 fn test_remove_dead_code() {
521 let script = ScriptBuf::builder()
523 .push_opcode(OP_RETURN)
524 .push_slice(b"data")
525 .push_opcode(OP_PUSHNUM_1) .push_opcode(OP_PUSHNUM_2) .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 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}