Skip to main content

bsv_rs/script/templates/
pushdrop.rs

1//! PushDrop Script Template
2//!
3//! A data envelope template that embeds arbitrary data in a Bitcoin transaction
4//! output, protected by a signature. Used by token protocols like BSV-20.
5//!
6//! # Script Structure
7//!
8//! ## Lock-Before Pattern (default)
9//! ```text
10//! <pubkey> OP_CHECKSIG <field1> <field2> ... OP_2DROP ... OP_DROP
11//! ```
12//!
13//! ## Lock-After Pattern
14//! ```text
15//! <field1> <field2> ... OP_2DROP ... OP_DROP <pubkey> OP_CHECKSIG
16//! ```
17//!
18//! # Example
19//!
20//! ```rust,ignore
21//! use bsv_rs::script::templates::PushDrop;
22//! use bsv_rs::primitives::ec::PrivateKey;
23//!
24//! let privkey = PrivateKey::random();
25//! let pubkey = privkey.public_key();
26//! let fields = vec![b"hello".to_vec(), b"world".to_vec()];
27//!
28//! let pushdrop = PushDrop::new(pubkey, fields);
29//! let script = pushdrop.lock();
30//! ```
31
32use crate::error::Error;
33use crate::primitives::bsv::TransactionSignature;
34use crate::primitives::ec::{PrivateKey, PublicKey};
35use crate::script::op::*;
36use crate::script::template::{
37    compute_sighash_scope, ScriptTemplateUnlock, SignOutputs, SigningContext,
38};
39use crate::script::{LockingScript, Script, ScriptChunk, UnlockingScript};
40use crate::Result;
41
42/// Lock position for PushDrop template.
43///
44/// Determines whether the public key and OP_CHECKSIG come before or after
45/// the data fields in the locking script.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
47pub enum LockPosition {
48    /// Pubkey and OP_CHECKSIG come before data fields (default).
49    /// Script: `<pubkey> OP_CHECKSIG <fields...> OP_2DROP... OP_DROP`
50    #[default]
51    Before,
52    /// Pubkey and OP_CHECKSIG come after data fields.
53    /// Script: `<fields...> OP_2DROP... OP_DROP <pubkey> OP_CHECKSIG`
54    After,
55}
56
57/// PushDrop locking script template.
58///
59/// This template creates scripts that embed arbitrary data fields alongside
60/// a P2PK (Pay-to-Public-Key) lock. The data fields are pushed onto the stack
61/// and then dropped, leaving only the signature verification.
62///
63/// # Example
64///
65/// ```rust,ignore
66/// use bsv_rs::script::templates::PushDrop;
67/// use bsv_rs::primitives::ec::PrivateKey;
68///
69/// let privkey = PrivateKey::random();
70/// let pubkey = privkey.public_key();
71///
72/// // Create with embedded token data
73/// let fields = vec![
74///     b"BSV20".to_vec(),
75///     b"transfer".to_vec(),
76///     b"1000".to_vec(),
77/// ];
78///
79/// let pushdrop = PushDrop::new(pubkey, fields);
80/// let locking_script = pushdrop.lock();
81/// ```
82#[derive(Debug, Clone)]
83pub struct PushDrop {
84    /// The public key that can unlock this output.
85    pub locking_public_key: PublicKey,
86    /// Embedded data fields.
87    pub fields: Vec<Vec<u8>>,
88    /// Lock position (before or after data).
89    pub lock_position: LockPosition,
90}
91
92impl PushDrop {
93    /// Creates a new PushDrop template with lock-before pattern.
94    ///
95    /// # Arguments
96    ///
97    /// * `locking_public_key` - Public key that can spend this output
98    /// * `fields` - Data fields to embed
99    ///
100    /// # Example
101    ///
102    /// ```rust,ignore
103    /// use bsv_rs::script::templates::PushDrop;
104    /// use bsv_rs::primitives::ec::PrivateKey;
105    ///
106    /// let privkey = PrivateKey::random();
107    /// let pubkey = privkey.public_key();
108    /// let fields = vec![b"hello".to_vec(), b"world".to_vec()];
109    ///
110    /// let pushdrop = PushDrop::new(pubkey, fields);
111    /// let script = pushdrop.lock();
112    /// ```
113    pub fn new(locking_public_key: PublicKey, fields: Vec<Vec<u8>>) -> Self {
114        Self {
115            locking_public_key,
116            fields,
117            lock_position: LockPosition::Before,
118        }
119    }
120
121    /// Sets the lock position and returns self for chaining.
122    ///
123    /// # Arguments
124    ///
125    /// * `position` - The lock position (Before or After)
126    ///
127    /// # Example
128    ///
129    /// ```rust,ignore
130    /// use bsv_rs::script::templates::{PushDrop, LockPosition};
131    /// use bsv_rs::primitives::ec::PrivateKey;
132    ///
133    /// let privkey = PrivateKey::random();
134    /// let pubkey = privkey.public_key();
135    ///
136    /// let pushdrop = PushDrop::new(pubkey, vec![b"data".to_vec()])
137    ///     .with_position(LockPosition::After);
138    /// ```
139    pub fn with_position(mut self, position: LockPosition) -> Self {
140        self.lock_position = position;
141        self
142    }
143
144    /// Creates the locking script.
145    ///
146    /// # Returns
147    ///
148    /// The locking script containing the embedded data and P2PK lock.
149    pub fn lock(&self) -> LockingScript {
150        let mut chunks = Vec::new();
151
152        match self.lock_position {
153            LockPosition::Before => {
154                // <pubkey> OP_CHECKSIG <fields...> OP_2DROP... OP_DROP
155                chunks.push(ScriptChunk::new_push(
156                    self.locking_public_key.to_compressed().to_vec(),
157                ));
158                chunks.push(ScriptChunk::new_opcode(OP_CHECKSIG));
159
160                // Add fields with minimal encoding
161                for field in &self.fields {
162                    chunks.push(Self::create_minimally_encoded_chunk(field));
163                }
164
165                // Add DROP operations
166                Self::add_drop_operations(&mut chunks, self.fields.len());
167            }
168            LockPosition::After => {
169                // <fields...> OP_2DROP... OP_DROP <pubkey> OP_CHECKSIG
170                for field in &self.fields {
171                    chunks.push(Self::create_minimally_encoded_chunk(field));
172                }
173
174                Self::add_drop_operations(&mut chunks, self.fields.len());
175
176                chunks.push(ScriptChunk::new_push(
177                    self.locking_public_key.to_compressed().to_vec(),
178                ));
179                chunks.push(ScriptChunk::new_opcode(OP_CHECKSIG));
180            }
181        }
182
183        LockingScript::from_chunks(chunks)
184    }
185
186    /// Creates a minimally encoded script chunk for data.
187    ///
188    /// Bitcoin Script requires minimal encoding for data pushes:
189    /// - Empty data or single zero byte -> OP_0
190    /// - Single byte 1-16 -> OP_1 through OP_16
191    /// - Single byte 0x81 (-1) -> OP_1NEGATE
192    /// - Otherwise -> standard data push
193    fn create_minimally_encoded_chunk(data: &[u8]) -> ScriptChunk {
194        // Empty or single zero byte -> OP_0
195        if data.is_empty() || (data.len() == 1 && data[0] == 0) {
196            return ScriptChunk::new_opcode(OP_0);
197        }
198
199        // Single byte special cases
200        if data.len() == 1 {
201            let byte = data[0];
202            // Values 1-16 -> OP_1 through OP_16
203            if (1..=16).contains(&byte) {
204                // OP_1 = 0x51, so OP_N = 0x50 + N
205                return ScriptChunk::new_opcode(0x50 + byte);
206            }
207            // 0x81 (-1 in Bitcoin script number encoding) -> OP_1NEGATE
208            if byte == 0x81 {
209                return ScriptChunk::new_opcode(OP_1NEGATE);
210            }
211        }
212
213        // Otherwise, standard data push
214        ScriptChunk::new_push(data.to_vec())
215    }
216
217    /// Adds appropriate DROP operations for the field count.
218    ///
219    /// Uses OP_2DROP for pairs of fields and OP_DROP for a remaining single field.
220    fn add_drop_operations(chunks: &mut Vec<ScriptChunk>, count: usize) {
221        let mut remaining = count;
222
223        // Use OP_2DROP for pairs
224        while remaining >= 2 {
225            chunks.push(ScriptChunk::new_opcode(OP_2DROP));
226            remaining -= 2;
227        }
228
229        // Use OP_DROP for remaining single item
230        if remaining == 1 {
231            chunks.push(ScriptChunk::new_opcode(OP_DROP));
232        }
233    }
234
235    /// Decodes a PushDrop locking script.
236    ///
237    /// Extracts the public key and embedded fields from a locking script.
238    ///
239    /// # Arguments
240    ///
241    /// * `script` - The locking script to decode
242    ///
243    /// # Returns
244    ///
245    /// The decoded PushDrop template, or an error if the script format is invalid.
246    ///
247    /// # Example
248    ///
249    /// ```rust,ignore
250    /// use bsv_rs::script::templates::PushDrop;
251    /// use bsv_rs::script::LockingScript;
252    ///
253    /// let script = LockingScript::from_hex("...")?;
254    /// let pushdrop = PushDrop::decode(&script)?;
255    /// println!("Fields: {:?}", pushdrop.fields);
256    /// ```
257    pub fn decode(script: &LockingScript) -> Result<Self> {
258        let chunks = script.chunks();
259
260        if chunks.len() < 2 {
261            return Err(Error::ScriptParseError(
262                "Script too short for PushDrop".into(),
263            ));
264        }
265
266        // Determine lock position by checking first chunk
267        // If first chunk is a 33 or 65 byte data push, it's lock-before
268        let first_is_pubkey = chunks[0]
269            .data
270            .as_ref()
271            .map(|d| d.len() == 33 || d.len() == 65)
272            .unwrap_or(false);
273
274        if first_is_pubkey {
275            Self::decode_lock_before(&chunks)
276        } else {
277            Self::decode_lock_after(&chunks)
278        }
279    }
280
281    /// Decodes a lock-before pattern script.
282    /// Pattern: <pubkey> OP_CHECKSIG <fields...> OP_2DROP... OP_DROP
283    fn decode_lock_before(chunks: &[ScriptChunk]) -> Result<Self> {
284        // First chunk must be pubkey data
285        let pubkey_data = chunks[0]
286            .data
287            .as_ref()
288            .ok_or_else(|| Error::ScriptParseError("Expected public key".into()))?;
289        let locking_public_key = PublicKey::from_bytes(pubkey_data)?;
290
291        // Second chunk must be OP_CHECKSIG
292        if chunks[1].op != OP_CHECKSIG {
293            return Err(Error::ScriptParseError(
294                "Expected OP_CHECKSIG after pubkey".into(),
295            ));
296        }
297
298        // Extract fields (between OP_CHECKSIG and first DROP)
299        let mut fields = Vec::new();
300        for chunk in chunks.iter().skip(2) {
301            // Stop when we hit a DROP operation
302            if chunk.op == OP_DROP || chunk.op == OP_2DROP {
303                break;
304            }
305            fields.push(Self::chunk_to_bytes(chunk));
306        }
307
308        Ok(Self {
309            locking_public_key,
310            fields,
311            lock_position: LockPosition::Before,
312        })
313    }
314
315    /// Decodes a lock-after pattern script.
316    /// Pattern: <fields...> OP_2DROP... OP_DROP <pubkey> OP_CHECKSIG
317    fn decode_lock_after(chunks: &[ScriptChunk]) -> Result<Self> {
318        if chunks.len() < 2 {
319            return Err(Error::ScriptParseError("Script too short".into()));
320        }
321
322        let last_idx = chunks.len() - 1;
323
324        // Last chunk must be OP_CHECKSIG
325        if chunks[last_idx].op != OP_CHECKSIG {
326            return Err(Error::ScriptParseError(
327                "Expected OP_CHECKSIG at end".into(),
328            ));
329        }
330
331        // Second to last must be pubkey
332        let pubkey_data = chunks[last_idx - 1].data.as_ref().ok_or_else(|| {
333            Error::ScriptParseError("Expected public key before OP_CHECKSIG".into())
334        })?;
335        let locking_public_key = PublicKey::from_bytes(pubkey_data)?;
336
337        // Extract fields (before the DROP operations)
338        let mut fields = Vec::new();
339        for chunk in chunks.iter().take(last_idx - 1) {
340            // Stop when we hit a DROP operation
341            if chunk.op == OP_DROP || chunk.op == OP_2DROP {
342                break;
343            }
344            fields.push(Self::chunk_to_bytes(chunk));
345        }
346
347        Ok(Self {
348            locking_public_key,
349            fields,
350            lock_position: LockPosition::After,
351        })
352    }
353
354    /// Converts a script chunk to bytes, handling minimal encoding.
355    fn chunk_to_bytes(chunk: &ScriptChunk) -> Vec<u8> {
356        // If chunk has data, return it
357        if let Some(ref data) = chunk.data {
358            return data.clone();
359        }
360
361        // Handle opcodes that represent data
362        let op = chunk.op;
363
364        // OP_0 -> [0]
365        if op == OP_0 {
366            return vec![0];
367        }
368
369        // OP_1 through OP_16 -> [1] through [16]
370        if (OP_1..=OP_16).contains(&op) {
371            return vec![op - 0x50];
372        }
373
374        // OP_1NEGATE -> [0x81]
375        if op == OP_1NEGATE {
376            return vec![0x81];
377        }
378
379        // Other opcodes have no data representation
380        Vec::new()
381    }
382
383    /// Creates an unlock template for spending a PushDrop output.
384    ///
385    /// PushDrop uses a P2PK (Pay-to-Public-Key) lock, so the unlocking script
386    /// is just a signature. Unlike P2PKH, the public key is already in the
387    /// locking script, so it doesn't need to be repeated in the unlock.
388    ///
389    /// # Arguments
390    ///
391    /// * `private_key` - The private key for signing (must match the public key in the lock)
392    /// * `sign_outputs` - Which outputs to sign
393    /// * `anyone_can_pay` - Whether to allow other inputs to be added
394    ///
395    /// # Returns
396    ///
397    /// A [`ScriptTemplateUnlock`] that can sign transaction inputs.
398    ///
399    /// # Example
400    ///
401    /// ```rust,ignore
402    /// use bsv_rs::script::templates::PushDrop;
403    /// use bsv_rs::script::template::{SignOutputs, SigningContext};
404    /// use bsv_rs::primitives::ec::PrivateKey;
405    ///
406    /// let private_key = PrivateKey::random();
407    /// let public_key = private_key.public_key();
408    /// let fields = vec![b"token_data".to_vec()];
409    ///
410    /// // Create locking script
411    /// let pushdrop = PushDrop::new(public_key, fields);
412    /// let locking_script = pushdrop.lock();
413    ///
414    /// // Create unlock template
415    /// let unlock = PushDrop::unlock(&private_key, SignOutputs::All, false);
416    ///
417    /// // Estimate length for fee calculation (73 bytes)
418    /// let estimated_size = unlock.estimate_length();
419    ///
420    /// // Sign with a transaction context
421    /// let context = SigningContext::new(&raw_tx, input_index, satoshis, locking_script.as_script());
422    /// let unlocking_script = unlock.sign(&context)?;
423    /// ```
424    pub fn unlock(
425        private_key: &PrivateKey,
426        sign_outputs: SignOutputs,
427        anyone_can_pay: bool,
428    ) -> ScriptTemplateUnlock {
429        let key = private_key.clone();
430        let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
431
432        ScriptTemplateUnlock::new(
433            move |context: &SigningContext| {
434                // Compute the sighash
435                let sighash = context.compute_sighash(scope)?;
436
437                // Sign the sighash
438                let signature = key.sign(&sighash)?;
439                let tx_sig = TransactionSignature::new(signature, scope);
440
441                // Build the unlocking script (signature only, no pubkey for P2PK)
442                let sig_bytes = tx_sig.to_checksig_format();
443
444                let mut script = Script::new();
445                script.write_bin(&sig_bytes);
446
447                Ok(UnlockingScript::from_script(script))
448            },
449            || {
450                // Estimate length: 1 (push opcode) + 72 (max DER signature + sighash byte)
451                // = 73 bytes (matches TypeScript SDK)
452                73
453            },
454        )
455    }
456
457    /// Creates an unlocking script with a precomputed sighash.
458    ///
459    /// This is useful when you already have the sighash computed and don't
460    /// need to parse the transaction.
461    ///
462    /// # Arguments
463    ///
464    /// * `private_key` - The private key for signing
465    /// * `sighash` - The precomputed sighash to sign
466    /// * `sign_outputs` - Which outputs to sign (for the scope byte)
467    /// * `anyone_can_pay` - Whether to allow other inputs to be added
468    ///
469    /// # Returns
470    ///
471    /// The unlocking script, or an error if signing fails.
472    ///
473    /// # Example
474    ///
475    /// ```rust,ignore
476    /// use bsv_rs::script::templates::PushDrop;
477    /// use bsv_rs::script::template::SignOutputs;
478    /// use bsv_rs::primitives::ec::PrivateKey;
479    ///
480    /// let private_key = PrivateKey::random();
481    /// let sighash: [u8; 32] = compute_sighash_externally();
482    ///
483    /// let unlocking = PushDrop::sign_with_sighash(
484    ///     &private_key,
485    ///     &sighash,
486    ///     SignOutputs::All,
487    ///     false,
488    /// )?;
489    /// ```
490    pub fn sign_with_sighash(
491        private_key: &PrivateKey,
492        sighash: &[u8; 32],
493        sign_outputs: SignOutputs,
494        anyone_can_pay: bool,
495    ) -> Result<UnlockingScript> {
496        let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
497
498        // Sign the sighash
499        let signature = private_key.sign(sighash)?;
500        let tx_sig = TransactionSignature::new(signature, scope);
501
502        // Build the unlocking script (signature only for P2PK)
503        let sig_bytes = tx_sig.to_checksig_format();
504
505        let mut script = Script::new();
506        script.write_bin(&sig_bytes);
507
508        Ok(UnlockingScript::from_script(script))
509    }
510
511    /// Estimates the unlocking script length.
512    ///
513    /// For a PushDrop output (P2PK lock), the unlocking script is just a signature.
514    /// Unlike P2PKH, the public key is already in the locking script.
515    ///
516    /// # Returns
517    ///
518    /// The estimated length in bytes (73 bytes for signature only).
519    /// This matches the TypeScript SDK's estimate.
520    pub fn estimate_unlocking_length(&self) -> usize {
521        // For P2PK style: 1 (push opcode) + ~72 (DER signature + sighash byte)
522        // = 73 bytes max
523        // This matches the TypeScript SDK's estimateLength() return value
524        73
525    }
526}
527
528impl PartialEq for PushDrop {
529    fn eq(&self, other: &Self) -> bool {
530        self.locking_public_key.to_compressed() == other.locking_public_key.to_compressed()
531            && self.fields == other.fields
532            && self.lock_position == other.lock_position
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539    use crate::primitives::ec::PrivateKey;
540
541    #[test]
542    fn test_pushdrop_lock_before() {
543        let privkey = PrivateKey::random();
544        let pubkey = privkey.public_key();
545
546        let fields = vec![b"field1".to_vec(), b"field2".to_vec()];
547        let pushdrop = PushDrop::new(pubkey.clone(), fields.clone());
548        let script = pushdrop.lock();
549
550        // Decode and verify
551        let decoded = PushDrop::decode(&script).unwrap();
552        assert_eq!(
553            decoded.locking_public_key.to_compressed(),
554            pubkey.to_compressed()
555        );
556        assert_eq!(decoded.fields, fields);
557        assert_eq!(decoded.lock_position, LockPosition::Before);
558    }
559
560    #[test]
561    fn test_pushdrop_lock_after() {
562        let privkey = PrivateKey::random();
563        let pubkey = privkey.public_key();
564
565        let fields = vec![b"data".to_vec()];
566        let pushdrop =
567            PushDrop::new(pubkey.clone(), fields.clone()).with_position(LockPosition::After);
568        let script = pushdrop.lock();
569
570        let decoded = PushDrop::decode(&script).unwrap();
571        assert_eq!(decoded.lock_position, LockPosition::After);
572        assert_eq!(
573            decoded.locking_public_key.to_compressed(),
574            pubkey.to_compressed()
575        );
576    }
577
578    #[test]
579    fn test_pushdrop_minimal_encoding() {
580        let privkey = PrivateKey::random();
581        let pubkey = privkey.public_key();
582
583        // Test minimal encoding for small values
584        let fields = vec![
585            vec![0],    // Should become OP_0
586            vec![1],    // Should become OP_1
587            vec![16],   // Should become OP_16
588            vec![0x81], // Should become OP_1NEGATE
589            vec![17],   // Regular push (not a special opcode)
590        ];
591
592        let pushdrop = PushDrop::new(pubkey, fields.clone());
593        let script = pushdrop.lock();
594
595        let decoded = PushDrop::decode(&script).unwrap();
596        assert_eq!(decoded.fields.len(), 5);
597
598        // Check the decoded values match the original fields
599        assert_eq!(decoded.fields[0], vec![0]);
600        assert_eq!(decoded.fields[1], vec![1]);
601        assert_eq!(decoded.fields[2], vec![16]);
602        assert_eq!(decoded.fields[3], vec![0x81]);
603        assert_eq!(decoded.fields[4], vec![17]);
604    }
605
606    #[test]
607    fn test_pushdrop_empty_fields() {
608        let privkey = PrivateKey::random();
609        let pubkey = privkey.public_key();
610
611        let pushdrop = PushDrop::new(pubkey.clone(), vec![]);
612        let script = pushdrop.lock();
613
614        // Should be simple <pubkey> OP_CHECKSIG (like P2PK)
615        let chunks = script.chunks();
616        assert_eq!(chunks.len(), 2);
617
618        let decoded = PushDrop::decode(&script).unwrap();
619        assert!(decoded.fields.is_empty());
620    }
621
622    #[test]
623    fn test_pushdrop_large_field() {
624        let privkey = PrivateKey::random();
625        let pubkey = privkey.public_key();
626
627        // 10KB field
628        let large_field = vec![0xABu8; 10_000];
629        let pushdrop = PushDrop::new(pubkey, vec![large_field.clone()]);
630        let script = pushdrop.lock();
631
632        let decoded = PushDrop::decode(&script).unwrap();
633        assert_eq!(decoded.fields[0], large_field);
634    }
635
636    #[test]
637    fn test_pushdrop_drop_count() {
638        let privkey = PrivateKey::random();
639        let pubkey = privkey.public_key();
640
641        // 5 fields should produce 2x OP_2DROP + 1x OP_DROP
642        let fields: Vec<Vec<u8>> = (0..5).map(|i| vec![i as u8 + 17]).collect(); // Use values > 16 to avoid OP_N
643        let pushdrop = PushDrop::new(pubkey, fields);
644        let script = pushdrop.lock();
645
646        let chunks = script.chunks();
647        let drop_count = chunks.iter().filter(|c| c.op == OP_DROP).count();
648        let drop2_count = chunks.iter().filter(|c| c.op == OP_2DROP).count();
649
650        assert_eq!(drop2_count, 2);
651        assert_eq!(drop_count, 1);
652    }
653
654    #[test]
655    fn test_pushdrop_estimate_unlocking_length() {
656        let privkey = PrivateKey::random();
657        let pubkey = privkey.public_key();
658
659        let pushdrop = PushDrop::new(pubkey, vec![b"test".to_vec()]);
660        // P2PK unlocking is just <signature>, so 73 bytes (1 push + 72 max DER + sighash)
661        assert_eq!(pushdrop.estimate_unlocking_length(), 73);
662    }
663
664    #[test]
665    fn test_pushdrop_unlock_estimate_length() {
666        use crate::script::template::SignOutputs;
667
668        let privkey = PrivateKey::random();
669        let unlock = PushDrop::unlock(&privkey, SignOutputs::All, false);
670
671        // Should match the instance method
672        assert_eq!(unlock.estimate_length(), 73);
673    }
674
675    #[test]
676    fn test_pushdrop_sign_with_sighash() {
677        use crate::script::template::SignOutputs;
678
679        let private_key = PrivateKey::from_hex(
680            "0000000000000000000000000000000000000000000000000000000000000001",
681        )
682        .unwrap();
683
684        // Create a simple test case with a mock sighash
685        let sighash = [1u8; 32];
686        let unlocking =
687            PushDrop::sign_with_sighash(&private_key, &sighash, SignOutputs::All, false).unwrap();
688
689        // The unlocking script should have 1 chunk: signature only (P2PK style)
690        let chunks = unlocking.chunks();
691        assert_eq!(chunks.len(), 1);
692
693        // First (and only) chunk should be the signature (push data)
694        assert!(chunks[0].data.is_some());
695        let sig_data = chunks[0].data.as_ref().unwrap();
696        // DER signature + 1 byte sighash
697        assert!(sig_data.len() >= 70 && sig_data.len() <= 73);
698
699        // Last byte should be the sighash type
700        assert_eq!(
701            sig_data.last().unwrap(),
702            &0x41_u8 // SIGHASH_ALL | SIGHASH_FORKID
703        );
704    }
705
706    #[test]
707    fn test_pushdrop_sign_outputs_variants() {
708        use crate::script::template::SignOutputs;
709
710        let private_key = PrivateKey::random();
711        let sighash = [1u8; 32];
712
713        // Test ALL
714        let unlocking =
715            PushDrop::sign_with_sighash(&private_key, &sighash, SignOutputs::All, false).unwrap();
716        let chunks = unlocking.chunks();
717        let sig_data = chunks[0].data.as_ref().unwrap();
718        assert_eq!(sig_data.last().unwrap(), &0x41u8); // ALL | FORKID
719
720        // Test NONE
721        let unlocking =
722            PushDrop::sign_with_sighash(&private_key, &sighash, SignOutputs::None, false).unwrap();
723        let chunks = unlocking.chunks();
724        let sig_data = chunks[0].data.as_ref().unwrap();
725        assert_eq!(sig_data.last().unwrap(), &0x42u8); // NONE | FORKID
726
727        // Test SINGLE
728        let unlocking =
729            PushDrop::sign_with_sighash(&private_key, &sighash, SignOutputs::Single, false)
730                .unwrap();
731        let chunks = unlocking.chunks();
732        let sig_data = chunks[0].data.as_ref().unwrap();
733        assert_eq!(sig_data.last().unwrap(), &0x43u8); // SINGLE | FORKID
734
735        // Test ALL | ANYONECANPAY
736        let unlocking =
737            PushDrop::sign_with_sighash(&private_key, &sighash, SignOutputs::All, true).unwrap();
738        let chunks = unlocking.chunks();
739        let sig_data = chunks[0].data.as_ref().unwrap();
740        assert_eq!(sig_data.last().unwrap(), &0xC1u8); // ALL | FORKID | ANYONECANPAY
741    }
742
743    #[test]
744    fn test_pushdrop_empty_data_becomes_op_0() {
745        let privkey = PrivateKey::random();
746        let pubkey = privkey.public_key();
747
748        // Empty vec should become OP_0
749        let fields = vec![vec![]];
750        let pushdrop = PushDrop::new(pubkey, fields);
751        let script = pushdrop.lock();
752
753        // Third chunk (after pubkey and OP_CHECKSIG) should be OP_0
754        let chunks = script.chunks();
755        assert_eq!(chunks[2].op, OP_0);
756        assert!(chunks[2].data.is_none());
757
758        // Decoded should give us [0] back (our convention for OP_0)
759        let decoded = PushDrop::decode(&script).unwrap();
760        assert_eq!(decoded.fields[0], vec![0]);
761    }
762
763    #[test]
764    fn test_pushdrop_roundtrip_all_special_values() {
765        let privkey = PrivateKey::random();
766        let pubkey = privkey.public_key();
767
768        // Test all special encoding cases
769        let fields = vec![
770            vec![],     // OP_0
771            vec![0],    // OP_0
772            vec![1],    // OP_1
773            vec![2],    // OP_2
774            vec![15],   // OP_15
775            vec![16],   // OP_16
776            vec![0x81], // OP_1NEGATE
777            vec![17],   // Regular push
778            vec![255],  // Regular push
779        ];
780
781        let pushdrop = PushDrop::new(pubkey, fields);
782        let script = pushdrop.lock();
783
784        let decoded = PushDrop::decode(&script).unwrap();
785
786        // Empty and [0] both decode to [0] (OP_0)
787        assert_eq!(decoded.fields[0], vec![0]);
788        assert_eq!(decoded.fields[1], vec![0]);
789        assert_eq!(decoded.fields[2], vec![1]);
790        assert_eq!(decoded.fields[3], vec![2]);
791        assert_eq!(decoded.fields[4], vec![15]);
792        assert_eq!(decoded.fields[5], vec![16]);
793        assert_eq!(decoded.fields[6], vec![0x81]);
794        assert_eq!(decoded.fields[7], vec![17]);
795        assert_eq!(decoded.fields[8], vec![255]);
796    }
797
798    #[test]
799    fn test_pushdrop_lock_after_multiple_fields() {
800        let privkey = PrivateKey::random();
801        let pubkey = privkey.public_key();
802
803        let fields = vec![b"token".to_vec(), b"transfer".to_vec(), b"100".to_vec()];
804        let pushdrop =
805            PushDrop::new(pubkey.clone(), fields.clone()).with_position(LockPosition::After);
806        let script = pushdrop.lock();
807
808        let decoded = PushDrop::decode(&script).unwrap();
809        assert_eq!(decoded.fields, fields);
810        assert_eq!(decoded.lock_position, LockPosition::After);
811        assert_eq!(
812            decoded.locking_public_key.to_compressed(),
813            pubkey.to_compressed()
814        );
815    }
816}