use crate::{
cursor::Cursor,
error::{ParseError, ParseResult},
};
pub const MAX_SCRIPT_SIZE: usize = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Script<'a> {
pub bytes: &'a [u8],
}
impl<'a> Script<'a> {
#[inline]
pub fn parse(cursor: &mut Cursor<'a>) -> ParseResult<Self> {
let bytes = cursor.read_var_bytes(MAX_SCRIPT_SIZE)?;
Ok(Self { bytes })
}
#[inline]
pub fn script_type(&self) -> ScriptType<'a> {
ScriptType::classify(self.bytes)
}
#[inline]
pub fn len(&self) -> usize {
self.bytes.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
pub fn instructions(&self) -> Instructions<'a> {
Instructions {
data: self.bytes,
pos: 0,
errored: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScriptType<'a> {
P2PKH {
pubkey_hash: &'a [u8; 20],
},
P2SH {
script_hash: &'a [u8; 20],
},
P2WPKH {
pubkey_hash: &'a [u8; 20],
},
P2WSH {
script_hash: &'a [u8; 32],
},
P2TR {
x_only_pubkey: &'a [u8; 32],
},
P2PK {
pubkey: &'a [u8],
},
OpReturn {
data: &'a [u8],
},
Multisig {
required: u8,
total: u8,
},
Unknown,
}
impl<'a> ScriptType<'a> {
fn classify(b: &'a [u8]) -> Self {
match b {
[0x76, 0xa9, 0x14, hash @ .., 0x88, 0xac] if hash.len() == 20 => ScriptType::P2PKH {
pubkey_hash: hash.try_into().unwrap(),
},
[0xa9, 0x14, hash @ .., 0x87] if hash.len() == 20 => ScriptType::P2SH {
script_hash: hash.try_into().unwrap(),
},
[0x00, 0x14, hash @ ..] if hash.len() == 20 => ScriptType::P2WPKH {
pubkey_hash: hash.try_into().unwrap(),
},
[0x00, 0x20, hash @ ..] if hash.len() == 32 => ScriptType::P2WSH {
script_hash: hash.try_into().unwrap(),
},
[0x51, 0x20, pk @ ..] if pk.len() == 32 => ScriptType::P2TR {
x_only_pubkey: pk.try_into().unwrap(),
},
[0x21, pk @ .., 0xac] if pk.len() == 33 => ScriptType::P2PK { pubkey: pk },
[0x41, pk @ .., 0xac] if pk.len() == 65 => ScriptType::P2PK { pubkey: pk },
[0x6a, rest @ ..] => ScriptType::OpReturn { data: rest },
_ => ScriptType::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Instruction<'a> {
PushBytes(&'a [u8]),
Op(u8),
}
pub struct Instructions<'a> {
data: &'a [u8],
pos: usize,
errored: bool,
}
impl<'a> Iterator for Instructions<'a> {
type Item = ParseResult<Instruction<'a>>;
fn next(&mut self) -> Option<Self::Item> {
if self.errored || self.pos >= self.data.len() {
return None;
}
let op = self.data[self.pos];
self.pos += 1;
let push_len: Option<Result<usize, ParseError>> = match op {
0x00 => Some(Ok(0)),
n @ 0x01..=0x4b => Some(Ok(n as usize)),
0x4c => {
let avail = self.data.len() - self.pos;
if avail < 1 {
Some(Err(ParseError::UnexpectedEof {
needed: 1,
available: avail,
}))
} else {
let n = self.data[self.pos] as usize;
self.pos += 1;
Some(Ok(n))
}
}
0x4d => {
let avail = self.data.len() - self.pos;
if avail < 2 {
Some(Err(ParseError::UnexpectedEof {
needed: 2,
available: avail,
}))
} else {
let n =
u16::from_le_bytes([self.data[self.pos], self.data[self.pos + 1]]) as usize;
self.pos += 2;
Some(Ok(n))
}
}
0x4e => {
let avail = self.data.len() - self.pos;
if avail < 4 {
Some(Err(ParseError::UnexpectedEof {
needed: 4,
available: avail,
}))
} else {
let n = u32::from_le_bytes([
self.data[self.pos],
self.data[self.pos + 1],
self.data[self.pos + 2],
self.data[self.pos + 3],
]) as usize;
self.pos += 4;
Some(Ok(n))
}
}
_ => None, };
match push_len {
None => Some(Ok(Instruction::Op(op))),
Some(Err(e)) => {
self.errored = true;
Some(Err(e))
}
Some(Ok(n)) => {
let avail = self.data.len() - self.pos;
if n > avail {
self.errored = true;
Some(Err(ParseError::UnexpectedEof {
needed: n,
available: avail,
}))
} else {
let bytes = &self.data[self.pos..self.pos + n];
self.pos += n;
Some(Ok(Instruction::PushBytes(bytes)))
}
}
}
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
use crate::cursor::Cursor;
use std::vec::Vec;
#[test]
fn p2pkh_classification() {
let script_bytes: &[u8] = &[
0x76, 0xa9, 0x14, 0x89, 0xab, 0xcd, 0xef, 0xab, 0xba, 0xab, 0xba, 0xab, 0xba, 0xab,
0xba, 0xab, 0xba, 0xab, 0xba, 0xab, 0xba, 0xab, 0xba, 0x88, 0xac,
];
let mut raw = Vec::new();
raw.push(script_bytes.len() as u8);
raw.extend_from_slice(script_bytes);
let mut cursor = Cursor::new(&raw);
let script = Script::parse(&mut cursor).unwrap();
assert!(matches!(script.script_type(), ScriptType::P2PKH { .. }));
}
#[test]
fn op_return_classification() {
let script_bytes: &[u8] = &[0x6a, 0x04, 0xde, 0xad, 0xbe, 0xef];
let mut raw = Vec::new();
raw.push(script_bytes.len() as u8);
raw.extend_from_slice(script_bytes);
let mut cursor = Cursor::new(&raw);
let script = Script::parse(&mut cursor).unwrap();
assert!(matches!(script.script_type(), ScriptType::OpReturn { .. }));
}
}