use crate::extension;
use bitcoin::consensus::encode::Decodable;
use bitcoin::consensus::encode::Encodable;
use bitcoin::consensus::encode::{self};
use bitcoin::io;
use bitcoin::ScriptBuf;
use bitcoin::Transaction;
use bitcoin::TxOut;
use bitcoin::VarInt;
use bitcoin::Witness;
use std::collections::BTreeSet;
use std::io::Cursor;
use std::io::Read;
const PACKET_TYPE: u8 = 0x01;
const MAX_ENTRY_COUNT: usize = 1_000;
const MAX_SCRIPT_LENGTH: usize = 10_000;
const MAX_WITNESS_LENGTH: usize = 1_000_000;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IntrospectorEntry {
pub vin: u16,
pub script: ScriptBuf,
pub witness: Witness,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Packet {
pub entries: Vec<IntrospectorEntry>,
}
#[derive(Debug, thiserror::Error)]
pub enum PacketError {
#[error("empty packet")]
EmptyPacket,
#[error("max introspector entry count exceeded, max={max} got={got}")]
EntryCountExceeded { max: usize, got: usize },
#[error("empty script at entry {0}")]
EmptyScript(usize),
#[error("duplicate vin {vin} at entry {entry}")]
DuplicateVin { vin: u16, entry: usize },
#[error("max introspector script length exceeded, max={max} got={got}")]
ScriptLengthExceeded { max: usize, got: usize },
#[error("max introspector witness length exceeded, max={max} got={got}")]
WitnessLengthExceeded { max: usize, got: usize },
#[error("failed to encode packet: {0}")]
Encode(io::Error),
#[error("failed to decode witness: {0}")]
WitnessDecode(encode::Error),
#[error("failed to decode packet: {0}")]
Decode(encode::Error),
#[error("failed to read packet: {0}")]
Read(std::io::Error),
#[error("introspector payload length overflows")]
PayloadLengthOverflow,
#[error("truncated introspector payload, expected {expected} bytes got {got}")]
TruncatedPayload { expected: usize, got: usize },
#[error("unexpected {0} trailing bytes")]
TrailingBytes(usize),
#[error("failed to process extension packet: {0}")]
Extension(#[from] extension::ExtensionError),
}
impl Packet {
pub fn new(entries: Vec<IntrospectorEntry>) -> Result<Self, PacketError> {
let packet = Self { entries };
packet.validate()?;
Ok(packet)
}
pub fn validate(&self) -> Result<(), PacketError> {
if self.entries.is_empty() {
return Err(PacketError::EmptyPacket);
}
if self.entries.len() > MAX_ENTRY_COUNT {
return Err(PacketError::EntryCountExceeded {
max: MAX_ENTRY_COUNT,
got: self.entries.len(),
});
}
let mut seen = BTreeSet::new();
for (index, entry) in self.entries.iter().enumerate() {
if entry.script.is_empty() {
return Err(PacketError::EmptyScript(index));
}
let script_len = entry.script.as_bytes().len();
if script_len > MAX_SCRIPT_LENGTH {
return Err(PacketError::ScriptLengthExceeded {
max: MAX_SCRIPT_LENGTH,
got: script_len,
});
}
if !seen.insert(entry.vin) {
return Err(PacketError::DuplicateVin {
vin: entry.vin,
entry: index,
});
}
}
Ok(())
}
pub fn encode(&self) -> Result<Vec<u8>, PacketError> {
self.validate()?;
let mut bytes = Vec::new();
VarInt(self.entries.len() as u64)
.consensus_encode(&mut bytes)
.map_err(PacketError::Encode)?;
for entry in &self.entries {
bytes.extend_from_slice(&entry.vin.to_le_bytes());
let script = entry.script.as_bytes();
VarInt(script.len() as u64)
.consensus_encode(&mut bytes)
.map_err(PacketError::Encode)?;
bytes.extend_from_slice(script);
let witness = encode::serialize(&entry.witness);
if witness.len() > MAX_WITNESS_LENGTH {
return Err(PacketError::WitnessLengthExceeded {
max: MAX_WITNESS_LENGTH,
got: witness.len(),
});
}
VarInt(witness.len() as u64)
.consensus_encode(&mut bytes)
.map_err(PacketError::Encode)?;
bytes.extend_from_slice(&witness);
}
Ok(bytes)
}
pub fn decode(data: &[u8]) -> Result<Self, PacketError> {
let mut reader = Cursor::new(data);
let entry_count = VarInt::consensus_decode(&mut reader)
.map_err(PacketError::Decode)?
.0 as usize;
if entry_count > MAX_ENTRY_COUNT {
return Err(PacketError::EntryCountExceeded {
max: MAX_ENTRY_COUNT,
got: entry_count,
});
}
let mut entries = Vec::with_capacity(entry_count);
for _ in 0..entry_count {
let mut vin = [0_u8; 2];
reader.read_exact(&mut vin).map_err(PacketError::Read)?;
let vin = u16::from_le_bytes(vin);
let script_len = VarInt::consensus_decode(&mut reader)
.map_err(PacketError::Decode)?
.0 as usize;
if script_len > MAX_SCRIPT_LENGTH {
return Err(PacketError::ScriptLengthExceeded {
max: MAX_SCRIPT_LENGTH,
got: script_len,
});
}
let mut script = vec![0_u8; script_len];
reader.read_exact(&mut script).map_err(PacketError::Read)?;
let witness_len = VarInt::consensus_decode(&mut reader)
.map_err(PacketError::Decode)?
.0 as usize;
if witness_len > MAX_WITNESS_LENGTH {
return Err(PacketError::WitnessLengthExceeded {
max: MAX_WITNESS_LENGTH,
got: witness_len,
});
}
let mut witness_bytes = vec![0_u8; witness_len];
reader
.read_exact(&mut witness_bytes)
.map_err(PacketError::Read)?;
let mut witness_reader = witness_bytes.as_slice();
let witness = Witness::consensus_decode(&mut witness_reader)
.map_err(PacketError::WitnessDecode)?;
if !witness_reader.is_empty() {
return Err(PacketError::TrailingBytes(witness_reader.len()));
}
entries.push(IntrospectorEntry {
vin,
script: ScriptBuf::from_bytes(script),
witness,
});
}
let remaining = data.len() - reader.position() as usize;
if remaining != 0 {
return Err(PacketError::TrailingBytes(remaining));
}
Self::new(entries)
}
pub fn to_txout(&self) -> Result<TxOut, PacketError> {
let packet = self.encode()?;
Ok(extension::packet_txout(PACKET_TYPE, &packet))
}
}
pub fn add_packet_to_psbt(psbt: &mut bitcoin::Psbt, packet: &Packet) -> Result<(), PacketError> {
let packet = packet.encode()?;
extension::add_packet_to_psbt(psbt, PACKET_TYPE, &packet)?;
Ok(())
}
pub fn find_packet(tx: &Transaction) -> Result<Option<Packet>, PacketError> {
let Some(payload) = extension::find_packet_payload(tx, PACKET_TYPE)? else {
return Ok(None);
};
Packet::decode(payload).map(Some)
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::absolute;
use bitcoin::hex::DisplayHex;
use bitcoin::hex::FromHex;
use bitcoin::script::PushBytesBuf;
use bitcoin::transaction;
use bitcoin::Amount;
use bitcoin::TxIn;
fn witness(items: &[&str]) -> Witness {
Witness::from_slice(
&items
.iter()
.map(|item| Vec::from_hex(item).unwrap())
.collect::<Vec<_>>(),
)
}
fn tx_with_op_return_payload(payload: Vec<u8>) -> Transaction {
let push_bytes = PushBytesBuf::try_from(payload).unwrap();
Transaction {
version: transaction::Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![],
output: vec![TxOut {
value: Amount::ZERO,
script_pubkey: ScriptBuf::builder()
.push_opcode(bitcoin::opcodes::all::OP_RETURN)
.push_slice(push_bytes)
.into_script(),
}],
}
}
#[test]
fn matches_go_vectors() {
let packet = Packet::new(vec![IntrospectorEntry {
vin: 0,
script: ScriptBuf::from_bytes(Vec::from_hex("010203").unwrap()),
witness: witness(&["0405"]),
}])
.unwrap();
assert_eq!(
packet.encode().unwrap().to_lower_hex_string(),
"010000030102030401020405"
);
let packet = Packet::new(vec![
IntrospectorEntry {
vin: 0,
script: ScriptBuf::from_bytes(Vec::from_hex("01").unwrap()),
witness: witness(&["02"]),
},
IntrospectorEntry {
vin: 1,
script: ScriptBuf::from_bytes(Vec::from_hex("0304").unwrap()),
witness: witness(&["05", "06"]),
},
IntrospectorEntry {
vin: 5,
script: ScriptBuf::from_bytes(Vec::from_hex("07").unwrap()),
witness: Witness::default(),
},
])
.unwrap();
assert_eq!(
packet.encode().unwrap().to_lower_hex_string(),
"0300000101030101020100020304050201050106050001070100"
);
}
#[test]
fn decode_rejects_invalid_packets() {
assert!(matches!(Packet::new(vec![]), Err(PacketError::EmptyPacket)));
assert!(matches!(
Packet::decode(&Vec::from_hex("0000000101ff").unwrap()),
Err(PacketError::TrailingBytes(5))
));
assert!(matches!(
Packet::decode(&Vec::from_hex("010000fd1127").unwrap()),
Err(PacketError::ScriptLengthExceeded { .. })
));
assert!(matches!(
Packet::decode(&Vec::from_hex("01000001510200ff").unwrap()),
Err(PacketError::TrailingBytes(1))
));
}
#[test]
fn find_packet_rejects_invalid_extension_payload_lengths() {
let tx = tx_with_op_return_payload(Vec::from_hex("41524b010200").unwrap());
assert!(matches!(
find_packet(&tx),
Err(PacketError::Extension(
extension::ExtensionError::TruncatedPacketPayload {
expected: 7,
got: 6,
}
))
));
let tx = tx_with_op_return_payload(Vec::from_hex("41524b010100ff").unwrap());
assert!(matches!(
find_packet(&tx),
Err(PacketError::Extension(
extension::ExtensionError::TruncatedPacketLength
))
));
let tx = tx_with_op_return_payload(Vec::from_hex("41524b01ffffffffffffffffff").unwrap());
assert!(matches!(
find_packet(&tx),
Err(PacketError::Extension(
extension::ExtensionError::TruncatedPacketLength
))
));
}
#[test]
fn add_and_find_packet() {
let packet = Packet::new(vec![IntrospectorEntry {
vin: 0,
script: ScriptBuf::from_bytes(Vec::from_hex("51").unwrap()),
witness: Witness::default(),
}])
.unwrap();
let mut psbt = bitcoin::Psbt::from_unsigned_tx(Transaction {
version: transaction::Version::TWO,
lock_time: absolute::LockTime::ZERO,
input: vec![TxIn::default()],
output: vec![
TxOut {
value: Amount::from_sat(1_000),
script_pubkey: ScriptBuf::new(),
},
TxOut {
value: Amount::ZERO,
script_pubkey: ScriptBuf::new(),
},
],
})
.unwrap();
add_packet_to_psbt(&mut psbt, &packet).unwrap();
assert_eq!(psbt.unsigned_tx.output.len(), 3);
let found = find_packet(&psbt.unsigned_tx).unwrap().unwrap();
assert_eq!(found, packet);
}
#[test]
fn duplicate_vins_are_rejected() {
let err = Packet::new(vec![
IntrospectorEntry {
vin: 1,
script: ScriptBuf::from_bytes(vec![0x51]),
witness: Witness::default(),
},
IntrospectorEntry {
vin: 1,
script: ScriptBuf::from_bytes(vec![0x52]),
witness: Witness::default(),
},
])
.unwrap_err();
assert!(matches!(
err,
PacketError::DuplicateVin { vin: 1, entry: 1 }
));
}
}