Skip to main content

ark_core/introspector/
packet.rs

1use crate::extension;
2use bitcoin::consensus::encode::Decodable;
3use bitcoin::consensus::encode::Encodable;
4use bitcoin::consensus::encode::{self};
5use bitcoin::io;
6use bitcoin::ScriptBuf;
7use bitcoin::Transaction;
8use bitcoin::TxOut;
9use bitcoin::VarInt;
10use bitcoin::Witness;
11use std::collections::BTreeSet;
12use std::io::Cursor;
13use std::io::Read;
14
15const PACKET_TYPE: u8 = 0x01;
16const MAX_ENTRY_COUNT: usize = 1_000;
17const MAX_SCRIPT_LENGTH: usize = 10_000;
18const MAX_WITNESS_LENGTH: usize = 1_000_000;
19
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct IntrospectorEntry {
22    pub vin: u16,
23    pub script: ScriptBuf,
24    pub witness: Witness,
25}
26
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct Packet {
29    pub entries: Vec<IntrospectorEntry>,
30}
31
32#[derive(Debug, thiserror::Error)]
33pub enum PacketError {
34    #[error("empty packet")]
35    EmptyPacket,
36    #[error("max introspector entry count exceeded, max={max} got={got}")]
37    EntryCountExceeded { max: usize, got: usize },
38    #[error("empty script at entry {0}")]
39    EmptyScript(usize),
40    #[error("duplicate vin {vin} at entry {entry}")]
41    DuplicateVin { vin: u16, entry: usize },
42    #[error("max introspector script length exceeded, max={max} got={got}")]
43    ScriptLengthExceeded { max: usize, got: usize },
44    #[error("max introspector witness length exceeded, max={max} got={got}")]
45    WitnessLengthExceeded { max: usize, got: usize },
46    #[error("failed to encode packet: {0}")]
47    Encode(io::Error),
48    #[error("failed to decode witness: {0}")]
49    WitnessDecode(encode::Error),
50    #[error("failed to decode packet: {0}")]
51    Decode(encode::Error),
52    #[error("failed to read packet: {0}")]
53    Read(std::io::Error),
54    #[error("introspector payload length overflows")]
55    PayloadLengthOverflow,
56    #[error("truncated introspector payload, expected {expected} bytes got {got}")]
57    TruncatedPayload { expected: usize, got: usize },
58    #[error("unexpected {0} trailing bytes")]
59    TrailingBytes(usize),
60    #[error("failed to process extension packet: {0}")]
61    Extension(#[from] extension::ExtensionError),
62}
63
64impl Packet {
65    pub fn new(entries: Vec<IntrospectorEntry>) -> Result<Self, PacketError> {
66        let packet = Self { entries };
67        packet.validate()?;
68        Ok(packet)
69    }
70
71    pub fn validate(&self) -> Result<(), PacketError> {
72        if self.entries.is_empty() {
73            return Err(PacketError::EmptyPacket);
74        }
75
76        if self.entries.len() > MAX_ENTRY_COUNT {
77            return Err(PacketError::EntryCountExceeded {
78                max: MAX_ENTRY_COUNT,
79                got: self.entries.len(),
80            });
81        }
82
83        let mut seen = BTreeSet::new();
84        for (index, entry) in self.entries.iter().enumerate() {
85            if entry.script.is_empty() {
86                return Err(PacketError::EmptyScript(index));
87            }
88
89            let script_len = entry.script.as_bytes().len();
90            if script_len > MAX_SCRIPT_LENGTH {
91                return Err(PacketError::ScriptLengthExceeded {
92                    max: MAX_SCRIPT_LENGTH,
93                    got: script_len,
94                });
95            }
96
97            if !seen.insert(entry.vin) {
98                return Err(PacketError::DuplicateVin {
99                    vin: entry.vin,
100                    entry: index,
101                });
102            }
103        }
104
105        Ok(())
106    }
107
108    pub fn encode(&self) -> Result<Vec<u8>, PacketError> {
109        self.validate()?;
110
111        let mut bytes = Vec::new();
112        VarInt(self.entries.len() as u64)
113            .consensus_encode(&mut bytes)
114            .map_err(PacketError::Encode)?;
115
116        for entry in &self.entries {
117            bytes.extend_from_slice(&entry.vin.to_le_bytes());
118
119            let script = entry.script.as_bytes();
120            VarInt(script.len() as u64)
121                .consensus_encode(&mut bytes)
122                .map_err(PacketError::Encode)?;
123            bytes.extend_from_slice(script);
124
125            let witness = encode::serialize(&entry.witness);
126            if witness.len() > MAX_WITNESS_LENGTH {
127                return Err(PacketError::WitnessLengthExceeded {
128                    max: MAX_WITNESS_LENGTH,
129                    got: witness.len(),
130                });
131            }
132            VarInt(witness.len() as u64)
133                .consensus_encode(&mut bytes)
134                .map_err(PacketError::Encode)?;
135            bytes.extend_from_slice(&witness);
136        }
137
138        Ok(bytes)
139    }
140
141    pub fn decode(data: &[u8]) -> Result<Self, PacketError> {
142        let mut reader = Cursor::new(data);
143        let entry_count = VarInt::consensus_decode(&mut reader)
144            .map_err(PacketError::Decode)?
145            .0 as usize;
146
147        if entry_count > MAX_ENTRY_COUNT {
148            return Err(PacketError::EntryCountExceeded {
149                max: MAX_ENTRY_COUNT,
150                got: entry_count,
151            });
152        }
153
154        let mut entries = Vec::with_capacity(entry_count);
155        for _ in 0..entry_count {
156            let mut vin = [0_u8; 2];
157            reader.read_exact(&mut vin).map_err(PacketError::Read)?;
158            let vin = u16::from_le_bytes(vin);
159
160            let script_len = VarInt::consensus_decode(&mut reader)
161                .map_err(PacketError::Decode)?
162                .0 as usize;
163            if script_len > MAX_SCRIPT_LENGTH {
164                return Err(PacketError::ScriptLengthExceeded {
165                    max: MAX_SCRIPT_LENGTH,
166                    got: script_len,
167                });
168            }
169            let mut script = vec![0_u8; script_len];
170            reader.read_exact(&mut script).map_err(PacketError::Read)?;
171
172            let witness_len = VarInt::consensus_decode(&mut reader)
173                .map_err(PacketError::Decode)?
174                .0 as usize;
175            if witness_len > MAX_WITNESS_LENGTH {
176                return Err(PacketError::WitnessLengthExceeded {
177                    max: MAX_WITNESS_LENGTH,
178                    got: witness_len,
179                });
180            }
181            let mut witness_bytes = vec![0_u8; witness_len];
182            reader
183                .read_exact(&mut witness_bytes)
184                .map_err(PacketError::Read)?;
185            let mut witness_reader = witness_bytes.as_slice();
186            let witness = Witness::consensus_decode(&mut witness_reader)
187                .map_err(PacketError::WitnessDecode)?;
188            if !witness_reader.is_empty() {
189                return Err(PacketError::TrailingBytes(witness_reader.len()));
190            }
191
192            entries.push(IntrospectorEntry {
193                vin,
194                script: ScriptBuf::from_bytes(script),
195                witness,
196            });
197        }
198
199        let remaining = data.len() - reader.position() as usize;
200        if remaining != 0 {
201            return Err(PacketError::TrailingBytes(remaining));
202        }
203
204        Self::new(entries)
205    }
206
207    pub fn to_txout(&self) -> Result<TxOut, PacketError> {
208        let packet = self.encode()?;
209
210        Ok(extension::packet_txout(PACKET_TYPE, &packet))
211    }
212}
213
214/// Add an introspector packet OP_RETURN output to a PSBT.
215///
216/// If the PSBT already has outputs, this inserts the packet before the last
217/// output. Arkade transactions are expected to keep the anchor output last, so
218/// callers must only use this helper with PSBTs that either have no outputs or
219/// whose final output is the anchor.
220pub fn add_packet_to_psbt(psbt: &mut bitcoin::Psbt, packet: &Packet) -> Result<(), PacketError> {
221    let packet = packet.encode()?;
222
223    extension::add_packet_to_psbt(psbt, PACKET_TYPE, &packet)?;
224
225    Ok(())
226}
227
228pub fn find_packet(tx: &Transaction) -> Result<Option<Packet>, PacketError> {
229    let Some(payload) = extension::find_packet_payload(tx, PACKET_TYPE)? else {
230        return Ok(None);
231    };
232
233    Packet::decode(payload).map(Some)
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use bitcoin::absolute;
240    use bitcoin::hex::DisplayHex;
241    use bitcoin::hex::FromHex;
242    use bitcoin::script::PushBytesBuf;
243    use bitcoin::transaction;
244    use bitcoin::Amount;
245    use bitcoin::TxIn;
246
247    fn witness(items: &[&str]) -> Witness {
248        Witness::from_slice(
249            &items
250                .iter()
251                .map(|item| Vec::from_hex(item).unwrap())
252                .collect::<Vec<_>>(),
253        )
254    }
255
256    fn tx_with_op_return_payload(payload: Vec<u8>) -> Transaction {
257        let push_bytes = PushBytesBuf::try_from(payload).unwrap();
258        Transaction {
259            version: transaction::Version::TWO,
260            lock_time: absolute::LockTime::ZERO,
261            input: vec![],
262            output: vec![TxOut {
263                value: Amount::ZERO,
264                script_pubkey: ScriptBuf::builder()
265                    .push_opcode(bitcoin::opcodes::all::OP_RETURN)
266                    .push_slice(push_bytes)
267                    .into_script(),
268            }],
269        }
270    }
271
272    #[test]
273    fn matches_go_vectors() {
274        let packet = Packet::new(vec![IntrospectorEntry {
275            vin: 0,
276            script: ScriptBuf::from_bytes(Vec::from_hex("010203").unwrap()),
277            witness: witness(&["0405"]),
278        }])
279        .unwrap();
280
281        assert_eq!(
282            packet.encode().unwrap().to_lower_hex_string(),
283            "010000030102030401020405"
284        );
285
286        let packet = Packet::new(vec![
287            IntrospectorEntry {
288                vin: 0,
289                script: ScriptBuf::from_bytes(Vec::from_hex("01").unwrap()),
290                witness: witness(&["02"]),
291            },
292            IntrospectorEntry {
293                vin: 1,
294                script: ScriptBuf::from_bytes(Vec::from_hex("0304").unwrap()),
295                witness: witness(&["05", "06"]),
296            },
297            IntrospectorEntry {
298                vin: 5,
299                script: ScriptBuf::from_bytes(Vec::from_hex("07").unwrap()),
300                witness: Witness::default(),
301            },
302        ])
303        .unwrap();
304
305        assert_eq!(
306            packet.encode().unwrap().to_lower_hex_string(),
307            "0300000101030101020100020304050201050106050001070100"
308        );
309    }
310
311    #[test]
312    fn decode_rejects_invalid_packets() {
313        assert!(matches!(Packet::new(vec![]), Err(PacketError::EmptyPacket)));
314        assert!(matches!(
315            Packet::decode(&Vec::from_hex("0000000101ff").unwrap()),
316            Err(PacketError::TrailingBytes(5))
317        ));
318        assert!(matches!(
319            Packet::decode(&Vec::from_hex("010000fd1127").unwrap()),
320            Err(PacketError::ScriptLengthExceeded { .. })
321        ));
322        assert!(matches!(
323            Packet::decode(&Vec::from_hex("01000001510200ff").unwrap()),
324            Err(PacketError::TrailingBytes(1))
325        ));
326    }
327
328    #[test]
329    fn find_packet_rejects_invalid_extension_payload_lengths() {
330        let tx = tx_with_op_return_payload(Vec::from_hex("41524b010200").unwrap());
331        assert!(matches!(
332            find_packet(&tx),
333            Err(PacketError::Extension(
334                extension::ExtensionError::TruncatedPacketPayload {
335                    expected: 7,
336                    got: 6,
337                }
338            ))
339        ));
340
341        let tx = tx_with_op_return_payload(Vec::from_hex("41524b010100ff").unwrap());
342        assert!(matches!(
343            find_packet(&tx),
344            Err(PacketError::Extension(
345                extension::ExtensionError::TruncatedPacketLength
346            ))
347        ));
348
349        let tx = tx_with_op_return_payload(Vec::from_hex("41524b01ffffffffffffffffff").unwrap());
350        assert!(matches!(
351            find_packet(&tx),
352            Err(PacketError::Extension(
353                extension::ExtensionError::TruncatedPacketLength
354            ))
355        ));
356    }
357
358    #[test]
359    fn add_and_find_packet() {
360        let packet = Packet::new(vec![IntrospectorEntry {
361            vin: 0,
362            script: ScriptBuf::from_bytes(Vec::from_hex("51").unwrap()),
363            witness: Witness::default(),
364        }])
365        .unwrap();
366
367        let mut psbt = bitcoin::Psbt::from_unsigned_tx(Transaction {
368            version: transaction::Version::TWO,
369            lock_time: absolute::LockTime::ZERO,
370            input: vec![TxIn::default()],
371            output: vec![
372                TxOut {
373                    value: Amount::from_sat(1_000),
374                    script_pubkey: ScriptBuf::new(),
375                },
376                TxOut {
377                    value: Amount::ZERO,
378                    script_pubkey: ScriptBuf::new(),
379                },
380            ],
381        })
382        .unwrap();
383
384        add_packet_to_psbt(&mut psbt, &packet).unwrap();
385        assert_eq!(psbt.unsigned_tx.output.len(), 3);
386
387        let found = find_packet(&psbt.unsigned_tx).unwrap().unwrap();
388        assert_eq!(found, packet);
389    }
390
391    #[test]
392    fn duplicate_vins_are_rejected() {
393        let err = Packet::new(vec![
394            IntrospectorEntry {
395                vin: 1,
396                script: ScriptBuf::from_bytes(vec![0x51]),
397                witness: Witness::default(),
398            },
399            IntrospectorEntry {
400                vin: 1,
401                script: ScriptBuf::from_bytes(vec![0x52]),
402                witness: Witness::default(),
403            },
404        ])
405        .unwrap_err();
406
407        assert!(matches!(
408            err,
409            PacketError::DuplicateVin { vin: 1, entry: 1 }
410        ));
411    }
412}