Skip to main content

bsv/script/templates/
push_drop.rs

1//! PushDrop script template for embedding data in Bitcoin scripts.
2//!
3//! PushDrop creates scripts that embed arbitrary data fields followed by
4//! OP_DROP operations to clean the stack, then lock with OP_CHECKSIG.
5//! This enables data storage on-chain while maintaining spending control.
6//! Translates the TS SDK PushDrop.ts (simplified without WalletInterface).
7
8use crate::primitives::ecdsa::ecdsa_sign;
9use crate::primitives::hash::sha256;
10use crate::primitives::private_key::PrivateKey;
11use crate::primitives::transaction_signature::{SIGHASH_ALL, SIGHASH_FORKID};
12use crate::script::error::ScriptError;
13use crate::script::locking_script::LockingScript;
14use crate::script::op::Op;
15use crate::script::script::Script;
16use crate::script::script_chunk::ScriptChunk;
17use crate::script::templates::{ScriptTemplateLock, ScriptTemplateUnlock};
18use crate::script::unlocking_script::UnlockingScript;
19
20/// PushDrop script template for embedding data with spending control.
21///
22/// Creates a locking script that pushes data fields onto the stack,
23/// drops them with OP_DROP operations, then verifies a signature
24/// against a public key (OP_CHECKSIG).
25#[derive(Clone, Debug)]
26pub struct PushDrop {
27    /// Data fields to embed in the script.
28    pub fields: Vec<Vec<u8>>,
29    /// Private key for signing (used for both lock pubkey and unlock signature).
30    pub private_key: Option<PrivateKey>,
31    /// Sighash scope for signing (default: SIGHASH_ALL | SIGHASH_FORKID).
32    pub sighash_type: u32,
33}
34
35impl PushDrop {
36    /// Create a PushDrop template with data fields and a key for locking and unlocking.
37    ///
38    /// The private key's public key will be used in the locking script,
39    /// and the private key will be used for signing in the unlocking script.
40    pub fn new(fields: Vec<Vec<u8>>, key: PrivateKey) -> Self {
41        PushDrop {
42            fields,
43            private_key: Some(key),
44            sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
45        }
46    }
47
48    /// Create a PushDrop template for locking only (no signing capability).
49    ///
50    /// Requires knowing the public key bytes to embed in the script.
51    /// Use `new()` instead if you also need unlock capability.
52    pub fn lock_only(fields: Vec<Vec<u8>>) -> Self {
53        PushDrop {
54            fields,
55            private_key: None,
56            sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
57        }
58    }
59
60    /// Create an unlocking script from a sighash preimage.
61    ///
62    /// Produces: `<signature_DER + sighash_byte>`
63    pub fn unlock(&self, preimage: &[u8]) -> Result<UnlockingScript, ScriptError> {
64        let key = self.private_key.as_ref().ok_or_else(|| {
65            ScriptError::InvalidScript("PushDrop: no private key for unlock".into())
66        })?;
67
68        let msg_hash = sha256(preimage);
69        let sig = ecdsa_sign(&msg_hash, key.bn(), true)
70            .map_err(|e| ScriptError::InvalidSignature(format!("ECDSA sign failed: {}", e)))?;
71
72        let mut sig_bytes = sig.to_der();
73        sig_bytes.push(self.sighash_type as u8);
74
75        let chunks = vec![ScriptChunk::new_raw(sig_bytes.len() as u8, Some(sig_bytes))];
76
77        Ok(UnlockingScript::from_script(Script::from_chunks(chunks)))
78    }
79
80    /// Estimate the byte length of the unlocking script.
81    ///
82    /// PushDrop unlock is just a signature: approximately 74 bytes
83    /// (1 push opcode + up to 72 DER sig bytes + 1 sighash byte).
84    pub fn estimate_unlock_length(&self) -> usize {
85        74
86    }
87
88    /// Decode a PushDrop locking script, recovering the embedded data fields.
89    ///
90    /// Parses the script pattern:
91    /// `<field_1> <field_2> ... <field_N> OP_DROP|OP_2DROP... <pubkey> OP_CHECKSIG`
92    ///
93    /// Returns a PushDrop with the extracted fields, no private key, and default sighash.
94    pub fn decode(script: &LockingScript) -> Result<PushDrop, ScriptError> {
95        let chunks = script.chunks();
96        if chunks.len() < 3 {
97            return Err(ScriptError::InvalidScript(
98                "PushDrop::decode: script too short".into(),
99            ));
100        }
101
102        // Last chunk must be OP_CHECKSIG
103        let last = &chunks[chunks.len() - 1];
104        if last.op != Op::OpCheckSig {
105            return Err(ScriptError::InvalidScript(
106                "PushDrop::decode: last opcode must be OP_CHECKSIG".into(),
107            ));
108        }
109
110        // Second-to-last must be a pubkey data push
111        let pubkey_chunk = &chunks[chunks.len() - 2];
112        if pubkey_chunk.data.is_none() {
113            return Err(ScriptError::InvalidScript(
114                "PushDrop::decode: expected pubkey data push before OP_CHECKSIG".into(),
115            ));
116        }
117
118        // Walk backwards from before the pubkey to count OP_DROP and OP_2DROP
119        let mut drop_field_count = 0usize;
120        let mut pos = chunks.len() - 3; // start just before pubkey
121        loop {
122            let chunk = &chunks[pos];
123            if chunk.op == Op::Op2Drop {
124                drop_field_count += 2;
125            } else if chunk.op == Op::OpDrop {
126                drop_field_count += 1;
127            } else {
128                break;
129            }
130            if pos == 0 {
131                break;
132            }
133            pos -= 1;
134        }
135
136        if drop_field_count == 0 {
137            return Err(ScriptError::InvalidScript(
138                "PushDrop::decode: no OP_DROP/OP_2DROP found".into(),
139            ));
140        }
141
142        // The leading chunks (0..drop_field_count) should be data pushes
143        if drop_field_count > pos + 1 {
144            return Err(ScriptError::InvalidScript(
145                "PushDrop::decode: not enough data pushes for drop count".into(),
146            ));
147        }
148
149        // Data fields are the first `drop_field_count` chunks
150        // pos currently points to the last non-drop chunk before drops, which should be
151        // the last data field. But we need to calculate: data fields end at the chunk
152        // just before the first drop opcode.
153        let data_end = pos + 1; // exclusive end of data field range
154        if data_end != drop_field_count {
155            return Err(ScriptError::InvalidScript(format!(
156                "PushDrop::decode: field count mismatch: {} data chunks but {} drops",
157                data_end, drop_field_count
158            )));
159        }
160
161        let mut fields = Vec::with_capacity(drop_field_count);
162        for chunk in &chunks[0..drop_field_count] {
163            let data = chunk.data.as_ref().ok_or_else(|| {
164                ScriptError::InvalidScript("PushDrop::decode: expected data push for field".into())
165            })?;
166            fields.push(data.clone());
167        }
168
169        Ok(PushDrop {
170            fields,
171            private_key: None,
172            sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
173        })
174    }
175
176    /// Create a data push chunk with appropriate opcode for the data length.
177    fn make_data_push(data: &[u8]) -> ScriptChunk {
178        let len = data.len();
179        if len < 0x4c {
180            // Direct push: opcode IS the length
181            ScriptChunk::new_raw(len as u8, Some(data.to_vec()))
182        } else if len < 256 {
183            ScriptChunk::new_raw(Op::OpPushData1.to_byte(), Some(data.to_vec()))
184        } else if len < 65536 {
185            ScriptChunk::new_raw(Op::OpPushData2.to_byte(), Some(data.to_vec()))
186        } else {
187            ScriptChunk::new_raw(Op::OpPushData4.to_byte(), Some(data.to_vec()))
188        }
189    }
190}
191
192impl ScriptTemplateLock for PushDrop {
193    /// Create a PushDrop locking script.
194    ///
195    /// Structure: `<field_1> <field_2> ... <field_N> OP_DROP|OP_2DROP... <pubkey> OP_CHECKSIG`
196    ///
197    /// Each field is pushed as data, then removed with OP_DROP (or OP_2DROP
198    /// for pairs). The final element on stack will be verified against the
199    /// embedded public key via OP_CHECKSIG.
200    fn lock(&self) -> Result<LockingScript, ScriptError> {
201        let key = self.private_key.as_ref().ok_or_else(|| {
202            ScriptError::InvalidScript(
203                "PushDrop: need private key to derive pubkey for lock".into(),
204            )
205        })?;
206
207        if self.fields.is_empty() {
208            return Err(ScriptError::InvalidScript(
209                "PushDrop: at least one data field required".into(),
210            ));
211        }
212
213        let mut chunks = Vec::new();
214
215        // Push each data field
216        for field in &self.fields {
217            chunks.push(Self::make_data_push(field));
218        }
219
220        // Add OP_DROP for each field to clean the stack
221        // Use OP_2DROP where possible for efficiency
222        let num_fields = self.fields.len();
223        let num_2drops = num_fields / 2;
224        let num_drops = num_fields % 2;
225
226        for _ in 0..num_2drops {
227            chunks.push(ScriptChunk::new_opcode(Op::Op2Drop));
228        }
229        for _ in 0..num_drops {
230            chunks.push(ScriptChunk::new_opcode(Op::OpDrop));
231        }
232
233        // Add <pubkey> OP_CHECKSIG
234        let pubkey = key.to_public_key();
235        let pubkey_bytes = pubkey.to_der();
236        chunks.push(ScriptChunk::new_raw(
237            pubkey_bytes.len() as u8,
238            Some(pubkey_bytes),
239        ));
240        chunks.push(ScriptChunk::new_opcode(Op::OpCheckSig));
241
242        Ok(LockingScript::from_script(Script::from_chunks(chunks)))
243    }
244}
245
246impl ScriptTemplateUnlock for PushDrop {
247    fn sign(&self, preimage: &[u8]) -> Result<UnlockingScript, ScriptError> {
248        self.unlock(preimage)
249    }
250
251    fn estimate_length(&self) -> Result<usize, ScriptError> {
252        Ok(self.estimate_unlock_length())
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    // -----------------------------------------------------------------------
261    // PushDrop::decode tests
262    // -----------------------------------------------------------------------
263
264    #[test]
265    fn test_pushdrop_decode_roundtrip_one_field() {
266        let key = PrivateKey::from_hex("1").unwrap();
267        let fields = vec![vec![0xca, 0xfe, 0xba, 0xbe]];
268        let pd = PushDrop::new(fields.clone(), key);
269        let lock_script = pd.lock().unwrap();
270
271        let decoded = PushDrop::decode(&lock_script).unwrap();
272        assert_eq!(decoded.fields, fields, "decode should recover 1 field");
273    }
274
275    #[test]
276    fn test_pushdrop_decode_roundtrip_two_fields() {
277        let key = PrivateKey::from_hex("1").unwrap();
278        let fields = vec![vec![0x01, 0x02], vec![0x03, 0x04]];
279        let pd = PushDrop::new(fields.clone(), key);
280        let lock_script = pd.lock().unwrap();
281
282        let decoded = PushDrop::decode(&lock_script).unwrap();
283        assert_eq!(
284            decoded.fields, fields,
285            "decode should recover 2 fields (OP_2DROP)"
286        );
287    }
288
289    #[test]
290    fn test_pushdrop_decode_roundtrip_three_fields() {
291        let key = PrivateKey::from_hex("1").unwrap();
292        let fields = vec![vec![0x01], vec![0x02], vec![0x03]];
293        let pd = PushDrop::new(fields.clone(), key);
294        let lock_script = pd.lock().unwrap();
295
296        let decoded = PushDrop::decode(&lock_script).unwrap();
297        assert_eq!(
298            decoded.fields, fields,
299            "decode should recover 3 fields (OP_2DROP + OP_DROP)"
300        );
301    }
302
303    #[test]
304    fn test_pushdrop_decode_non_pushdrop_script_errors() {
305        // A simple P2PKH script should not decode as PushDrop
306        let script = LockingScript::from_binary(&[0x76, 0xa9, 0x14]);
307        assert!(PushDrop::decode(&script).is_err());
308    }
309
310    // -----------------------------------------------------------------------
311    // PushDrop lock: 1 field produces script with data and OP_DROP
312    // -----------------------------------------------------------------------
313
314    #[test]
315    fn test_pushdrop_lock_one_field() {
316        let key = PrivateKey::from_hex("1").unwrap();
317        let data = vec![0xca, 0xfe, 0xba, 0xbe];
318        let pd = PushDrop::new(vec![data.clone()], key);
319
320        let lock_script = pd.lock().unwrap();
321        let chunks = lock_script.chunks();
322
323        // Should have: <data> OP_DROP <pubkey> OP_CHECKSIG = 4 chunks
324        assert_eq!(chunks.len(), 4, "1-field PushDrop should have 4 chunks");
325
326        // First chunk: data push
327        assert_eq!(chunks[0].data.as_ref().unwrap(), &data);
328        // Second: OP_DROP
329        assert_eq!(chunks[1].op, Op::OpDrop);
330        // Third: pubkey (33 bytes)
331        assert_eq!(chunks[2].data.as_ref().unwrap().len(), 33);
332        // Fourth: OP_CHECKSIG
333        assert_eq!(chunks[3].op, Op::OpCheckSig);
334    }
335
336    // -----------------------------------------------------------------------
337    // PushDrop lock: multiple fields includes all data
338    // -----------------------------------------------------------------------
339
340    #[test]
341    fn test_pushdrop_lock_multiple_fields() {
342        let key = PrivateKey::from_hex("1").unwrap();
343        let fields = vec![vec![0x01, 0x02], vec![0x03, 0x04], vec![0x05, 0x06]];
344        let pd = PushDrop::new(fields.clone(), key);
345
346        let lock_script = pd.lock().unwrap();
347        let chunks = lock_script.chunks();
348
349        // 3 data pushes + 1 OP_2DROP + 1 OP_DROP + 1 pubkey + 1 OP_CHECKSIG = 7
350        assert_eq!(chunks.len(), 7, "3-field PushDrop should have 7 chunks");
351
352        // Verify data fields are present
353        assert_eq!(chunks[0].data.as_ref().unwrap(), &fields[0]);
354        assert_eq!(chunks[1].data.as_ref().unwrap(), &fields[1]);
355        assert_eq!(chunks[2].data.as_ref().unwrap(), &fields[2]);
356
357        // OP_2DROP for first pair, OP_DROP for odd one
358        assert_eq!(chunks[3].op, Op::Op2Drop);
359        assert_eq!(chunks[4].op, Op::OpDrop);
360
361        // Pubkey + checksig
362        assert_eq!(chunks[6].op, Op::OpCheckSig);
363    }
364
365    // -----------------------------------------------------------------------
366    // PushDrop lock: even number of fields uses OP_2DROP efficiently
367    // -----------------------------------------------------------------------
368
369    #[test]
370    fn test_pushdrop_lock_even_fields() {
371        let key = PrivateKey::from_hex("1").unwrap();
372        let fields = vec![vec![0x01], vec![0x02]];
373        let pd = PushDrop::new(fields, key);
374
375        let lock_script = pd.lock().unwrap();
376        let chunks = lock_script.chunks();
377
378        // 2 data pushes + 1 OP_2DROP + 1 pubkey + 1 OP_CHECKSIG = 5 chunks
379        assert_eq!(chunks.len(), 5);
380        assert_eq!(chunks[2].op, Op::Op2Drop);
381    }
382
383    // -----------------------------------------------------------------------
384    // PushDrop unlock: produces valid signature
385    // -----------------------------------------------------------------------
386
387    #[test]
388    fn test_pushdrop_unlock_produces_signature() {
389        let key = PrivateKey::from_hex("1").unwrap();
390        let pd = PushDrop::new(vec![vec![0xaa]], key);
391
392        let unlock_script = pd.unlock(b"test preimage").unwrap();
393        assert_eq!(
394            unlock_script.chunks().len(),
395            1,
396            "PushDrop unlock should be 1 chunk (just sig)"
397        );
398
399        let sig_data = unlock_script.chunks()[0].data.as_ref().unwrap();
400        // DER signature is typically 70-73 bytes + 1 sighash byte
401        assert!(sig_data.len() >= 70 && sig_data.len() <= 74);
402        // Last byte is sighash
403        assert_eq!(
404            *sig_data.last().unwrap(),
405            (SIGHASH_ALL | SIGHASH_FORKID) as u8
406        );
407    }
408
409    // -----------------------------------------------------------------------
410    // PushDrop: estimate length
411    // -----------------------------------------------------------------------
412
413    #[test]
414    fn test_pushdrop_estimate_length() {
415        let key = PrivateKey::from_hex("1").unwrap();
416        let pd = PushDrop::new(vec![vec![0x01]], key);
417        assert_eq!(pd.estimate_unlock_length(), 74);
418    }
419
420    // -----------------------------------------------------------------------
421    // PushDrop: error cases
422    // -----------------------------------------------------------------------
423
424    #[test]
425    fn test_pushdrop_lock_no_key() {
426        let pd = PushDrop::lock_only(vec![vec![0x01]]);
427        assert!(pd.lock().is_err());
428    }
429
430    #[test]
431    fn test_pushdrop_lock_no_fields() {
432        let key = PrivateKey::from_hex("1").unwrap();
433        let pd = PushDrop::new(vec![], key);
434        assert!(pd.lock().is_err());
435    }
436
437    #[test]
438    fn test_pushdrop_unlock_no_key() {
439        let pd = PushDrop::lock_only(vec![vec![0x01]]);
440        assert!(pd.unlock(b"test").is_err());
441    }
442
443    // -----------------------------------------------------------------------
444    // PushDrop: trait implementations
445    // -----------------------------------------------------------------------
446
447    #[test]
448    fn test_pushdrop_trait_sign() {
449        let key = PrivateKey::from_hex("ff").unwrap();
450        let pd = PushDrop::new(vec![vec![0x01, 0x02, 0x03]], key);
451        let unlock_script = pd.sign(b"sighash data").unwrap();
452        assert_eq!(unlock_script.chunks().len(), 1);
453    }
454
455    // -----------------------------------------------------------------------
456    // PushDrop: binary roundtrip
457    // -----------------------------------------------------------------------
458
459    #[test]
460    fn test_pushdrop_lock_binary_roundtrip() {
461        let key = PrivateKey::from_hex("1").unwrap();
462        let pd = PushDrop::new(vec![vec![0xde, 0xad]], key);
463
464        let lock_script = pd.lock().unwrap();
465        let binary = lock_script.to_binary();
466
467        // Re-parse and verify
468        let reparsed = Script::from_binary(&binary);
469        assert_eq!(
470            reparsed.to_binary(),
471            binary,
472            "binary roundtrip should match"
473        );
474    }
475}