use crate::script::error::ScriptError;
use crate::script::op::Op;
use crate::script::script_chunk::ScriptChunk;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Script {
chunks: Vec<ScriptChunk>,
}
impl Script {
pub fn new() -> Self {
Script { chunks: Vec::new() }
}
pub fn from_binary(bytes: &[u8]) -> Self {
Script {
chunks: Self::parse_chunks(bytes),
}
}
pub fn from_hex(hex: &str) -> Result<Self, ScriptError> {
if hex.is_empty() {
return Ok(Script::new());
}
if !hex.len().is_multiple_of(2) {
return Err(ScriptError::InvalidFormat(
"hex string has odd length".to_string(),
));
}
let bytes = hex_to_bytes(hex).map_err(ScriptError::InvalidFormat)?;
Ok(Script::from_binary(&bytes))
}
pub fn from_asm(asm: &str) -> Self {
if asm.is_empty() {
return Script::new();
}
let mut chunks = Vec::new();
let tokens: Vec<&str> = asm.split(' ').collect();
let mut i = 0;
while i < tokens.len() {
let token = tokens[i];
if token == "0" {
chunks.push(ScriptChunk::new_opcode(Op::Op0));
i += 1;
continue;
}
if token == "-1" {
chunks.push(ScriptChunk::new_opcode(Op::Op1Negate));
i += 1;
continue;
}
if let Some(op) = Op::from_name(token) {
if op == Op::OpPushData1 || op == Op::OpPushData2 || op == Op::OpPushData4 {
if i + 2 < tokens.len() {
let hex_data = tokens[i + 2];
let data = hex_to_bytes(hex_data).unwrap_or_default();
chunks.push(ScriptChunk::new_raw(op.to_byte(), Some(data)));
i += 3;
} else {
chunks.push(ScriptChunk::new_opcode(op));
i += 1;
}
} else {
chunks.push(ScriptChunk::new_opcode(op));
i += 1;
}
continue;
}
let mut hex = token.to_string();
if !hex.len().is_multiple_of(2) {
hex = format!("0{}", hex);
}
if let Ok(data) = hex_to_bytes(&hex) {
let len = data.len();
let op_byte = if len < 0x4c {
len as u8
} else if len < 256 {
Op::OpPushData1.to_byte()
} else if len < 65536 {
Op::OpPushData2.to_byte()
} else {
Op::OpPushData4.to_byte()
};
chunks.push(ScriptChunk::new_raw(op_byte, Some(data)));
}
i += 1;
}
Script { chunks }
}
pub fn from_chunks(chunks: Vec<ScriptChunk>) -> Self {
Script { chunks }
}
pub fn to_binary(&self) -> Vec<u8> {
let mut out = Vec::new();
for (i, chunk) in self.chunks.iter().enumerate() {
let serialized = chunk.serialize();
out.extend_from_slice(&serialized);
if chunk.op == Op::OpReturn && chunk.data.is_some() {
let _ = i; }
}
out
}
pub fn to_hex(&self) -> String {
let bytes = self.to_binary();
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
pub fn to_asm(&self) -> String {
self.chunks
.iter()
.map(|c| c.to_asm())
.collect::<Vec<_>>()
.join(" ")
}
pub fn chunks(&self) -> &[ScriptChunk] {
&self.chunks
}
pub fn len(&self) -> usize {
self.chunks.len()
}
pub fn is_empty(&self) -> bool {
self.chunks.is_empty()
}
pub fn find_and_delete(&self, target: &Script) -> Script {
let target_bytes = target.to_binary();
let target_len = target_bytes.len();
if target_len == 0 {
return self.clone();
}
let target_op = target_bytes[0];
if !self.chunks.iter().any(|c| c.op_byte == target_op) {
return self.clone();
}
let mut result_chunks = self.chunks.clone();
Self::retain_non_matching(&mut result_chunks, target_op, target_len, &target_bytes);
Script {
chunks: result_chunks,
}
}
pub fn find_and_delete_owned(mut self, target: &Script) -> Script {
let target_bytes = target.to_binary();
let target_len = target_bytes.len();
if target_len == 0 {
return self;
}
let target_op = target_bytes[0];
if !self.chunks.iter().any(|c| c.op_byte == target_op) {
return self;
}
Self::retain_non_matching(&mut self.chunks, target_op, target_len, &target_bytes);
self
}
fn retain_non_matching(
chunks: &mut Vec<ScriptChunk>,
target_op: u8,
target_len: usize,
target_bytes: &[u8],
) {
chunks.retain(|chunk| {
if chunk.op_byte != target_op {
return true;
}
let data = chunk.data.as_deref().unwrap_or(&[]);
let data_len = data.len();
if data_len == 0 && chunk.data.is_none() {
return target_len != 1;
}
if chunk.op == Op::OpReturn || chunk.op_byte < Op::OpPushData1.to_byte() {
if target_len != 1 + data_len {
return true;
}
return target_bytes[1..] != *data;
}
if chunk.op == Op::OpPushData1 {
if target_len != 2 + data_len {
return true;
}
if target_bytes[1] != (data_len & 0xff) as u8 {
return true;
}
return target_bytes[2..] != *data;
}
if chunk.op == Op::OpPushData2 {
if target_len != 3 + data_len {
return true;
}
if target_bytes[1] != (data_len & 0xff) as u8 {
return true;
}
if target_bytes[2] != ((data_len >> 8) & 0xff) as u8 {
return true;
}
return target_bytes[3..] != *data;
}
if chunk.op == Op::OpPushData4 {
if target_len != 5 + data_len {
return true;
}
let size = data_len as u32;
if target_bytes[1] != (size & 0xff) as u8 {
return true;
}
if target_bytes[2] != ((size >> 8) & 0xff) as u8 {
return true;
}
if target_bytes[3] != ((size >> 16) & 0xff) as u8 {
return true;
}
if target_bytes[4] != ((size >> 24) & 0xff) as u8 {
return true;
}
return target_bytes[5..] != *data;
}
true
});
}
pub fn is_push_only(&self) -> bool {
for chunk in &self.chunks {
if chunk.op_byte > Op::Op16.to_byte() {
return false;
}
}
true
}
fn parse_chunks(bytes: &[u8]) -> Vec<ScriptChunk> {
let mut chunks = Vec::new();
let length = bytes.len();
let mut pos = 0;
let mut in_conditional_block: i32 = 0;
while pos < length {
let op_byte = bytes[pos];
pos += 1;
if op_byte == Op::OpReturn.to_byte() && in_conditional_block == 0 {
let remaining = bytes[pos..].to_vec();
chunks.push(ScriptChunk::new_raw(
op_byte,
if remaining.is_empty() {
None
} else {
Some(remaining)
},
));
break;
}
if op_byte == Op::OpIf.to_byte()
|| op_byte == Op::OpNotIf.to_byte()
|| op_byte == Op::OpVerIf.to_byte()
|| op_byte == Op::OpVerNotIf.to_byte()
{
in_conditional_block += 1;
} else if op_byte == Op::OpEndIf.to_byte() {
in_conditional_block -= 1;
}
if op_byte > 0 && op_byte < Op::OpPushData1.to_byte() {
let push_len = op_byte as usize;
let end = (pos + push_len).min(length);
let data = bytes[pos..end].to_vec();
chunks.push(ScriptChunk::new_raw(op_byte, Some(data)));
pos = end;
} else if op_byte == Op::OpPushData1.to_byte() {
let push_len = if pos < length { bytes[pos] as usize } else { 0 };
pos += 1;
let end = (pos + push_len).min(length);
let data = bytes[pos..end].to_vec();
chunks.push(ScriptChunk::new_raw(op_byte, Some(data)));
pos = end;
} else if op_byte == Op::OpPushData2.to_byte() {
let b0 = if pos < length { bytes[pos] as usize } else { 0 };
let b1 = if pos + 1 < length {
bytes[pos + 1] as usize
} else {
0
};
let push_len = b0 | (b1 << 8);
pos = (pos + 2).min(length);
let end = (pos + push_len).min(length);
let data = bytes[pos..end].to_vec();
chunks.push(ScriptChunk::new_raw(op_byte, Some(data)));
pos = end;
} else if op_byte == Op::OpPushData4.to_byte() {
let b0 = if pos < length { bytes[pos] as usize } else { 0 };
let b1 = if pos + 1 < length {
bytes[pos + 1] as usize
} else {
0
};
let b2 = if pos + 2 < length {
bytes[pos + 2] as usize
} else {
0
};
let b3 = if pos + 3 < length {
bytes[pos + 3] as usize
} else {
0
};
let push_len = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
pos = (pos + 4).min(length);
let end = (pos + push_len).min(length);
let data = bytes[pos..end].to_vec();
chunks.push(ScriptChunk::new_raw(op_byte, Some(data)));
pos = end;
} else {
chunks.push(ScriptChunk::new_raw(op_byte, None));
}
}
chunks
}
}
impl Default for Script {
fn default() -> Self {
Self::new()
}
}
fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
if !hex.len().is_multiple_of(2) {
return Err("odd hex length".to_string());
}
let mut bytes = Vec::with_capacity(hex.len() / 2);
for i in (0..hex.len()).step_by(2) {
let byte = u8::from_str_radix(&hex[i..i + 2], 16)
.map_err(|_| format!("invalid hex at position {}", i))?;
bytes.push(byte);
}
Ok(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
fn bytes_to_hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
#[test]
fn test_binary_roundtrip_empty() {
let script = Script::from_binary(&[]);
assert!(script.is_empty());
assert_eq!(script.to_binary(), Vec::<u8>::new());
}
#[test]
fn test_binary_roundtrip_p2pkh() {
let pubkey_hash = [0xab; 20];
let mut script_bytes = vec![0x76, 0xa9, 0x14]; script_bytes.extend_from_slice(&pubkey_hash);
script_bytes.push(0x88); script_bytes.push(0xac);
let script = Script::from_binary(&script_bytes);
let rt = script.to_binary();
assert_eq!(rt, script_bytes, "binary round-trip failed for P2PKH");
assert_eq!(script.len(), 5);
}
#[test]
fn test_binary_roundtrip_pushdata1() {
let mut script_bytes = vec![0x4c, 100]; script_bytes.extend_from_slice(&[0xcc; 100]);
let script = Script::from_binary(&script_bytes);
assert_eq!(script.to_binary(), script_bytes);
}
#[test]
fn test_binary_roundtrip_pushdata2() {
let mut script_bytes = vec![0x4d, 0x2c, 0x01]; script_bytes.extend_from_slice(&[0xdd; 300]);
let script = Script::from_binary(&script_bytes);
assert_eq!(script.to_binary(), script_bytes);
}
#[test]
fn test_hex_roundtrip() {
let hex = "76a914abababababababababababababababababababab88ac";
let script = Script::from_hex(hex).unwrap();
assert_eq!(script.to_hex(), hex);
}
#[test]
fn test_from_hex_empty() {
let script = Script::from_hex("").unwrap();
assert!(script.is_empty());
}
#[test]
fn test_from_hex_odd_length() {
let result = Script::from_hex("abc");
assert!(result.is_err());
}
#[test]
fn test_asm_roundtrip_p2pkh() {
let asm =
"OP_DUP OP_HASH160 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa OP_EQUALVERIFY OP_CHECKSIG";
let script = Script::from_asm(asm);
let result_asm = script.to_asm();
assert_eq!(result_asm, asm);
}
#[test]
fn test_asm_zero_and_negative_one() {
let asm = "0 -1 OP_ADD";
let script = Script::from_asm(asm);
assert_eq!(script.to_asm(), "0 -1 OP_ADD");
}
#[test]
fn test_op_return_outside_conditional() {
let mut script_bytes = vec![0x6a]; script_bytes.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]);
let script = Script::from_binary(&script_bytes);
assert_eq!(script.len(), 1, "OP_RETURN + data should be one chunk");
assert_eq!(
script.chunks()[0].data.as_ref().unwrap(),
&[0x01, 0x02, 0x03, 0x04]
);
assert_eq!(script.to_binary(), script_bytes);
}
#[test]
fn test_op_return_inside_conditional() {
let script_bytes = vec![
0x63, 0x6a, 0x68, ];
let script = Script::from_binary(&script_bytes);
assert_eq!(
script.len(),
3,
"OP_RETURN inside conditional should be a standalone opcode"
);
assert!(
script.chunks()[1].data.is_none(),
"OP_RETURN inside conditional should have no data"
);
assert_eq!(script.to_binary(), script_bytes);
}
#[test]
fn test_op_return_no_data() {
let script_bytes = vec![0x6a];
let script = Script::from_binary(&script_bytes);
assert_eq!(script.len(), 1);
assert!(script.chunks()[0].data.is_none());
assert_eq!(script.to_binary(), script_bytes);
}
#[test]
fn test_find_and_delete_simple() {
let script = Script::from_binary(&[0x51, 0x52, 0x53, 0x52]);
let target = Script::from_binary(&[0x52]);
let result = script.find_and_delete(&target);
assert_eq!(result.to_binary(), vec![0x51, 0x53]);
}
#[test]
fn test_find_and_delete_data_push() {
let script_bytes = vec![0x03, 0xaa, 0xbb, 0xcc, 0x76];
let script = Script::from_binary(&script_bytes);
let target = Script::from_binary(&[0x03, 0xaa, 0xbb, 0xcc]);
let result = script.find_and_delete(&target);
assert_eq!(result.to_binary(), vec![0x76]); }
#[test]
fn test_find_and_delete_empty_target() {
let script = Script::from_binary(&[0x76, 0x76]);
let target = Script::new();
let result = script.find_and_delete(&target);
assert_eq!(result.to_binary(), vec![0x76, 0x76]);
}
#[test]
fn test_is_push_only() {
let push_script = Script::from_binary(&[0x51, 0x03, 0xaa, 0xbb, 0xcc, 0x60]);
assert!(push_script.is_push_only());
let non_push = Script::from_binary(&[0x76]);
assert!(!non_push.is_push_only());
}
#[test]
fn test_from_chunks() {
let chunks = vec![
ScriptChunk::new_opcode(Op::OpDup),
ScriptChunk::new_opcode(Op::OpCheckSig),
];
let script = Script::from_chunks(chunks);
assert_eq!(script.len(), 2);
assert_eq!(script.to_binary(), vec![0x76, 0xac]);
}
#[test]
fn test_complex_script_roundtrip() {
let hex = "5121031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f2103acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe52ae";
let script = Script::from_hex(hex).unwrap();
assert_eq!(script.to_hex(), hex);
}
#[test]
fn test_nested_conditional_op_return() {
let script_bytes = vec![
0x63, 0x63, 0x6a, 0x68, 0x68, ];
let script = Script::from_binary(&script_bytes);
assert_eq!(script.len(), 5);
assert!(script.chunks()[2].data.is_none());
assert_eq!(script.to_binary(), script_bytes);
}
}