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//!
7//! Supports two lock positions matching the TS SDK:
8//! - **Before** (default): `<pubkey> OP_CHECKSIG <fields...> OP_2DROP... OP_DROP`
9//! - **After**: `<fields...> OP_2DROP... OP_DROP <pubkey> OP_CHECKSIG`
10
11use crate::primitives::ecdsa::ecdsa_sign;
12use crate::primitives::hash::sha256;
13use crate::primitives::private_key::PrivateKey;
14use crate::primitives::transaction_signature::{SIGHASH_ALL, SIGHASH_FORKID};
15use crate::script::error::ScriptError;
16use crate::script::locking_script::LockingScript;
17use crate::script::op::Op;
18use crate::script::script::Script;
19use crate::script::script_chunk::ScriptChunk;
20use crate::script::templates::{ScriptTemplateLock, ScriptTemplateUnlock};
21use crate::script::unlocking_script::UnlockingScript;
22
23/// Lock position for the public key in the PushDrop script.
24#[derive(Clone, Copy, Debug, Default, PartialEq)]
25pub enum LockPosition {
26    /// `<pubkey> OP_CHECKSIG <fields...> OP_2DROP...` (TS default)
27    #[default]
28    Before,
29    /// `<fields...> OP_2DROP... <pubkey> OP_CHECKSIG`
30    After,
31}
32
33/// PushDrop script template for embedding data with spending control.
34///
35/// Creates a locking script that pushes data fields onto the stack,
36/// drops them with OP_DROP operations, then verifies a signature
37/// against a public key (OP_CHECKSIG).
38#[derive(Clone, Debug)]
39pub struct PushDrop {
40    /// Data fields to embed in the script.
41    pub fields: Vec<Vec<u8>>,
42    /// Private key for signing (used for both lock pubkey and unlock signature).
43    pub private_key: Option<PrivateKey>,
44    /// Sighash scope for signing (default: SIGHASH_ALL | SIGHASH_FORKID).
45    pub sighash_type: u32,
46    /// Where the locking pubkey is placed in the script.
47    pub lock_position: LockPosition,
48}
49
50impl PushDrop {
51    /// Create a PushDrop template with data fields and a key for locking and unlocking.
52    pub fn new(fields: Vec<Vec<u8>>, key: PrivateKey) -> Self {
53        PushDrop {
54            fields,
55            private_key: Some(key),
56            sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
57            lock_position: LockPosition::default(),
58        }
59    }
60
61    /// Create a PushDrop template for locking only (no signing capability).
62    pub fn lock_only(fields: Vec<Vec<u8>>) -> Self {
63        PushDrop {
64            fields,
65            private_key: None,
66            sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
67            lock_position: LockPosition::default(),
68        }
69    }
70
71    /// Set the lock position (builder pattern).
72    pub fn with_lock_position(mut self, position: LockPosition) -> Self {
73        self.lock_position = position;
74        self
75    }
76
77    /// Create an unlocking script from a sighash preimage.
78    pub fn unlock(&self, preimage: &[u8]) -> Result<UnlockingScript, ScriptError> {
79        let key = self.private_key.as_ref().ok_or_else(|| {
80            ScriptError::InvalidScript("PushDrop: no private key for unlock".into())
81        })?;
82
83        let msg_hash = sha256(preimage);
84        let sig = ecdsa_sign(&msg_hash, key.bn(), true)
85            .map_err(|e| ScriptError::InvalidSignature(format!("ECDSA sign failed: {}", e)))?;
86
87        let mut sig_bytes = sig.to_der();
88        sig_bytes.push(self.sighash_type as u8);
89
90        let chunks = vec![ScriptChunk::new_raw(sig_bytes.len() as u8, Some(sig_bytes))];
91
92        Ok(UnlockingScript::from_script(Script::from_chunks(chunks)))
93    }
94
95    /// Estimate the byte length of the unlocking script.
96    pub fn estimate_unlock_length(&self) -> usize {
97        74
98    }
99
100    /// Decode a PushDrop locking script, recovering the embedded data fields.
101    ///
102    /// Supports both lock positions:
103    /// - **Before**: `<pubkey> OP_CHECKSIG <fields...> [<sig>] OP_2DROP...`
104    /// - **After**: `<fields...> OP_2DROP... <pubkey> OP_CHECKSIG`
105    ///
106    /// Defaults to `Before` (matching TS SDK default).
107    pub fn decode(script: &LockingScript) -> Result<PushDrop, ScriptError> {
108        Self::decode_with_position(script, LockPosition::Before)
109    }
110
111    /// Decode with explicit lock position.
112    pub fn decode_with_position(
113        script: &LockingScript,
114        position: LockPosition,
115    ) -> Result<PushDrop, ScriptError> {
116        let chunks = script.chunks();
117        if chunks.len() < 3 {
118            return Err(ScriptError::InvalidScript(
119                "PushDrop::decode: script too short".into(),
120            ));
121        }
122
123        let fields = match position {
124            LockPosition::Before => Self::decode_before(chunks)?,
125            LockPosition::After => Self::decode_after(chunks)?,
126        };
127
128        Ok(PushDrop {
129            fields,
130            private_key: None,
131            sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
132            lock_position: position,
133        })
134    }
135
136    /// Decode "before" layout: `<pubkey> OP_CHECKSIG <fields...> OP_2DROP...`
137    ///
138    /// Matches TS SDK `PushDrop.decode(script, 'before')`:
139    /// - Skip chunks[0] (pubkey) and chunks[1] (OP_CHECKSIG)
140    /// - Read data pushes from index 2 until next chunk is OP_DROP/OP_2DROP
141    fn decode_before(chunks: &[ScriptChunk]) -> Result<Vec<Vec<u8>>, ScriptError> {
142        if chunks.len() < 2 || chunks[0].data.is_none() || chunks[1].op != Op::OpCheckSig {
143            return Err(ScriptError::InvalidScript(
144                "PushDrop::decode(before): expected <pubkey> OP_CHECKSIG at start".into(),
145            ));
146        }
147
148        let mut fields = Vec::new();
149        for i in 2..chunks.len() {
150            // Check if next chunk is a DROP — if so, this is the last field
151            let next_is_drop = chunks
152                .get(i + 1)
153                .is_some_and(|next| next.op == Op::OpDrop || next.op == Op::Op2Drop);
154
155            // Stop if THIS chunk is a DROP
156            if chunks[i].op == Op::OpDrop || chunks[i].op == Op::Op2Drop {
157                break;
158            }
159
160            if let Some(ref data) = chunks[i].data {
161                fields.push(data.clone());
162            }
163
164            if next_is_drop {
165                break;
166            }
167        }
168
169        Ok(fields)
170    }
171
172    /// Decode "after" layout: `<fields...> OP_2DROP... <pubkey> OP_CHECKSIG`
173    fn decode_after(chunks: &[ScriptChunk]) -> Result<Vec<Vec<u8>>, ScriptError> {
174        let last = &chunks[chunks.len() - 1];
175        if last.op != Op::OpCheckSig {
176            return Err(ScriptError::InvalidScript(
177                "PushDrop::decode(after): last opcode must be OP_CHECKSIG".into(),
178            ));
179        }
180
181        if chunks[chunks.len() - 2].data.is_none() {
182            return Err(ScriptError::InvalidScript(
183                "PushDrop::decode(after): expected pubkey before OP_CHECKSIG".into(),
184            ));
185        }
186
187        // Walk backwards from before the pubkey to count DROP opcodes
188        let mut drop_field_count = 0usize;
189        let mut pos = chunks.len() - 3;
190        loop {
191            let chunk = &chunks[pos];
192            if chunk.op == Op::Op2Drop {
193                drop_field_count += 2;
194            } else if chunk.op == Op::OpDrop {
195                drop_field_count += 1;
196            } else {
197                break;
198            }
199            if pos == 0 {
200                break;
201            }
202            pos -= 1;
203        }
204
205        if drop_field_count == 0 {
206            return Err(ScriptError::InvalidScript(
207                "PushDrop::decode(after): no OP_DROP/OP_2DROP found".into(),
208            ));
209        }
210
211        let mut fields = Vec::with_capacity(drop_field_count);
212        for chunk in &chunks[0..drop_field_count] {
213            let data = chunk.data.as_ref().ok_or_else(|| {
214                ScriptError::InvalidScript(
215                    "PushDrop::decode(after): expected data push for field".into(),
216                )
217            })?;
218            fields.push(data.clone());
219        }
220
221        Ok(fields)
222    }
223
224    /// Create a data push chunk with appropriate opcode for the data length.
225    fn make_data_push(data: &[u8]) -> ScriptChunk {
226        let len = data.len();
227        if len < 0x4c {
228            ScriptChunk::new_raw(len as u8, Some(data.to_vec()))
229        } else if len < 256 {
230            ScriptChunk::new_raw(Op::OpPushData1.to_byte(), Some(data.to_vec()))
231        } else if len < 65536 {
232            ScriptChunk::new_raw(Op::OpPushData2.to_byte(), Some(data.to_vec()))
233        } else {
234            ScriptChunk::new_raw(Op::OpPushData4.to_byte(), Some(data.to_vec()))
235        }
236    }
237}
238
239impl ScriptTemplateLock for PushDrop {
240    /// Create a PushDrop locking script using the configured lock position.
241    fn lock(&self) -> Result<LockingScript, ScriptError> {
242        let key = self.private_key.as_ref().ok_or_else(|| {
243            ScriptError::InvalidScript(
244                "PushDrop: need private key to derive pubkey for lock".into(),
245            )
246        })?;
247
248        if self.fields.is_empty() {
249            return Err(ScriptError::InvalidScript(
250                "PushDrop: at least one data field required".into(),
251            ));
252        }
253
254        let pubkey = key.to_public_key();
255        let pubkey_bytes = pubkey.to_der();
256
257        let mut lock_chunks = vec![
258            ScriptChunk::new_raw(pubkey_bytes.len() as u8, Some(pubkey_bytes)),
259            ScriptChunk::new_opcode(Op::OpCheckSig),
260        ];
261
262        let mut field_chunks = Vec::new();
263        for field in &self.fields {
264            field_chunks.push(Self::make_data_push(field));
265        }
266
267        let num_fields = self.fields.len();
268        for _ in 0..num_fields / 2 {
269            field_chunks.push(ScriptChunk::new_opcode(Op::Op2Drop));
270        }
271        for _ in 0..num_fields % 2 {
272            field_chunks.push(ScriptChunk::new_opcode(Op::OpDrop));
273        }
274
275        let chunks = match self.lock_position {
276            LockPosition::Before => {
277                lock_chunks.extend(field_chunks);
278                lock_chunks
279            }
280            LockPosition::After => {
281                field_chunks.extend(lock_chunks);
282                field_chunks
283            }
284        };
285
286        Ok(LockingScript::from_script(Script::from_chunks(chunks)))
287    }
288}
289
290impl ScriptTemplateUnlock for PushDrop {
291    fn sign(&self, preimage: &[u8]) -> Result<UnlockingScript, ScriptError> {
292        self.unlock(preimage)
293    }
294
295    fn estimate_length(&self) -> Result<usize, ScriptError> {
296        Ok(self.estimate_unlock_length())
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    // -----------------------------------------------------------------------
305    // Lock position: Before (default, matches TS SDK)
306    // -----------------------------------------------------------------------
307
308    #[test]
309    fn test_before_lock_one_field() {
310        let key = PrivateKey::from_hex("1").unwrap();
311        let data = vec![0xca, 0xfe, 0xba, 0xbe];
312        let pd = PushDrop::new(vec![data.clone()], key);
313        let lock_script = pd.lock().unwrap();
314        let chunks = lock_script.chunks();
315
316        // <pubkey> OP_CHECKSIG <data> OP_DROP = 4 chunks
317        assert_eq!(chunks.len(), 4);
318        assert_eq!(chunks[0].data.as_ref().unwrap().len(), 33); // pubkey
319        assert_eq!(chunks[1].op, Op::OpCheckSig);
320        assert_eq!(chunks[2].data.as_ref().unwrap(), &data);
321        assert_eq!(chunks[3].op, Op::OpDrop);
322    }
323
324    #[test]
325    fn test_before_lock_three_fields() {
326        let key = PrivateKey::from_hex("1").unwrap();
327        let fields = vec![vec![0x01], vec![0x02], vec![0x03]];
328        let pd = PushDrop::new(fields.clone(), key);
329        let lock_script = pd.lock().unwrap();
330        let chunks = lock_script.chunks();
331
332        // <pubkey> OP_CHECKSIG <f0> <f1> <f2> OP_2DROP OP_DROP = 7
333        assert_eq!(chunks.len(), 7);
334        assert_eq!(chunks[0].data.as_ref().unwrap().len(), 33);
335        assert_eq!(chunks[1].op, Op::OpCheckSig);
336        assert_eq!(chunks[2].data.as_ref().unwrap(), &fields[0]);
337        assert_eq!(chunks[3].data.as_ref().unwrap(), &fields[1]);
338        assert_eq!(chunks[4].data.as_ref().unwrap(), &fields[2]);
339        assert_eq!(chunks[5].op, Op::Op2Drop);
340        assert_eq!(chunks[6].op, Op::OpDrop);
341    }
342
343    #[test]
344    fn test_before_decode_roundtrip() {
345        let key = PrivateKey::from_hex("1").unwrap();
346        let fields = vec![
347            b"SLAP".to_vec(),
348            vec![0x02, 0x79],
349            b"https://example.com".to_vec(),
350            b"ls_ship".to_vec(),
351        ];
352        let pd = PushDrop::new(fields.clone(), key);
353        let lock_script = pd.lock().unwrap();
354
355        let decoded = PushDrop::decode(&lock_script).unwrap();
356        assert_eq!(decoded.fields, fields);
357        assert_eq!(decoded.lock_position, LockPosition::Before);
358    }
359
360    // -----------------------------------------------------------------------
361    // Lock position: After (legacy)
362    // -----------------------------------------------------------------------
363
364    #[test]
365    fn test_after_lock_one_field() {
366        let key = PrivateKey::from_hex("1").unwrap();
367        let data = vec![0xca, 0xfe];
368        let pd = PushDrop::new(vec![data.clone()], key).with_lock_position(LockPosition::After);
369        let lock_script = pd.lock().unwrap();
370        let chunks = lock_script.chunks();
371
372        // <data> OP_DROP <pubkey> OP_CHECKSIG = 4
373        assert_eq!(chunks.len(), 4);
374        assert_eq!(chunks[0].data.as_ref().unwrap(), &data);
375        assert_eq!(chunks[1].op, Op::OpDrop);
376        assert_eq!(chunks[2].data.as_ref().unwrap().len(), 33);
377        assert_eq!(chunks[3].op, Op::OpCheckSig);
378    }
379
380    #[test]
381    fn test_after_decode_roundtrip() {
382        let key = PrivateKey::from_hex("1").unwrap();
383        let fields = vec![vec![0x01, 0x02], vec![0x03, 0x04]];
384        let pd = PushDrop::new(fields.clone(), key).with_lock_position(LockPosition::After);
385        let lock_script = pd.lock().unwrap();
386
387        let decoded = PushDrop::decode_with_position(&lock_script, LockPosition::After).unwrap();
388        assert_eq!(decoded.fields, fields);
389    }
390
391    // -----------------------------------------------------------------------
392    // Unlock + error cases
393    // -----------------------------------------------------------------------
394
395    #[test]
396    fn test_unlock_produces_signature() {
397        let key = PrivateKey::from_hex("1").unwrap();
398        let pd = PushDrop::new(vec![vec![0xaa]], key);
399        let unlock_script = pd.unlock(b"test preimage").unwrap();
400        assert_eq!(unlock_script.chunks().len(), 1);
401        let sig_data = unlock_script.chunks()[0].data.as_ref().unwrap();
402        assert!(sig_data.len() >= 70 && sig_data.len() <= 74);
403    }
404
405    #[test]
406    fn test_lock_no_key_errors() {
407        let pd = PushDrop::lock_only(vec![vec![0x01]]);
408        assert!(pd.lock().is_err());
409    }
410
411    #[test]
412    fn test_lock_no_fields_errors() {
413        let key = PrivateKey::from_hex("1").unwrap();
414        let pd = PushDrop::new(vec![], key);
415        assert!(pd.lock().is_err());
416    }
417
418    #[test]
419    fn test_decode_non_pushdrop_errors() {
420        let script = LockingScript::from_binary(&[0x76, 0xa9, 0x14]);
421        assert!(PushDrop::decode(&script).is_err());
422    }
423}