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    /// Create a data push chunk with appropriate opcode for the data length.
89    fn make_data_push(data: &[u8]) -> ScriptChunk {
90        let len = data.len();
91        if len < 0x4c {
92            // Direct push: opcode IS the length
93            ScriptChunk::new_raw(len as u8, Some(data.to_vec()))
94        } else if len < 256 {
95            ScriptChunk::new_raw(Op::OpPushData1.to_byte(), Some(data.to_vec()))
96        } else if len < 65536 {
97            ScriptChunk::new_raw(Op::OpPushData2.to_byte(), Some(data.to_vec()))
98        } else {
99            ScriptChunk::new_raw(Op::OpPushData4.to_byte(), Some(data.to_vec()))
100        }
101    }
102}
103
104impl ScriptTemplateLock for PushDrop {
105    /// Create a PushDrop locking script.
106    ///
107    /// Structure: `<field_1> <field_2> ... <field_N> OP_DROP|OP_2DROP... <pubkey> OP_CHECKSIG`
108    ///
109    /// Each field is pushed as data, then removed with OP_DROP (or OP_2DROP
110    /// for pairs). The final element on stack will be verified against the
111    /// embedded public key via OP_CHECKSIG.
112    fn lock(&self) -> Result<LockingScript, ScriptError> {
113        let key = self.private_key.as_ref().ok_or_else(|| {
114            ScriptError::InvalidScript(
115                "PushDrop: need private key to derive pubkey for lock".into(),
116            )
117        })?;
118
119        if self.fields.is_empty() {
120            return Err(ScriptError::InvalidScript(
121                "PushDrop: at least one data field required".into(),
122            ));
123        }
124
125        let mut chunks = Vec::new();
126
127        // Push each data field
128        for field in &self.fields {
129            chunks.push(Self::make_data_push(field));
130        }
131
132        // Add OP_DROP for each field to clean the stack
133        // Use OP_2DROP where possible for efficiency
134        let num_fields = self.fields.len();
135        let num_2drops = num_fields / 2;
136        let num_drops = num_fields % 2;
137
138        for _ in 0..num_2drops {
139            chunks.push(ScriptChunk::new_opcode(Op::Op2Drop));
140        }
141        for _ in 0..num_drops {
142            chunks.push(ScriptChunk::new_opcode(Op::OpDrop));
143        }
144
145        // Add <pubkey> OP_CHECKSIG
146        let pubkey = key.to_public_key();
147        let pubkey_bytes = pubkey.to_der();
148        chunks.push(ScriptChunk::new_raw(
149            pubkey_bytes.len() as u8,
150            Some(pubkey_bytes),
151        ));
152        chunks.push(ScriptChunk::new_opcode(Op::OpCheckSig));
153
154        Ok(LockingScript::from_script(Script::from_chunks(chunks)))
155    }
156}
157
158impl ScriptTemplateUnlock for PushDrop {
159    fn sign(&self, preimage: &[u8]) -> Result<UnlockingScript, ScriptError> {
160        self.unlock(preimage)
161    }
162
163    fn estimate_length(&self) -> Result<usize, ScriptError> {
164        Ok(self.estimate_unlock_length())
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    // -----------------------------------------------------------------------
173    // PushDrop lock: 1 field produces script with data and OP_DROP
174    // -----------------------------------------------------------------------
175
176    #[test]
177    fn test_pushdrop_lock_one_field() {
178        let key = PrivateKey::from_hex("1").unwrap();
179        let data = vec![0xca, 0xfe, 0xba, 0xbe];
180        let pd = PushDrop::new(vec![data.clone()], key);
181
182        let lock_script = pd.lock().unwrap();
183        let chunks = lock_script.chunks();
184
185        // Should have: <data> OP_DROP <pubkey> OP_CHECKSIG = 4 chunks
186        assert_eq!(chunks.len(), 4, "1-field PushDrop should have 4 chunks");
187
188        // First chunk: data push
189        assert_eq!(chunks[0].data.as_ref().unwrap(), &data);
190        // Second: OP_DROP
191        assert_eq!(chunks[1].op, Op::OpDrop);
192        // Third: pubkey (33 bytes)
193        assert_eq!(chunks[2].data.as_ref().unwrap().len(), 33);
194        // Fourth: OP_CHECKSIG
195        assert_eq!(chunks[3].op, Op::OpCheckSig);
196    }
197
198    // -----------------------------------------------------------------------
199    // PushDrop lock: multiple fields includes all data
200    // -----------------------------------------------------------------------
201
202    #[test]
203    fn test_pushdrop_lock_multiple_fields() {
204        let key = PrivateKey::from_hex("1").unwrap();
205        let fields = vec![vec![0x01, 0x02], vec![0x03, 0x04], vec![0x05, 0x06]];
206        let pd = PushDrop::new(fields.clone(), key);
207
208        let lock_script = pd.lock().unwrap();
209        let chunks = lock_script.chunks();
210
211        // 3 data pushes + 1 OP_2DROP + 1 OP_DROP + 1 pubkey + 1 OP_CHECKSIG = 7
212        assert_eq!(chunks.len(), 7, "3-field PushDrop should have 7 chunks");
213
214        // Verify data fields are present
215        assert_eq!(chunks[0].data.as_ref().unwrap(), &fields[0]);
216        assert_eq!(chunks[1].data.as_ref().unwrap(), &fields[1]);
217        assert_eq!(chunks[2].data.as_ref().unwrap(), &fields[2]);
218
219        // OP_2DROP for first pair, OP_DROP for odd one
220        assert_eq!(chunks[3].op, Op::Op2Drop);
221        assert_eq!(chunks[4].op, Op::OpDrop);
222
223        // Pubkey + checksig
224        assert_eq!(chunks[6].op, Op::OpCheckSig);
225    }
226
227    // -----------------------------------------------------------------------
228    // PushDrop lock: even number of fields uses OP_2DROP efficiently
229    // -----------------------------------------------------------------------
230
231    #[test]
232    fn test_pushdrop_lock_even_fields() {
233        let key = PrivateKey::from_hex("1").unwrap();
234        let fields = vec![vec![0x01], vec![0x02]];
235        let pd = PushDrop::new(fields, key);
236
237        let lock_script = pd.lock().unwrap();
238        let chunks = lock_script.chunks();
239
240        // 2 data pushes + 1 OP_2DROP + 1 pubkey + 1 OP_CHECKSIG = 5 chunks
241        assert_eq!(chunks.len(), 5);
242        assert_eq!(chunks[2].op, Op::Op2Drop);
243    }
244
245    // -----------------------------------------------------------------------
246    // PushDrop unlock: produces valid signature
247    // -----------------------------------------------------------------------
248
249    #[test]
250    fn test_pushdrop_unlock_produces_signature() {
251        let key = PrivateKey::from_hex("1").unwrap();
252        let pd = PushDrop::new(vec![vec![0xaa]], key);
253
254        let unlock_script = pd.unlock(b"test preimage").unwrap();
255        assert_eq!(
256            unlock_script.chunks().len(),
257            1,
258            "PushDrop unlock should be 1 chunk (just sig)"
259        );
260
261        let sig_data = unlock_script.chunks()[0].data.as_ref().unwrap();
262        // DER signature is typically 70-73 bytes + 1 sighash byte
263        assert!(sig_data.len() >= 70 && sig_data.len() <= 74);
264        // Last byte is sighash
265        assert_eq!(
266            *sig_data.last().unwrap(),
267            (SIGHASH_ALL | SIGHASH_FORKID) as u8
268        );
269    }
270
271    // -----------------------------------------------------------------------
272    // PushDrop: estimate length
273    // -----------------------------------------------------------------------
274
275    #[test]
276    fn test_pushdrop_estimate_length() {
277        let key = PrivateKey::from_hex("1").unwrap();
278        let pd = PushDrop::new(vec![vec![0x01]], key);
279        assert_eq!(pd.estimate_unlock_length(), 74);
280    }
281
282    // -----------------------------------------------------------------------
283    // PushDrop: error cases
284    // -----------------------------------------------------------------------
285
286    #[test]
287    fn test_pushdrop_lock_no_key() {
288        let pd = PushDrop::lock_only(vec![vec![0x01]]);
289        assert!(pd.lock().is_err());
290    }
291
292    #[test]
293    fn test_pushdrop_lock_no_fields() {
294        let key = PrivateKey::from_hex("1").unwrap();
295        let pd = PushDrop::new(vec![], key);
296        assert!(pd.lock().is_err());
297    }
298
299    #[test]
300    fn test_pushdrop_unlock_no_key() {
301        let pd = PushDrop::lock_only(vec![vec![0x01]]);
302        assert!(pd.unlock(b"test").is_err());
303    }
304
305    // -----------------------------------------------------------------------
306    // PushDrop: trait implementations
307    // -----------------------------------------------------------------------
308
309    #[test]
310    fn test_pushdrop_trait_sign() {
311        let key = PrivateKey::from_hex("ff").unwrap();
312        let pd = PushDrop::new(vec![vec![0x01, 0x02, 0x03]], key);
313        let unlock_script = pd.sign(b"sighash data").unwrap();
314        assert_eq!(unlock_script.chunks().len(), 1);
315    }
316
317    // -----------------------------------------------------------------------
318    // PushDrop: binary roundtrip
319    // -----------------------------------------------------------------------
320
321    #[test]
322    fn test_pushdrop_lock_binary_roundtrip() {
323        let key = PrivateKey::from_hex("1").unwrap();
324        let pd = PushDrop::new(vec![vec![0xde, 0xad]], key);
325
326        let lock_script = pd.lock().unwrap();
327        let binary = lock_script.to_binary();
328
329        // Re-parse and verify
330        let reparsed = Script::from_binary(&binary);
331        assert_eq!(
332            reparsed.to_binary(),
333            binary,
334            "binary roundtrip should match"
335        );
336    }
337}