1use bitcoin::opcodes::all::OP_RETURN;
2use bitcoin::script::Instruction;
3use bitcoin::Amount;
4use bitcoin::Script;
5use bitcoin::ScriptBuf;
6use bitcoin::TxOut;
7
8pub const MAGIC_BYTES: [u8; 3] = [0x41, 0x52, 0x4b];
9
10#[derive(Debug, thiserror::Error)]
11pub enum ExtensionError {
12 #[error("extension payload length overflows")]
13 PayloadLengthOverflow,
14 #[error("truncated extension packet type")]
15 TruncatedPacketType,
16 #[error("truncated extension packet length")]
17 TruncatedPacketLength,
18 #[error("truncated extension packet payload, expected {expected} bytes got {got}")]
19 TruncatedPacketPayload { expected: usize, got: usize },
20 #[error("duplicate extension packet type {0}")]
21 DuplicatePacketType(u8),
22}
23
24pub fn encode_uvarint(buf: &mut Vec<u8>, mut value: u64) {
25 loop {
26 let mut byte = (value & 0x7f) as u8;
27 value >>= 7;
28 if value != 0 {
29 byte |= 0x80;
30 }
31 buf.push(byte);
32 if value == 0 {
33 break;
34 }
35 }
36}
37
38fn decode_uvarint(data: &[u8], offset: &mut usize) -> Result<u64, ExtensionError> {
39 let mut value = 0_u64;
40 for shift in (0..64).step_by(7) {
41 let Some(byte) = data.get(*offset) else {
42 return Err(ExtensionError::TruncatedPacketLength);
43 };
44 *offset += 1;
45 value |= u64::from(byte & 0x7f) << shift;
46 if byte & 0x80 == 0 {
47 return Ok(value);
48 }
49 }
50 Err(ExtensionError::PayloadLengthOverflow)
51}
52
53pub fn is_extension(script: &Script) -> bool {
54 extension_payload(script).is_some()
55}
56
57pub fn extension_payload(script: &Script) -> Option<&[u8]> {
58 let mut instructions = script.instructions();
59 if !matches!(instructions.next(), Some(Ok(Instruction::Op(OP_RETURN)))) {
60 return None;
61 }
62 let Some(Ok(Instruction::PushBytes(bytes))) = instructions.next() else {
63 return None;
64 };
65 let bytes = bytes.as_bytes();
66 (bytes.len() >= MAGIC_BYTES.len() && bytes[..MAGIC_BYTES.len()] == MAGIC_BYTES).then_some(bytes)
67}
68
69pub fn iter_packets(payload: &[u8]) -> Result<Vec<(u8, &[u8])>, ExtensionError> {
70 let mut packets = Vec::new();
71 let mut offset = MAGIC_BYTES.len();
72
73 while offset < payload.len() {
74 let Some(packet_type) = payload.get(offset).copied() else {
75 return Err(ExtensionError::TruncatedPacketType);
76 };
77 offset += 1;
78
79 let packet_len = decode_uvarint(payload, &mut offset)? as usize;
80 let end = offset
81 .checked_add(packet_len)
82 .ok_or(ExtensionError::PayloadLengthOverflow)?;
83 if end > payload.len() {
84 return Err(ExtensionError::TruncatedPacketPayload {
85 expected: end,
86 got: payload.len(),
87 });
88 }
89
90 packets.push((packet_type, &payload[offset..end]));
91 offset = end;
92 }
93
94 Ok(packets)
95}
96
97pub fn find_packet_payload(
98 tx: &bitcoin::Transaction,
99 packet_type: u8,
100) -> Result<Option<&[u8]>, ExtensionError> {
101 for output in &tx.output {
102 let Some(payload) = extension_payload(&output.script_pubkey) else {
103 continue;
104 };
105 for (current_type, current_payload) in iter_packets(payload)? {
106 if current_type == packet_type {
107 return Ok(Some(current_payload));
108 }
109 }
110 return Ok(None);
111 }
112
113 Ok(None)
114}
115
116pub fn packet_txout(packet_type: u8, packet_payload: &[u8]) -> TxOut {
117 let mut payload = Vec::new();
118 payload.extend_from_slice(&MAGIC_BYTES);
119 push_packet(&mut payload, packet_type, packet_payload);
120
121 TxOut {
122 value: Amount::ZERO,
123 script_pubkey: op_return_script(&payload),
124 }
125}
126
127pub fn add_packet_to_psbt(
128 psbt: &mut bitcoin::Psbt,
129 packet_type: u8,
130 packet_payload: &[u8],
131) -> Result<(), ExtensionError> {
132 let mut encoded_packet = Vec::new();
133 push_packet(&mut encoded_packet, packet_type, packet_payload);
134
135 for output in &mut psbt.unsigned_tx.output {
136 let Some(existing_payload) = extension_payload(&output.script_pubkey) else {
137 continue;
138 };
139
140 for (existing_type, _) in iter_packets(existing_payload)? {
141 if existing_type == packet_type {
142 return Err(ExtensionError::DuplicatePacketType(packet_type));
143 }
144 }
145
146 let mut payload = existing_payload.to_vec();
147 payload.extend_from_slice(&encoded_packet);
148 output.script_pubkey = op_return_script(&payload);
149 return Ok(());
150 }
151
152 let txout = packet_txout(packet_type, packet_payload);
153 let len = psbt.unsigned_tx.output.len();
154
155 if len == 0 {
156 psbt.unsigned_tx.output.push(txout);
157 psbt.outputs.push(bitcoin::psbt::Output::default());
158 return Ok(());
159 }
160
161 let anchor_index = len - 1;
162 psbt.unsigned_tx.output.insert(anchor_index, txout);
163 psbt.outputs
164 .insert(anchor_index, bitcoin::psbt::Output::default());
165 Ok(())
166}
167
168fn push_packet(payload: &mut Vec<u8>, packet_type: u8, packet_payload: &[u8]) {
169 payload.push(packet_type);
170 encode_uvarint(payload, packet_payload.len() as u64);
171 payload.extend_from_slice(packet_payload);
172}
173
174fn op_return_script(data: &[u8]) -> ScriptBuf {
175 let mut script = Vec::new();
176 script.push(OP_RETURN.to_u8());
177 push_data(&mut script, data);
178 ScriptBuf::from_bytes(script)
179}
180
181fn push_data(script: &mut Vec<u8>, data: &[u8]) {
182 let len = data.len();
183 if len <= 75 {
184 script.push(len as u8);
185 } else if len <= 0xff {
186 script.push(0x4c);
187 script.push(len as u8);
188 } else if len <= 0xffff {
189 script.push(0x4d);
190 script.extend_from_slice(&(len as u16).to_le_bytes());
191 } else {
192 script.push(0x4e);
193 script.extend_from_slice(&(len as u32).to_le_bytes());
194 }
195 script.extend_from_slice(data);
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use bitcoin::absolute;
202 use bitcoin::transaction;
203 use bitcoin::TxIn;
204
205 #[test]
206 fn encodes_go_uvarint_packet_lengths() {
207 let txout = packet_txout(0x01, &[0; 136]);
208 let payload = extension_payload(&txout.script_pubkey).unwrap();
209 assert_eq!(&payload[..5], &[0x41, 0x52, 0x4b, 0x01, 0x88]);
210 assert_eq!(payload[5], 0x01);
211 }
212
213 #[test]
214 fn appends_to_existing_extension_output() {
215 let mut psbt = bitcoin::Psbt::from_unsigned_tx(bitcoin::Transaction {
216 version: transaction::Version::TWO,
217 lock_time: absolute::LockTime::ZERO,
218 input: vec![TxIn::default()],
219 output: vec![
220 TxOut {
221 value: Amount::ZERO,
222 script_pubkey: packet_txout(0x00, &[0xaa]).script_pubkey,
223 },
224 TxOut {
225 value: Amount::ZERO,
226 script_pubkey: ScriptBuf::new(),
227 },
228 ],
229 })
230 .unwrap();
231
232 add_packet_to_psbt(&mut psbt, 0x01, &[0xbb, 0xcc]).unwrap();
233
234 assert_eq!(psbt.unsigned_tx.output.len(), 2);
235 let payload = extension_payload(&psbt.unsigned_tx.output[0].script_pubkey).unwrap();
236 let packets = iter_packets(payload).unwrap();
237 assert_eq!(
238 packets,
239 vec![(0x00, &[0xaa][..]), (0x01, &[0xbb, 0xcc][..])]
240 );
241 }
242}