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}