Skip to main content

bitcoin_embed/
lib.rs

1//! # Bitcoin Embed
2//!
3//! A library for embedding arbitrary data and TLV-encoded messages in Bitcoin transactions. Supports
4//! OP_RETURN outputs, witness script envelopes, and taproot annexes.
5//!
6//! See README.md for detailed documentation.
7
8// Coding conventions
9#![deny(unsafe_code)]
10#![deny(non_upper_case_globals)]
11#![deny(non_camel_case_types)]
12#![deny(non_snake_case)]
13#![deny(unused_mut)]
14#![deny(dead_code)]
15#![deny(unused_imports)]
16#![deny(missing_docs)]
17
18#[cfg(not(any(feature = "std")))]
19compile_error!("`std` must be enabled");
20
21use bitcoin::{Transaction, Txid, taproot::LeafVersion};
22use std::fmt;
23use std::str::FromStr;
24
25pub mod envelope;
26pub mod message;
27pub mod varint;
28
29/// The initial byte in a data-carrying taproot annex
30pub const TAPROOT_ANNEX_DATA_TAG: u8 = 0;
31
32/// The script type used by an envelope
33#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
34pub enum ScriptType {
35    /// Legacy script (P2WSH)
36    Legacy,
37    /// Tapscript
38    Tapscript,
39}
40
41/// The type of location where data may exist in a transaction
42#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
43pub enum EmbeddingType {
44    /// An `OP_RETURN`
45    OpReturn,
46    /// A taproot annex
47    TaprootAnnex,
48    /// An `OP_FALSE OP_IF <DATA> OP_ENDIF` envelope
49    WitnessEnvelope(ScriptType),
50}
51
52/// The location where data exists in a transaction
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum EmbeddingLocation {
55    /// An `OP_RETURN` with the output index
56    OpReturn {
57        /// The index of the transaction output
58        output: usize,
59    },
60
61    /// A taproot annex with the input index
62    TaprootAnnex {
63        /// The index of the transaction input
64        input: usize,
65    },
66
67    /// An `OP_FALSE OP_IF <DATA> OP_ENDIF` envelope with the input index, envelope index, and data push sizes
68    ///
69    /// Witness envelopes are found in the following contexts:
70    /// 1. TapScript leaf scripts in Taproot script path spends (P2TR)
71    /// 2. Witness scripts in P2WSH spends with at least 2 witness stack elements
72    WitnessEnvelope {
73        /// The index of the transaction input
74        input: usize,
75        /// The index of the envelope within the script
76        index: usize,
77        /// The sizes of individual data pushes within the envelope
78        pushes: Vec<usize>,
79        /// The script type
80        script_type: ScriptType,
81    },
82}
83
84impl EmbeddingLocation {
85    /// Returns the embedding type
86    pub fn to_type(&self) -> EmbeddingType {
87        match self {
88            EmbeddingLocation::OpReturn { .. } => EmbeddingType::OpReturn,
89            EmbeddingLocation::TaprootAnnex { .. } => EmbeddingType::TaprootAnnex,
90            EmbeddingLocation::WitnessEnvelope { script_type, .. } => {
91                EmbeddingType::WitnessEnvelope(*script_type)
92            }
93        }
94    }
95}
96
97/// A unique identifier for an embedding
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99pub struct EmbeddingId {
100    /// The transaction ID
101    pub txid: Txid,
102    /// The embedding type
103    pub embedding_type: EmbeddingType,
104    /// The input or output index
105    pub index: usize,
106    /// The sub-index (only for envelope embeddings)
107    pub sub_index: Option<usize>,
108    /// Private field to prevent direct construction
109    _private: bool,
110}
111
112/// Error types for decoding an EmbeddingId
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub enum EmbeddingIdError {
115    /// Invalid format for embedding ID
116    InvalidFormat,
117    /// Invalid transaction ID
118    InvalidTxid,
119    /// Invalid embedding type
120    InvalidType,
121    /// Invalid index value
122    InvalidIndex,
123}
124
125/// A struct containing data and its location in a transaction
126#[derive(Debug, Clone, PartialEq)]
127pub struct Embedding {
128    /// The data
129    pub bytes: Vec<u8>,
130    /// The transaction ID
131    pub txid: Txid,
132    /// The location in the transaction
133    pub location: EmbeddingLocation,
134}
135
136impl Embedding {
137    /// Returns the embedding id
138    pub fn id(&self) -> EmbeddingId {
139        let embedding_type = self.location.to_type();
140
141        let (index, sub_index) = match self.location {
142            EmbeddingLocation::OpReturn { output } => (output, None),
143            EmbeddingLocation::TaprootAnnex { input } => (input, None),
144            EmbeddingLocation::WitnessEnvelope { input, index, .. } => (input, Some(index)),
145        };
146
147        EmbeddingId {
148            txid: self.txid,
149            embedding_type,
150            index,
151            sub_index,
152            _private: false,
153        }
154    }
155
156    /// Returns the embedding type
157    pub fn to_type(&self) -> EmbeddingType {
158        self.location.to_type()
159    }
160
161    /// Extracts the tape in a transaction
162    pub fn from_transaction(tx: &Transaction) -> Vec<Self> {
163        let mut embeddings = Vec::new();
164        let txid = tx.compute_txid();
165
166        // OP_RETURN
167        for (output, txout) in tx.output.iter().enumerate() {
168            if !txout.script_pubkey.is_op_return() {
169                continue;
170            }
171
172            let location = EmbeddingLocation::OpReturn { output };
173
174            embeddings.push(Self {
175                bytes: txout.script_pubkey.to_bytes()[1..].to_vec(),
176                txid,
177                location,
178            });
179        }
180
181        // Witness Envelope
182        for (input, txin) in tx.input.iter().enumerate() {
183            let mut script = None;
184            let mut script_type = None;
185            let witness = &txin.witness;
186
187            // Tapscript
188            if let Some(leaf_script) = witness.taproot_leaf_script() {
189                if leaf_script.version == LeafVersion::TapScript {
190                    script = Some(leaf_script.script);
191                    script_type = Some(ScriptType::Tapscript);
192                }
193            }
194
195            // P2WSH (no tapscript, no annex, and at least 2 elements)
196            if script.is_none() && witness.taproot_annex().is_none() && witness.len() > 1 {
197                if let Some(witness_script) = witness.witness_script() {
198                    script = Some(witness_script);
199                    script_type = Some(ScriptType::Legacy);
200                }
201            }
202
203            let (Some(script), Some(script_type)) = (script, script_type) else {
204                continue;
205            };
206
207            let envelopes = envelope::from_script(script);
208
209            for (index, envelope) in envelopes.into_iter().enumerate() {
210                let mut bytes = Vec::new();
211                let mut pushes = Vec::new();
212
213                for chunk in envelope {
214                    bytes.extend(chunk.clone());
215                    pushes.push(chunk.len());
216                }
217
218                let location = EmbeddingLocation::WitnessEnvelope {
219                    input,
220                    index,
221                    pushes,
222                    script_type,
223                };
224
225                embeddings.push(Self {
226                    bytes,
227                    txid,
228                    location,
229                });
230            }
231        }
232
233        // Annex
234        for (input, txin) in tx.input.iter().enumerate() {
235            if let Some(annex) = txin.witness.taproot_annex() {
236                if annex.len() > 2 && annex[1] == TAPROOT_ANNEX_DATA_TAG {
237                    let location = EmbeddingLocation::TaprootAnnex { input };
238
239                    embeddings.push(Self {
240                        bytes: annex[2..].to_vec(),
241                        txid,
242                        location,
243                    });
244                }
245            }
246        }
247
248        embeddings
249    }
250}
251
252impl fmt::Display for ScriptType {
253    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
254        match self {
255            ScriptType::Legacy => write!(f, "Legacy P2WSH"),
256            ScriptType::Tapscript => write!(f, "Tapscript"),
257        }
258    }
259}
260
261impl fmt::Display for EmbeddingType {
262    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
263        match self {
264            EmbeddingType::OpReturn => write!(f, "OP_RETURN"),
265            EmbeddingType::TaprootAnnex => write!(f, "Taproot Annex"),
266            EmbeddingType::WitnessEnvelope(script_type) => write!(f, "{} Envelope", script_type),
267        }
268    }
269}
270
271impl fmt::Display for EmbeddingLocation {
272    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
273        match self {
274            EmbeddingLocation::OpReturn { output } => {
275                write!(f, "OP_RETURN at output {}", output)
276            }
277            EmbeddingLocation::TaprootAnnex { input } => {
278                write!(f, "Taproot Annex at input {}", input)
279            }
280            EmbeddingLocation::WitnessEnvelope {
281                input,
282                index,
283                script_type,
284                ..
285            } => {
286                write!(
287                    f,
288                    "{} Envelope at input {} (index {})",
289                    script_type, input, index
290                )
291            }
292        }
293    }
294}
295
296impl fmt::Display for EmbeddingId {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        match self.embedding_type {
299            EmbeddingType::OpReturn => {
300                write!(f, "{}:rt:{}", self.txid, self.index)
301            }
302            EmbeddingType::TaprootAnnex => {
303                write!(f, "{}:ta:{}", self.txid, self.index)
304            }
305            EmbeddingType::WitnessEnvelope(script_type) => {
306                let type_code = match script_type {
307                    ScriptType::Legacy => "le",
308                    ScriptType::Tapscript => "te",
309                };
310
311                if let Some(sub_index) = self.sub_index {
312                    if sub_index > 0 {
313                        return write!(
314                            f,
315                            "{}:{}:{}:{}",
316                            self.txid, type_code, self.index, sub_index
317                        );
318                    }
319                }
320
321                write!(f, "{}:{}:{}", self.txid, type_code, self.index)
322            }
323        }
324    }
325}
326
327impl FromStr for EmbeddingId {
328    type Err = EmbeddingIdError;
329
330    fn from_str(s: &str) -> Result<Self, Self::Err> {
331        let parts: Vec<&str> = s.split(':').collect();
332
333        if parts.len() < 3 || parts.len() > 4 {
334            return Err(EmbeddingIdError::InvalidFormat);
335        }
336
337        let txid = Txid::from_str(parts[0]).map_err(|_| EmbeddingIdError::InvalidTxid)?;
338
339        let embedding_type = match parts[1] {
340            "rt" => EmbeddingType::OpReturn,
341            "ta" => EmbeddingType::TaprootAnnex,
342            "le" => EmbeddingType::WitnessEnvelope(ScriptType::Legacy),
343            "te" => EmbeddingType::WitnessEnvelope(ScriptType::Tapscript),
344            _ => return Err(EmbeddingIdError::InvalidType),
345        };
346
347        let index = parts[2]
348            .parse::<usize>()
349            .map_err(|_| EmbeddingIdError::InvalidIndex)?;
350
351        let mut sub_index = if parts.len() == 4 {
352            Some(
353                parts[3]
354                    .parse::<usize>()
355                    .map_err(|_| EmbeddingIdError::InvalidIndex)?,
356            )
357        } else {
358            None
359        };
360
361        // sub_index should only be present in envelopes
362        match embedding_type {
363            EmbeddingType::WitnessEnvelope(_) => {
364                if sub_index.is_none() {
365                    sub_index = Some(0);
366                }
367            }
368            _ if sub_index.is_some() => return Err(EmbeddingIdError::InvalidFormat),
369            _ => {}
370        }
371
372        Ok(Self {
373            txid,
374            embedding_type,
375            index,
376            sub_index,
377            _private: false,
378        })
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385    use bitcoin::{
386        Amount, OutPoint, ScriptBuf, Sequence, TxIn, TxOut, Witness,
387        absolute::LockTime,
388        hashes::Hash,
389        script::Builder,
390        taproot::{TAPROOT_ANNEX_PREFIX, TAPROOT_CONTROL_BASE_SIZE, TAPROOT_LEAF_TAPSCRIPT},
391        transaction::Version,
392    };
393
394    #[test]
395    fn test_embedding_to_type() {
396        let op_return_loc = EmbeddingLocation::OpReturn { output: 0 };
397        let annex_loc = EmbeddingLocation::TaprootAnnex { input: 1 };
398        let legacy_loc = EmbeddingLocation::WitnessEnvelope {
399            input: 2,
400            index: 0,
401            pushes: vec![4, 8],
402            script_type: ScriptType::Legacy,
403        };
404        let tapscript_loc = EmbeddingLocation::WitnessEnvelope {
405            input: 3,
406            index: 0,
407            pushes: vec![5, 10],
408            script_type: ScriptType::Tapscript,
409        };
410
411        assert_eq!(op_return_loc.to_type(), EmbeddingType::OpReturn);
412        assert_eq!(annex_loc.to_type(), EmbeddingType::TaprootAnnex);
413        assert_eq!(
414            legacy_loc.to_type(),
415            EmbeddingType::WitnessEnvelope(ScriptType::Legacy)
416        );
417        assert_eq!(
418            tapscript_loc.to_type(),
419            EmbeddingType::WitnessEnvelope(ScriptType::Tapscript)
420        );
421
422        for loc in [op_return_loc, annex_loc, legacy_loc, tapscript_loc] {
423            let embedding = Embedding {
424                bytes: vec![1, 2, 3],
425                txid: Txid::all_zeros(),
426                location: loc.clone(),
427            };
428
429            assert_eq!(embedding.to_type(), loc.to_type());
430        }
431    }
432
433    #[test]
434    fn test_from_transaction_op_return() {
435        // Create transaction with OP_RETURN output
436        let mut tx = Transaction {
437            version: Version::ONE,
438            lock_time: LockTime::ZERO,
439            input: vec![],
440            output: vec![TxOut {
441                value: Amount::ZERO,
442                script_pubkey: ScriptBuf::from_hex("6a48656c6c6f").unwrap(), // OP_RETURN "Hello"
443            }],
444        };
445
446        let embeddings = Embedding::from_transaction(&tx);
447
448        assert_eq!(embeddings.len(), 1);
449        assert_eq!(embeddings[0].bytes, b"Hello");
450        assert_eq!(embeddings[0].txid, tx.compute_txid());
451        assert_eq!(
452            embeddings[0].location,
453            EmbeddingLocation::OpReturn { output: 0 }
454        );
455
456        // Test with multiple outputs including non-OP_RETURN
457        tx.output.push(TxOut {
458            value: Amount::from_sat(1000),
459            script_pubkey: ScriptBuf::new(),
460        });
461        tx.output.push(TxOut {
462            value: Amount::ZERO,
463            script_pubkey: ScriptBuf::from_hex("6a576f726c64").unwrap(), // OP_RETURN "World"
464        });
465
466        let embeddings = Embedding::from_transaction(&tx);
467
468        assert_eq!(embeddings.len(), 2);
469        assert_eq!(embeddings[0].bytes, b"Hello");
470        assert_eq!(embeddings[0].txid, tx.compute_txid());
471        assert_eq!(
472            embeddings[0].location,
473            EmbeddingLocation::OpReturn { output: 0 }
474        );
475        assert_eq!(embeddings[1].bytes, b"World");
476        assert_eq!(embeddings[1].txid, tx.compute_txid());
477        assert_eq!(
478            embeddings[1].location,
479            EmbeddingLocation::OpReturn { output: 2 }
480        );
481    }
482
483    #[test]
484    fn test_from_transaction_p2wsh_envelope() {
485        // Create a transaction with p2wsh envelopes
486        let mut builder = envelope::append_bytes_to_builder(b"data", Builder::new());
487        builder = envelope::append_bytes_to_builder(b"data-two", builder);
488        let witness0 = Witness::from_slice(&[vec![1], builder.into_bytes()]);
489
490        let builder = envelope::append_to_builder(
491            vec![b"data-three".to_vec(), b"<extension>".to_vec()],
492            Builder::new(),
493        );
494        let witness1 = Witness::from_slice(&[vec![1], builder.into_bytes()]);
495
496        let tx = Transaction {
497            version: Version::ONE,
498            lock_time: LockTime::ZERO,
499            input: vec![
500                TxIn {
501                    previous_output: OutPoint::null(),
502                    script_sig: ScriptBuf::new(),
503                    sequence: Sequence::ZERO,
504                    witness: witness0,
505                },
506                TxIn {
507                    previous_output: OutPoint::null(),
508                    script_sig: ScriptBuf::new(),
509                    sequence: Sequence::ZERO,
510                    witness: witness1,
511                },
512            ],
513            output: vec![],
514        };
515
516        let embeddings = Embedding::from_transaction(&tx);
517
518        assert_eq!(embeddings.len(), 3);
519
520        assert_eq!(embeddings[0].bytes, b"data");
521        assert_eq!(embeddings[0].txid, tx.compute_txid());
522        assert_eq!(
523            embeddings[0].location,
524            EmbeddingLocation::WitnessEnvelope {
525                input: 0,
526                index: 0,
527                pushes: vec![4],
528                script_type: ScriptType::Legacy,
529            }
530        );
531
532        assert_eq!(embeddings[1].bytes, b"data-two");
533        assert_eq!(embeddings[1].txid, tx.compute_txid());
534        assert_eq!(
535            embeddings[1].location,
536            EmbeddingLocation::WitnessEnvelope {
537                input: 0,
538                index: 1,
539                pushes: vec![8],
540                script_type: ScriptType::Legacy,
541            }
542        );
543
544        assert_eq!(embeddings[2].bytes, b"data-three<extension>");
545        assert_eq!(embeddings[2].txid, tx.compute_txid());
546        assert_eq!(
547            embeddings[2].location,
548            EmbeddingLocation::WitnessEnvelope {
549                input: 1,
550                index: 0,
551                pushes: vec![10, 11],
552                script_type: ScriptType::Legacy,
553            }
554        );
555    }
556
557    #[test]
558    fn test_from_transaction_tapscript_envelope() {
559        // Create transaction with a tapscript envelope
560        let builder = envelope::append_bytes_to_builder(b"data", Builder::new());
561        let witness = Witness::from_slice(&[
562            builder.into_bytes(),
563            vec![TAPROOT_LEAF_TAPSCRIPT; TAPROOT_CONTROL_BASE_SIZE],
564        ]);
565
566        let tx = Transaction {
567            version: Version::ONE,
568            lock_time: LockTime::ZERO,
569            input: vec![TxIn {
570                previous_output: OutPoint::null(),
571                script_sig: ScriptBuf::new(),
572                sequence: Sequence::ZERO,
573                witness,
574            }],
575            output: vec![],
576        };
577
578        let embeddings = Embedding::from_transaction(&tx);
579
580        assert_eq!(embeddings.len(), 1);
581
582        assert_eq!(embeddings[0].bytes, b"data");
583        assert_eq!(embeddings[0].txid, tx.compute_txid());
584        assert_eq!(
585            embeddings[0].location,
586            EmbeddingLocation::WitnessEnvelope {
587                input: 0,
588                index: 0,
589                pushes: vec![4],
590                script_type: ScriptType::Tapscript,
591            }
592        );
593    }
594
595    #[test]
596    fn test_from_transaction_taproot_annex() {
597        // Create transaction with an annex
598        let witness0 = Witness::from_slice(&[
599            vec![1],
600            [
601                vec![TAPROOT_ANNEX_PREFIX, TAPROOT_ANNEX_DATA_TAG],
602                b"Hello".to_vec(),
603            ]
604            .concat(),
605        ]);
606
607        let witness1 = Witness::from_slice(&[
608            vec![1],
609            [
610                vec![TAPROOT_ANNEX_PREFIX, TAPROOT_ANNEX_DATA_TAG],
611                b"World".to_vec(),
612            ]
613            .concat(),
614        ]);
615
616        let tx = Transaction {
617            version: Version::ONE,
618            lock_time: LockTime::ZERO,
619            input: vec![
620                TxIn {
621                    previous_output: OutPoint::null(),
622                    script_sig: ScriptBuf::new(),
623                    sequence: Sequence::ZERO,
624                    witness: witness0,
625                },
626                TxIn {
627                    previous_output: OutPoint::null(),
628                    script_sig: ScriptBuf::new(),
629                    sequence: Sequence::ZERO,
630                    witness: Witness::new(),
631                },
632                TxIn {
633                    previous_output: OutPoint::null(),
634                    script_sig: ScriptBuf::new(),
635                    sequence: Sequence::ZERO,
636                    witness: witness1,
637                },
638            ],
639            output: vec![],
640        };
641
642        let embeddings = Embedding::from_transaction(&tx);
643
644        assert_eq!(embeddings.len(), 2);
645        assert_eq!(embeddings[0].bytes, b"Hello");
646        assert_eq!(embeddings[0].txid, tx.compute_txid());
647        assert_eq!(
648            embeddings[0].location,
649            EmbeddingLocation::TaprootAnnex { input: 0 }
650        );
651        assert_eq!(embeddings[1].bytes, b"World");
652        assert_eq!(embeddings[1].txid, tx.compute_txid());
653        assert_eq!(
654            embeddings[1].location,
655            EmbeddingLocation::TaprootAnnex { input: 2 }
656        );
657    }
658
659    #[test]
660    fn test_from_transaction_complex() {
661        // 1. Create OP_RETURN outputs
662        let op_return_output0 = TxOut {
663            value: Amount::ZERO,
664            script_pubkey: ScriptBuf::from_hex("6a48656c6c6f").unwrap(), // OP_RETURN "Hello"
665        };
666
667        let op_return_output1 = TxOut {
668            value: Amount::ZERO,
669            script_pubkey: ScriptBuf::from_hex("6a576f726c64").unwrap(), // OP_RETURN "World"
670        };
671
672        // 2. Create P2WSH input with envelope data
673        let p2wsh_builder = envelope::append_bytes_to_builder(b"p2wsh-data", Builder::new());
674        let p2wsh_witness = Witness::from_slice(&[vec![1], p2wsh_builder.into_bytes()]);
675
676        // 3. Create Tapscript input with envelope data
677        let mut tapscript_builder =
678            envelope::append_bytes_to_builder(b"tapscript-data1", Builder::new());
679        tapscript_builder = envelope::append_to_builder(
680            vec![b"multi".to_vec(), b"part".to_vec(), b"data".to_vec()],
681            tapscript_builder,
682        );
683        let tapscript_witness = Witness::from_slice(&[
684            tapscript_builder.into_bytes(),
685            vec![TAPROOT_LEAF_TAPSCRIPT; TAPROOT_CONTROL_BASE_SIZE],
686        ]);
687
688        // 4. Create Taproot Annex input
689        let annex_witness = Witness::from_slice(&[
690            vec![1],
691            [
692                vec![TAPROOT_ANNEX_PREFIX, TAPROOT_ANNEX_DATA_TAG],
693                b"annex-data".to_vec(),
694            ]
695            .concat(),
696        ]);
697
698        // Create the transaction
699        let tx = Transaction {
700            version: Version::ONE,
701            lock_time: LockTime::ZERO,
702            input: vec![
703                // Empty witness
704                TxIn {
705                    previous_output: OutPoint::null(),
706                    script_sig: ScriptBuf::new(),
707                    sequence: Sequence::ZERO,
708                    witness: Witness::new(),
709                },
710                // P2WSH input with envelope
711                TxIn {
712                    previous_output: OutPoint::null(),
713                    script_sig: ScriptBuf::new(),
714                    sequence: Sequence::ZERO,
715                    witness: p2wsh_witness,
716                },
717                // Tapscript input with envelopes
718                TxIn {
719                    previous_output: OutPoint::null(),
720                    script_sig: ScriptBuf::new(),
721                    sequence: Sequence::ZERO,
722                    witness: tapscript_witness,
723                },
724                // Taproot annex input
725                TxIn {
726                    previous_output: OutPoint::null(),
727                    script_sig: ScriptBuf::new(),
728                    sequence: Sequence::ZERO,
729                    witness: annex_witness,
730                },
731            ],
732            output: vec![
733                op_return_output0,
734                TxOut {
735                    value: Amount::from_sat(10000),
736                    script_pubkey: ScriptBuf::new(),
737                },
738                op_return_output1,
739            ],
740        };
741
742        let embeddings = Embedding::from_transaction(&tx);
743
744        assert_eq!(embeddings.len(), 6);
745
746        assert_eq!(embeddings[0].bytes, b"Hello");
747        assert_eq!(embeddings[0].txid, tx.compute_txid());
748        assert_eq!(
749            embeddings[0].location,
750            EmbeddingLocation::OpReturn { output: 0 }
751        );
752
753        assert_eq!(embeddings[1].bytes, b"World");
754        assert_eq!(embeddings[1].txid, tx.compute_txid());
755        assert_eq!(
756            embeddings[1].location,
757            EmbeddingLocation::OpReturn { output: 2 }
758        );
759
760        assert_eq!(embeddings[2].bytes, b"p2wsh-data");
761        assert_eq!(embeddings[2].txid, tx.compute_txid());
762        assert_eq!(
763            embeddings[2].location,
764            EmbeddingLocation::WitnessEnvelope {
765                input: 1,
766                index: 0,
767                pushes: vec![10],
768                script_type: ScriptType::Legacy,
769            }
770        );
771
772        assert_eq!(embeddings[3].bytes, b"tapscript-data1");
773        assert_eq!(embeddings[3].txid, tx.compute_txid());
774        assert_eq!(
775            embeddings[3].location,
776            EmbeddingLocation::WitnessEnvelope {
777                input: 2,
778                index: 0,
779                pushes: vec![15],
780                script_type: ScriptType::Tapscript,
781            }
782        );
783
784        assert_eq!(embeddings[4].bytes, b"multipartdata");
785        assert_eq!(embeddings[4].txid, tx.compute_txid());
786        assert_eq!(
787            embeddings[4].location,
788            EmbeddingLocation::WitnessEnvelope {
789                input: 2,
790                index: 1,
791                pushes: vec![5, 4, 4],
792                script_type: ScriptType::Tapscript,
793            }
794        );
795
796        assert_eq!(embeddings[5].bytes, b"annex-data");
797        assert_eq!(embeddings[5].txid, tx.compute_txid());
798        assert_eq!(
799            embeddings[5].location,
800            EmbeddingLocation::TaprootAnnex { input: 3 }
801        );
802
803        for embedding in &embeddings {
804            assert_eq!(embedding.txid, tx.compute_txid());
805        }
806    }
807
808    #[test]
809    fn test_embedding_id_from_str() {
810        let txid_str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
811
812        // OP_RETURN
813        let op_return_id = EmbeddingId::from_str(&format!("{}:rt:2", txid_str)).unwrap();
814        assert_eq!(op_return_id.embedding_type, EmbeddingType::OpReturn);
815        assert_eq!(op_return_id.index, 2);
816        assert_eq!(op_return_id.sub_index, None);
817
818        // TaprootAnnex
819        let annex_id = EmbeddingId::from_str(&format!("{}:ta:1", txid_str)).unwrap();
820        assert_eq!(annex_id.embedding_type, EmbeddingType::TaprootAnnex);
821        assert_eq!(annex_id.index, 1);
822        assert_eq!(annex_id.sub_index, None);
823
824        // Legacy envelope with explicit sub_index
825        let legacy_id = EmbeddingId::from_str(&format!("{}:le:0:3", txid_str)).unwrap();
826        assert_eq!(
827            legacy_id.embedding_type,
828            EmbeddingType::WitnessEnvelope(ScriptType::Legacy)
829        );
830        assert_eq!(legacy_id.index, 0);
831        assert_eq!(legacy_id.sub_index, Some(3));
832
833        // Legacy envelope without sub_index (defaults to 0)
834        let legacy_id2 = EmbeddingId::from_str(&format!("{}:le:0", txid_str)).unwrap();
835        assert_eq!(
836            legacy_id2.embedding_type,
837            EmbeddingType::WitnessEnvelope(ScriptType::Legacy)
838        );
839        assert_eq!(legacy_id2.index, 0);
840        assert_eq!(legacy_id2.sub_index, Some(0));
841
842        // Tapscript envelope with explicit sub_index
843        let tapscript_id = EmbeddingId::from_str(&format!("{}:te:2:1", txid_str)).unwrap();
844        assert_eq!(
845            tapscript_id.embedding_type,
846            EmbeddingType::WitnessEnvelope(ScriptType::Tapscript)
847        );
848        assert_eq!(tapscript_id.index, 2);
849        assert_eq!(tapscript_id.sub_index, Some(1));
850    }
851
852    #[test]
853    fn test_embedding_id_to_string() {
854        let txid = Txid::all_zeros();
855
856        // OP_RETURN id
857        let op_return_id = EmbeddingId {
858            txid,
859            embedding_type: EmbeddingType::OpReturn,
860            index: 2,
861            sub_index: None,
862            _private: false,
863        };
864
865        // TaprootAnnex id
866        let annex_id = EmbeddingId {
867            txid,
868            embedding_type: EmbeddingType::TaprootAnnex,
869            index: 1,
870            sub_index: None,
871            _private: false,
872        };
873
874        // Legacy envelope id
875        let legacy_id = EmbeddingId {
876            txid,
877            embedding_type: EmbeddingType::WitnessEnvelope(ScriptType::Legacy),
878            index: 0,
879            sub_index: Some(3),
880            _private: false,
881        };
882
883        // Tapscript envelope id with index 0 (should not show sub_index)
884        let tapscript_id = EmbeddingId {
885            txid,
886            embedding_type: EmbeddingType::WitnessEnvelope(ScriptType::Tapscript),
887            index: 2,
888            sub_index: Some(0),
889            _private: false,
890        };
891
892        let txid_str = txid.to_string();
893
894        assert_eq!(op_return_id.to_string(), format!("{}:rt:2", txid_str));
895        assert_eq!(annex_id.to_string(), format!("{}:ta:1", txid_str));
896        assert_eq!(legacy_id.to_string(), format!("{}:le:0:3", txid_str));
897        assert_eq!(tapscript_id.to_string(), format!("{}:te:2", txid_str));
898    }
899
900    #[test]
901    fn test_embedding_id_invalid_parsing() {
902        let txid_str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
903
904        // Too few parts
905        let err = EmbeddingId::from_str(txid_str).unwrap_err();
906        assert_eq!(err, EmbeddingIdError::InvalidFormat);
907
908        // Too many parts
909        let err = EmbeddingId::from_str(&format!("{}:rt:2:3:extra", txid_str)).unwrap_err();
910        assert_eq!(err, EmbeddingIdError::InvalidFormat);
911
912        // Invalid txid
913        let err = EmbeddingId::from_str("invalid:rt:2").unwrap_err();
914        assert_eq!(err, EmbeddingIdError::InvalidTxid);
915
916        // Invalid type
917        let err = EmbeddingId::from_str(&format!("{}:invalid:2", txid_str)).unwrap_err();
918        assert_eq!(err, EmbeddingIdError::InvalidType);
919
920        // Invalid index (not a number)
921        let err = EmbeddingId::from_str(&format!("{}:rt:abc", txid_str)).unwrap_err();
922        assert_eq!(err, EmbeddingIdError::InvalidIndex);
923
924        // Invalid sub_index (not a number)
925        let err = EmbeddingId::from_str(&format!("{}:te:2:abc", txid_str)).unwrap_err();
926        assert_eq!(err, EmbeddingIdError::InvalidIndex);
927
928        // Sub_index not allowed for OP_RETURN
929        let err = EmbeddingId::from_str(&format!("{}:rt:2:1", txid_str)).unwrap_err();
930        assert_eq!(err, EmbeddingIdError::InvalidFormat);
931
932        // Sub_index not allowed for TaprootAnnex
933        let err = EmbeddingId::from_str(&format!("{}:ta:1:2", txid_str)).unwrap_err();
934        assert_eq!(err, EmbeddingIdError::InvalidFormat);
935    }
936
937    #[test]
938    fn test_embedding_id_from_embedding() {
939        let txid = Txid::all_zeros();
940
941        // OP_RETURN embedding
942        let op_return_embedding = Embedding {
943            bytes: vec![1, 2, 3],
944            txid,
945            location: EmbeddingLocation::OpReturn { output: 2 },
946        };
947
948        let op_return_id = op_return_embedding.id();
949        assert_eq!(op_return_id.txid, txid);
950        assert_eq!(op_return_id.embedding_type, EmbeddingType::OpReturn);
951        assert_eq!(op_return_id.index, 2);
952        assert_eq!(op_return_id.sub_index, None);
953
954        // TaprootAnnex embedding
955        let annex_embedding = Embedding {
956            bytes: vec![4, 5, 6],
957            txid,
958            location: EmbeddingLocation::TaprootAnnex { input: 1 },
959        };
960
961        let annex_id = annex_embedding.id();
962        assert_eq!(annex_id.txid, txid);
963        assert_eq!(annex_id.embedding_type, EmbeddingType::TaprootAnnex);
964        assert_eq!(annex_id.index, 1);
965        assert_eq!(annex_id.sub_index, None);
966
967        // WitnessEnvelope (Legacy) with index 0 (no sub_index)
968        let legacy_embedding0 = Embedding {
969            bytes: vec![7, 8, 9],
970            txid,
971            location: EmbeddingLocation::WitnessEnvelope {
972                input: 3,
973                index: 0,
974                pushes: vec![3],
975                script_type: ScriptType::Legacy,
976            },
977        };
978
979        let legacy_id0 = legacy_embedding0.id();
980        assert_eq!(legacy_id0.txid, txid);
981        assert_eq!(
982            legacy_id0.embedding_type,
983            EmbeddingType::WitnessEnvelope(ScriptType::Legacy)
984        );
985        assert_eq!(legacy_id0.index, 3);
986        assert_eq!(legacy_id0.sub_index, Some(0));
987
988        // WitnessEnvelope (Legacy) with index > 0 (has sub_index)
989        let legacy_embedding1 = Embedding {
990            bytes: vec![7, 8, 9],
991            txid,
992            location: EmbeddingLocation::WitnessEnvelope {
993                input: 3,
994                index: 2,
995                pushes: vec![3],
996                script_type: ScriptType::Legacy,
997            },
998        };
999
1000        let legacy_id1 = legacy_embedding1.id();
1001        assert_eq!(legacy_id1.txid, txid);
1002        assert_eq!(
1003            legacy_id1.embedding_type,
1004            EmbeddingType::WitnessEnvelope(ScriptType::Legacy)
1005        );
1006        assert_eq!(legacy_id1.index, 3);
1007        assert_eq!(legacy_id1.sub_index, Some(2));
1008
1009        // WitnessEnvelope (Tapscript)
1010        let tapscript_embedding = Embedding {
1011            bytes: vec![10, 11, 12],
1012            txid,
1013            location: EmbeddingLocation::WitnessEnvelope {
1014                input: 4,
1015                index: 1,
1016                pushes: vec![3],
1017                script_type: ScriptType::Tapscript,
1018            },
1019        };
1020
1021        let tapscript_id = tapscript_embedding.id();
1022        assert_eq!(tapscript_id.txid, txid);
1023        assert_eq!(
1024            tapscript_id.embedding_type,
1025            EmbeddingType::WitnessEnvelope(ScriptType::Tapscript)
1026        );
1027        assert_eq!(tapscript_id.index, 4);
1028        assert_eq!(tapscript_id.sub_index, Some(1));
1029    }
1030
1031    #[test]
1032    fn test_embedding_id_roundtrip() {
1033        let txid = Txid::all_zeros();
1034
1035        // OP_RETURN id
1036        let op_return_id = EmbeddingId {
1037            txid,
1038            embedding_type: EmbeddingType::OpReturn,
1039            index: 2,
1040            sub_index: None,
1041            _private: false,
1042        };
1043
1044        let op_return_str = op_return_id.to_string();
1045        let op_return_id2 = EmbeddingId::from_str(&op_return_str).unwrap();
1046        assert_eq!(op_return_id.txid, op_return_id2.txid);
1047        assert_eq!(op_return_id.embedding_type, op_return_id2.embedding_type);
1048        assert_eq!(op_return_id.index, op_return_id2.index);
1049        assert_eq!(op_return_id.sub_index, op_return_id2.sub_index);
1050
1051        // TaprootAnnex id
1052        let annex_id = EmbeddingId {
1053            txid,
1054            embedding_type: EmbeddingType::TaprootAnnex,
1055            index: 1,
1056            sub_index: None,
1057            _private: false,
1058        };
1059
1060        let annex_str = annex_id.to_string();
1061        let annex_id2 = EmbeddingId::from_str(&annex_str).unwrap();
1062        assert_eq!(annex_id.txid, annex_id2.txid);
1063        assert_eq!(annex_id.embedding_type, annex_id2.embedding_type);
1064        assert_eq!(annex_id.index, annex_id2.index);
1065        assert_eq!(annex_id.sub_index, annex_id2.sub_index);
1066
1067        // Legacy envelope id
1068        let legacy_id = EmbeddingId {
1069            txid,
1070            embedding_type: EmbeddingType::WitnessEnvelope(ScriptType::Legacy),
1071            index: 0,
1072            sub_index: Some(3),
1073            _private: false,
1074        };
1075
1076        let legacy_str = legacy_id.to_string();
1077        let legacy_id2 = EmbeddingId::from_str(&legacy_str).unwrap();
1078        assert_eq!(legacy_id.txid, legacy_id2.txid);
1079        assert_eq!(legacy_id.embedding_type, legacy_id2.embedding_type);
1080        assert_eq!(legacy_id.index, legacy_id2.index);
1081        assert_eq!(legacy_id.sub_index, legacy_id2.sub_index);
1082
1083        // Tapscript envelope id
1084        let tapscript_id = EmbeddingId {
1085            txid,
1086            embedding_type: EmbeddingType::WitnessEnvelope(ScriptType::Tapscript),
1087            index: 2,
1088            sub_index: Some(0),
1089            _private: false,
1090        };
1091
1092        let tapscript_str = tapscript_id.to_string();
1093        let tapscript_id2 = EmbeddingId::from_str(&tapscript_str).unwrap();
1094        assert_eq!(tapscript_id.txid, tapscript_id2.txid);
1095        assert_eq!(tapscript_id.embedding_type, tapscript_id2.embedding_type);
1096        assert_eq!(tapscript_id.index, tapscript_id2.index);
1097        assert_eq!(Some(0), tapscript_id2.sub_index);
1098    }
1099}