1use crate::error::BitcoinError;
28use bitcoin::blockdata::opcodes::all::*;
29use bitcoin::blockdata::script::Instruction;
30use bitcoin::{Script, ScriptBuf};
31use serde::{Deserialize, Serialize};
32use std::fmt;
33
34pub struct ScriptDisassembler {
36 pub show_addresses: bool,
38 pub use_symbolic_names: bool,
40}
41
42impl ScriptDisassembler {
43 pub fn new() -> Self {
45 Self {
46 show_addresses: false,
47 use_symbolic_names: true,
48 }
49 }
50
51 pub fn disassemble(&self, script: &Script) -> String {
53 let mut result = Vec::new();
54
55 for instruction in script.instructions() {
56 match instruction {
57 Ok(Instruction::Op(opcode)) => {
58 result.push(format!("{:?}", opcode));
59 }
60 Ok(Instruction::PushBytes(bytes)) => {
61 let hex_data = hex::encode(bytes.as_bytes());
62 if bytes.len() <= 4 {
63 if let Ok(num) = bytes.as_bytes().try_into().map(i32::from_le_bytes) {
65 result.push(format!("PUSH {} (0x{})", num, hex_data));
66 } else {
67 result.push(format!("PUSH 0x{}", hex_data));
68 }
69 } else {
70 result.push(format!("PUSH 0x{}", hex_data));
71 }
72 }
73 Err(e) => {
74 result.push(format!("ERROR: {:?}", e));
75 }
76 }
77 }
78
79 if self.use_symbolic_names {
81 if script.is_p2pkh() {
82 return format!("P2PKH ({})", result.join(" "));
83 } else if script.is_p2sh() {
84 return format!("P2SH ({})", result.join(" "));
85 } else if script.is_p2wpkh() {
86 return format!("P2WPKH ({})", result.join(" "));
87 } else if script.is_p2wsh() {
88 return format!("P2WSH ({})", result.join(" "));
89 } else if script.is_p2tr() {
90 return format!("P2TR ({})", result.join(" "));
91 } else if script.is_op_return() {
92 return format!("OP_RETURN ({})", result.join(" "));
93 }
94 }
95
96 result.join(" ")
97 }
98
99 pub fn disassemble_compact(&self, script: &Script) -> String {
101 if script.is_p2pkh() {
102 "P2PKH".to_string()
103 } else if script.is_p2sh() {
104 "P2SH".to_string()
105 } else if script.is_p2wpkh() {
106 "P2WPKH".to_string()
107 } else if script.is_p2wsh() {
108 "P2WSH".to_string()
109 } else if script.is_p2tr() {
110 "P2TR".to_string()
111 } else if script.is_op_return() {
112 "OP_RETURN".to_string()
113 } else {
114 format!("<{} bytes>", script.len())
115 }
116 }
117}
118
119impl Default for ScriptDisassembler {
120 fn default() -> Self {
121 Self::new()
122 }
123}
124
125pub struct ScriptAnalyzer<'a> {
127 script: &'a Script,
128}
129
130impl<'a> ScriptAnalyzer<'a> {
131 pub fn new(script: &'a Script) -> Self {
133 Self { script }
134 }
135
136 pub fn complexity_score(&self) -> u32 {
144 let mut score = 0u32;
145
146 for instruction in self.script.instructions().flatten() {
147 score += match instruction {
148 Instruction::Op(opcode) => self.opcode_complexity(opcode),
149 Instruction::PushBytes(bytes) => {
150 1 + (bytes.len() / 32) as u32
152 }
153 };
154 }
155
156 score
157 }
158
159 fn opcode_complexity(&self, opcode: bitcoin::opcodes::Opcode) -> u32 {
161 use bitcoin::blockdata::opcodes::all::*;
162
163 match opcode.to_u8() {
164 x if x == OP_CHECKSIG.to_u8()
166 || x == OP_CHECKSIGVERIFY.to_u8()
167 || x == OP_CHECKMULTISIG.to_u8()
168 || x == OP_CHECKMULTISIGVERIFY.to_u8() =>
169 {
170 10
171 }
172 x if x == OP_HASH160.to_u8()
173 || x == OP_HASH256.to_u8()
174 || x == OP_SHA256.to_u8()
175 || x == OP_RIPEMD160.to_u8() =>
176 {
177 5
178 }
179
180 x if x == OP_IF.to_u8()
182 || x == OP_NOTIF.to_u8()
183 || x == OP_ELSE.to_u8()
184 || x == OP_ENDIF.to_u8() =>
185 {
186 3
187 }
188 x if x == OP_RETURN.to_u8() => 1,
189
190 x if x == OP_DUP.to_u8()
192 || x == OP_DROP.to_u8()
193 || x == OP_SWAP.to_u8()
194 || x == OP_ROT.to_u8() =>
195 {
196 1
197 }
198
199 x if x == OP_ADD.to_u8() || x == OP_SUB.to_u8() => 2,
201
202 _ => 1,
204 }
205 }
206
207 pub fn opcode_count(&self) -> usize {
209 self.script
210 .instructions()
211 .filter(|i| matches!(i, Ok(Instruction::Op(_))))
212 .count()
213 }
214
215 pub fn push_count(&self) -> usize {
217 self.script
218 .instructions()
219 .filter(|i| matches!(i, Ok(Instruction::PushBytes(_))))
220 .count()
221 }
222
223 pub fn estimate_witness_cost(&self) -> usize {
227 let base_cost = self.script.len() * 4;
229
230 let witness_cost = if self.script.is_p2wpkh() {
232 72 + 33 } else if self.script.is_p2wsh() {
235 let complexity = self.complexity_score();
237 (complexity * 10) as usize
238 } else if self.script.is_p2tr() {
239 64
241 } else {
242 0
243 };
244
245 base_cost + witness_cost
246 }
247
248 pub fn has_risky_opcodes(&self) -> bool {
252 for instruction in self.script.instructions() {
253 if let Ok(Instruction::Op(opcode)) = instruction {
254 let op_byte = opcode.to_u8();
255 if (126..=129).contains(&op_byte)
258 || (131..=134).contains(&op_byte)
259 || (141..=142).contains(&op_byte)
260 || (149..=153).contains(&op_byte)
261 {
262 return true;
263 }
264 }
265 }
266 false
267 }
268
269 pub fn script_type(&self) -> ScriptType {
271 if self.script.is_p2pkh() {
272 ScriptType::P2PKH
273 } else if self.script.is_p2sh() {
274 ScriptType::P2SH
275 } else if self.script.is_p2wpkh() {
276 ScriptType::P2WPKH
277 } else if self.script.is_p2wsh() {
278 ScriptType::P2WSH
279 } else if self.script.is_p2tr() {
280 ScriptType::P2TR
281 } else if self.script.is_op_return() {
282 ScriptType::OpReturn
283 } else {
284 ScriptType::NonStandard
285 }
286 }
287
288 pub fn analyze(&self) -> ScriptAnalysis {
290 ScriptAnalysis {
291 script_type: self.script_type(),
292 script_size: self.script.len(),
293 opcode_count: self.opcode_count(),
294 push_count: self.push_count(),
295 complexity_score: self.complexity_score(),
296 estimated_witness_cost: self.estimate_witness_cost(),
297 has_risky_opcodes: self.has_risky_opcodes(),
298 is_standard: self.is_standard_script(),
299 }
300 }
301
302 fn is_standard_script(&self) -> bool {
304 self.script.is_p2pkh()
305 || self.script.is_p2sh()
306 || self.script.is_p2wpkh()
307 || self.script.is_p2wsh()
308 || self.script.is_p2tr()
309 || self.script.is_op_return()
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
315pub enum ScriptType {
316 P2PKH,
318 P2SH,
320 P2WPKH,
322 P2WSH,
324 P2TR,
326 OpReturn,
328 NonStandard,
330}
331
332impl fmt::Display for ScriptType {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 match self {
335 ScriptType::P2PKH => write!(f, "P2PKH"),
336 ScriptType::P2SH => write!(f, "P2SH"),
337 ScriptType::P2WPKH => write!(f, "P2WPKH"),
338 ScriptType::P2WSH => write!(f, "P2WSH"),
339 ScriptType::P2TR => write!(f, "P2TR"),
340 ScriptType::OpReturn => write!(f, "OP_RETURN"),
341 ScriptType::NonStandard => write!(f, "Non-Standard"),
342 }
343 }
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct ScriptAnalysis {
349 pub script_type: ScriptType,
351 pub script_size: usize,
353 pub opcode_count: usize,
355 pub push_count: usize,
357 pub complexity_score: u32,
359 pub estimated_witness_cost: usize,
361 pub has_risky_opcodes: bool,
363 pub is_standard: bool,
365}
366
367impl fmt::Display for ScriptAnalysis {
368 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
369 writeln!(f, "Script Analysis:")?;
370 writeln!(f, " Type: {}", self.script_type)?;
371 writeln!(f, " Size: {} bytes", self.script_size)?;
372 writeln!(f, " Opcodes: {}", self.opcode_count)?;
373 writeln!(f, " Push operations: {}", self.push_count)?;
374 writeln!(f, " Complexity: {}", self.complexity_score)?;
375 writeln!(
376 f,
377 " Estimated witness cost: {} WU",
378 self.estimated_witness_cost
379 )?;
380 writeln!(f, " Standard script: {}", self.is_standard)?;
381 writeln!(f, " Risky opcodes: {}", self.has_risky_opcodes)?;
382 Ok(())
383 }
384}
385
386pub struct ScriptTemplateBuilder;
388
389impl ScriptTemplateBuilder {
390 pub fn p2pkh(pubkey_hash: &[u8; 20]) -> Result<ScriptBuf, BitcoinError> {
392 use bitcoin::hashes::Hash;
393 let hash = bitcoin::hashes::hash160::Hash::from_byte_array(*pubkey_hash);
394 Ok(ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::from_raw_hash(
395 hash,
396 )))
397 }
398
399 pub fn op_return(data: &[u8]) -> Result<ScriptBuf, BitcoinError> {
401 if data.len() > 80 {
402 return Err(BitcoinError::InvalidInput(
403 "OP_RETURN data too large (max 80 bytes)".to_string(),
404 ));
405 }
406
407 let mut script_bytes = vec![OP_RETURN.to_u8()];
409
410 if data.is_empty() {
412 } else if data.len() <= 75 {
414 script_bytes.push(data.len() as u8);
415 script_bytes.extend_from_slice(data);
416 } else {
417 script_bytes.push(0x4c); script_bytes.push(data.len() as u8);
420 script_bytes.extend_from_slice(data);
421 }
422
423 Ok(ScriptBuf::from_bytes(script_bytes))
424 }
425
426 pub fn timelock_cltv(locktime: u32, pubkey_hash: &[u8; 20]) -> Result<ScriptBuf, BitcoinError> {
428 let script = ScriptBuf::builder()
430 .push_int(locktime as i64)
431 .push_opcode(OP_CLTV)
432 .push_opcode(OP_DROP)
433 .push_opcode(OP_DUP)
434 .push_opcode(OP_HASH160)
435 .push_slice(pubkey_hash)
436 .push_opcode(OP_EQUALVERIFY)
437 .push_opcode(OP_CHECKSIG)
438 .into_script();
439
440 Ok(script)
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use bitcoin::hashes::Hash;
448
449 #[test]
450 fn test_disassemble_empty_script() {
451 let script = ScriptBuf::new();
452 let disassembler = ScriptDisassembler::new();
453 let asm = disassembler.disassemble(&script);
454 assert_eq!(asm, "");
455 }
456
457 #[test]
458 fn test_disassemble_compact() {
459 let pubkey_hash = [0u8; 20];
460 let hash = bitcoin::hashes::hash160::Hash::from_byte_array(pubkey_hash);
461 let script = ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::from_raw_hash(hash));
462 let disassembler = ScriptDisassembler::new();
463 let compact = disassembler.disassemble_compact(&script);
464 assert_eq!(compact, "P2PKH");
465 }
466
467 #[test]
468 fn test_script_analyzer_p2pkh() {
469 let pubkey_hash = [0u8; 20];
470 let hash = bitcoin::hashes::hash160::Hash::from_byte_array(pubkey_hash);
471 let script = ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::from_raw_hash(hash));
472 let analyzer = ScriptAnalyzer::new(&script);
473 assert_eq!(analyzer.script_type(), ScriptType::P2PKH);
474 assert!(analyzer.complexity_score() > 0);
475 }
476
477 #[test]
478 fn test_script_analyzer_p2wpkh() {
479 let pubkey_hash = [0u8; 20];
480 let hash = bitcoin::hashes::hash160::Hash::from_byte_array(pubkey_hash);
481 let script = ScriptBuf::new_p2wpkh(&bitcoin::WPubkeyHash::from_raw_hash(hash));
482 let analyzer = ScriptAnalyzer::new(&script);
483 assert_eq!(analyzer.script_type(), ScriptType::P2WPKH);
484 }
485
486 #[test]
487 fn test_complexity_score() {
488 let pubkey_hash = [0u8; 20];
489 let hash = bitcoin::hashes::hash160::Hash::from_byte_array(pubkey_hash);
490 let script = ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::from_raw_hash(hash));
491 let analyzer = ScriptAnalyzer::new(&script);
492 let score = analyzer.complexity_score();
493 assert!(score > 0);
494 }
495
496 #[test]
497 fn test_opcode_count() {
498 let pubkey_hash = [0u8; 20];
499 let hash = bitcoin::hashes::hash160::Hash::from_byte_array(pubkey_hash);
500 let script = ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::from_raw_hash(hash));
501 let analyzer = ScriptAnalyzer::new(&script);
502 assert!(analyzer.opcode_count() > 0);
503 }
504
505 #[test]
506 fn test_push_count() {
507 let pubkey_hash = [0u8; 20];
508 let hash = bitcoin::hashes::hash160::Hash::from_byte_array(pubkey_hash);
509 let script = ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::from_raw_hash(hash));
510 let analyzer = ScriptAnalyzer::new(&script);
511 assert!(analyzer.push_count() > 0);
512 }
513
514 #[test]
515 fn test_witness_cost_estimation() {
516 let pubkey_hash = [0u8; 20];
517 let hash = bitcoin::hashes::hash160::Hash::from_byte_array(pubkey_hash);
518 let script = ScriptBuf::new_p2wpkh(&bitcoin::WPubkeyHash::from_raw_hash(hash));
519 let analyzer = ScriptAnalyzer::new(&script);
520 let cost = analyzer.estimate_witness_cost();
521 assert!(cost > 0);
522 }
523
524 #[test]
525 fn test_no_risky_opcodes() {
526 let pubkey_hash = [0u8; 20];
527 let hash = bitcoin::hashes::hash160::Hash::from_byte_array(pubkey_hash);
528 let script = ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::from_raw_hash(hash));
529 let analyzer = ScriptAnalyzer::new(&script);
530 assert!(!analyzer.has_risky_opcodes());
531 }
532
533 #[test]
534 fn test_script_analysis() {
535 let pubkey_hash = [0u8; 20];
536 let hash = bitcoin::hashes::hash160::Hash::from_byte_array(pubkey_hash);
537 let script = ScriptBuf::new_p2pkh(&bitcoin::PubkeyHash::from_raw_hash(hash));
538 let analyzer = ScriptAnalyzer::new(&script);
539 let analysis = analyzer.analyze();
540 assert_eq!(analysis.script_type, ScriptType::P2PKH);
541 assert!(analysis.is_standard);
542 assert!(!analysis.has_risky_opcodes);
543 }
544
545 #[test]
546 fn test_op_return_template() {
547 let data = b"Hello, Bitcoin!";
548 let script = ScriptTemplateBuilder::op_return(data).unwrap();
549 let analyzer = ScriptAnalyzer::new(&script);
550 assert_eq!(analyzer.script_type(), ScriptType::OpReturn);
551 }
552
553 #[test]
554 fn test_op_return_too_large() {
555 let data = vec![0u8; 81]; let result = ScriptTemplateBuilder::op_return(&data);
557 assert!(result.is_err());
558 }
559
560 #[test]
561 fn test_timelock_cltv_template() {
562 let pubkey_hash = [0u8; 20];
563 let locktime = 600000;
564 let script = ScriptTemplateBuilder::timelock_cltv(locktime, &pubkey_hash).unwrap();
565 let analyzer = ScriptAnalyzer::new(&script);
566 assert!(analyzer.complexity_score() > 10); }
568}