Skip to main content

bsv/script/templates/
r_puzzle.rs

1//! RPuzzle script template for R-value puzzle scripts.
2//!
3//! RPuzzle creates scripts that extract the R-value from a DER-encoded
4//! signature and compare it (optionally hashed) against an expected value.
5//! This enables knowledge-of-k-value based script puzzles.
6//! Translates the TS SDK RPuzzle.ts.
7
8use crate::primitives::big_number::BigNumber;
9use crate::primitives::ecdsa::ecdsa_sign_with_k;
10use crate::primitives::hash::sha256;
11use crate::primitives::private_key::PrivateKey;
12use crate::primitives::transaction_signature::{SIGHASH_ALL, SIGHASH_FORKID};
13use crate::script::error::ScriptError;
14use crate::script::locking_script::LockingScript;
15use crate::script::op::Op;
16use crate::script::script::Script;
17use crate::script::script_chunk::ScriptChunk;
18use crate::script::templates::{ScriptTemplateLock, ScriptTemplateUnlock};
19use crate::script::unlocking_script::UnlockingScript;
20
21/// The type of hash applied to the R-value before comparison.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum RPuzzleType {
24    /// Compare R-value directly (no hashing).
25    Raw,
26    /// Apply SHA-1 before comparison.
27    SHA1,
28    /// Apply SHA-256 before comparison.
29    SHA256,
30    /// Apply double SHA-256 (Hash256) before comparison.
31    Hash256,
32    /// Apply RIPEMD-160 before comparison.
33    RIPEMD160,
34    /// Apply Hash160 (RIPEMD160(SHA256)) before comparison.
35    Hash160,
36}
37
38/// RPuzzle script template for R-value puzzle scripts.
39///
40/// Creates a locking script that extracts the R-value from a DER signature
41/// on the stack, optionally hashes it, and compares against an expected value.
42/// Unlocking requires knowledge of the k-value used to produce a matching R.
43#[derive(Clone, Debug)]
44pub struct RPuzzle {
45    /// The type of hash to apply to the extracted R-value.
46    pub puzzle_type: RPuzzleType,
47    /// The expected R-value or hash of R-value for locking.
48    pub value: Vec<u8>,
49    /// The known k-value for unlocking (allows computing matching R).
50    pub k_value: Option<BigNumber>,
51    /// Private key for signing with known k.
52    pub private_key: Option<PrivateKey>,
53    /// Sighash scope for signing.
54    pub sighash_type: u32,
55}
56
57impl RPuzzle {
58    /// Create an RPuzzle template configured for locking.
59    ///
60    /// The `value` is the expected R-value (or hash of R-value, depending on
61    /// `puzzle_type`) that must be matched by the unlocking signature.
62    pub fn from_value(puzzle_type: RPuzzleType, value: Vec<u8>) -> Self {
63        RPuzzle {
64            puzzle_type,
65            value,
66            k_value: None,
67            private_key: None,
68            sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
69        }
70    }
71
72    /// Create an RPuzzle template configured for unlocking.
73    ///
74    /// The `k` value is the nonce that will produce the expected R-value.
75    /// The `key` is the private key used for signing.
76    pub fn from_k(puzzle_type: RPuzzleType, value: Vec<u8>, k: BigNumber, key: PrivateKey) -> Self {
77        RPuzzle {
78            puzzle_type,
79            value,
80            k_value: Some(k),
81            private_key: Some(key),
82            sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
83        }
84    }
85
86    /// Create an unlocking script from a sighash preimage.
87    ///
88    /// Signs with the known k-value to produce a signature whose R-value
89    /// matches the puzzle's expected value.
90    pub fn unlock(&self, preimage: &[u8]) -> Result<UnlockingScript, ScriptError> {
91        let key = self.private_key.as_ref().ok_or_else(|| {
92            ScriptError::InvalidScript("RPuzzle: no private key for unlock".into())
93        })?;
94        let k = self
95            .k_value
96            .as_ref()
97            .ok_or_else(|| ScriptError::InvalidScript("RPuzzle: no k-value for unlock".into()))?;
98
99        let msg_hash = sha256(preimage);
100        let sig = ecdsa_sign_with_k(&msg_hash, key.bn(), k, true)
101            .map_err(|e| ScriptError::InvalidSignature(format!("ECDSA sign with k failed: {e}")))?;
102
103        let mut sig_bytes = sig.to_der();
104        sig_bytes.push(self.sighash_type as u8);
105
106        let chunks = vec![ScriptChunk::new_raw(sig_bytes.len() as u8, Some(sig_bytes))];
107
108        Ok(UnlockingScript::from_script(Script::from_chunks(chunks)))
109    }
110
111    /// Estimate the byte length of the unlocking script.
112    ///
113    /// RPuzzle unlock is just a signature: approximately 74 bytes.
114    pub fn estimate_unlock_length(&self) -> usize {
115        74
116    }
117
118    /// Build the R-value extraction opcodes from a DER signature on the stack.
119    ///
120    /// DER sig format: 0x30 <total_len> 0x02 <r_len> <r_bytes> 0x02 <s_len> <s_bytes>
121    /// The extraction opcodes split the signature to isolate the R bytes:
122    ///   OP_DUP OP_3 OP_SPLIT OP_NIP OP_1 OP_SPLIT OP_SWAP OP_SPLIT OP_DROP
123    fn r_extraction_chunks() -> Vec<ScriptChunk> {
124        vec![
125            ScriptChunk::new_opcode(Op::OpDup),   // dup the sig
126            ScriptChunk::new_opcode(Op::Op3),     // push 3
127            ScriptChunk::new_opcode(Op::OpSplit), // split at byte 3 -> [first3] [rest]
128            ScriptChunk::new_opcode(Op::OpNip),   // remove first3 -> [rest] (r_len|r|02|s...)
129            ScriptChunk::new_opcode(Op::Op1),     // push 1
130            ScriptChunk::new_opcode(Op::OpSplit), // split at 1 -> [r_len_byte] [r|02|s...]
131            ScriptChunk::new_opcode(Op::OpSwap),  // swap -> [r|02|s...] [r_len_byte]
132            ScriptChunk::new_opcode(Op::OpSplit), // split at r_len -> [r_bytes] [02|s...]
133            ScriptChunk::new_opcode(Op::OpDrop),  // drop the s part -> [r_bytes]
134        ]
135    }
136
137    /// Get the hash opcode for the puzzle type (if any).
138    fn hash_opcode(&self) -> Option<Op> {
139        match self.puzzle_type {
140            RPuzzleType::Raw => None,
141            RPuzzleType::SHA1 => Some(Op::OpSha1),
142            RPuzzleType::SHA256 => Some(Op::OpSha256),
143            RPuzzleType::Hash256 => Some(Op::OpHash256),
144            RPuzzleType::RIPEMD160 => Some(Op::OpRipemd160),
145            RPuzzleType::Hash160 => Some(Op::OpHash160),
146        }
147    }
148}
149
150impl ScriptTemplateLock for RPuzzle {
151    /// Create an RPuzzle locking script.
152    ///
153    /// Structure:
154    ///   OP_DUP OP_3 OP_SPLIT OP_NIP OP_1 OP_SPLIT OP_SWAP OP_SPLIT OP_DROP
155    ///   `[OP_hash]`  (only if not Raw)
156    ///   <expected_value> OP_EQUALVERIFY OP_CHECKSIG
157    fn lock(&self) -> Result<LockingScript, ScriptError> {
158        if self.value.is_empty() {
159            return Err(ScriptError::InvalidScript(
160                "RPuzzle: value must not be empty".into(),
161            ));
162        }
163
164        let mut chunks = Self::r_extraction_chunks();
165
166        // Add hash opcode if needed
167        if let Some(hash_op) = self.hash_opcode() {
168            chunks.push(ScriptChunk::new_opcode(hash_op));
169        }
170
171        // Push expected value
172        let val_len = self.value.len();
173        if val_len < 0x4c {
174            chunks.push(ScriptChunk::new_raw(
175                val_len as u8,
176                Some(self.value.clone()),
177            ));
178        } else {
179            chunks.push(ScriptChunk::new_raw(
180                Op::OpPushData1.to_byte(),
181                Some(self.value.clone()),
182            ));
183        }
184
185        chunks.push(ScriptChunk::new_opcode(Op::OpEqualVerify));
186        chunks.push(ScriptChunk::new_opcode(Op::OpCheckSig));
187
188        Ok(LockingScript::from_script(Script::from_chunks(chunks)))
189    }
190}
191
192impl ScriptTemplateUnlock for RPuzzle {
193    fn sign(&self, preimage: &[u8]) -> Result<UnlockingScript, ScriptError> {
194        self.unlock(preimage)
195    }
196
197    fn estimate_length(&self) -> Result<usize, ScriptError> {
198        Ok(self.estimate_unlock_length())
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use crate::primitives::base_point::BasePoint;
206    use crate::primitives::big_number::Endian;
207    use crate::primitives::hash::sha256;
208
209    fn bytes_to_hex(bytes: &[u8]) -> String {
210        bytes.iter().map(|b| format!("{b:02x}")).collect()
211    }
212
213    // -----------------------------------------------------------------------
214    // RPuzzle lock: Raw type produces correct extraction opcodes
215    // -----------------------------------------------------------------------
216
217    #[test]
218    fn test_rpuzzle_lock_raw() {
219        let value = vec![0xaa; 32];
220        let rp = RPuzzle::from_value(RPuzzleType::Raw, value.clone());
221
222        let lock_script = rp.lock().unwrap();
223        let chunks = lock_script.chunks();
224
225        // 9 extraction opcodes + <value> + OP_EQUALVERIFY + OP_CHECKSIG = 12
226        assert_eq!(chunks.len(), 12, "Raw RPuzzle should have 12 chunks");
227
228        // Verify extraction opcodes
229        assert_eq!(chunks[0].op, Op::OpDup);
230        assert_eq!(chunks[1].op, Op::Op3);
231        assert_eq!(chunks[2].op, Op::OpSplit);
232        assert_eq!(chunks[3].op, Op::OpNip);
233        assert_eq!(chunks[4].op, Op::Op1);
234        assert_eq!(chunks[5].op, Op::OpSplit);
235        assert_eq!(chunks[6].op, Op::OpSwap);
236        assert_eq!(chunks[7].op, Op::OpSplit);
237        assert_eq!(chunks[8].op, Op::OpDrop);
238
239        // No hash opcode for Raw, directly the value
240        assert_eq!(chunks[9].data.as_ref().unwrap(), &value);
241        assert_eq!(chunks[10].op, Op::OpEqualVerify);
242        assert_eq!(chunks[11].op, Op::OpCheckSig);
243    }
244
245    // -----------------------------------------------------------------------
246    // RPuzzle lock: SHA256 type includes OP_SHA256 before comparison
247    // -----------------------------------------------------------------------
248
249    #[test]
250    fn test_rpuzzle_lock_sha256() {
251        let value = vec![0xbb; 32];
252        let rp = RPuzzle::from_value(RPuzzleType::SHA256, value.clone());
253
254        let lock_script = rp.lock().unwrap();
255        let chunks = lock_script.chunks();
256
257        // 9 extraction + OP_SHA256 + <value> + OP_EQUALVERIFY + OP_CHECKSIG = 13
258        assert_eq!(chunks.len(), 13, "SHA256 RPuzzle should have 13 chunks");
259
260        // Check OP_SHA256 is present after extraction
261        assert_eq!(chunks[9].op, Op::OpSha256);
262        // Then value
263        assert_eq!(chunks[10].data.as_ref().unwrap(), &value);
264    }
265
266    // -----------------------------------------------------------------------
267    // RPuzzle lock: other hash types
268    // -----------------------------------------------------------------------
269
270    #[test]
271    fn test_rpuzzle_lock_hash_types() {
272        let value = vec![0xcc; 20];
273
274        let test_cases = vec![
275            (RPuzzleType::SHA1, Op::OpSha1, 13),
276            (RPuzzleType::Hash256, Op::OpHash256, 13),
277            (RPuzzleType::RIPEMD160, Op::OpRipemd160, 13),
278            (RPuzzleType::Hash160, Op::OpHash160, 13),
279        ];
280
281        for (ptype, expected_op, expected_chunks) in test_cases {
282            let rp = RPuzzle::from_value(ptype, value.clone());
283            let lock_script = rp.lock().unwrap();
284            let chunks = lock_script.chunks();
285
286            assert_eq!(
287                chunks.len(),
288                expected_chunks,
289                "{ptype:?} should have {expected_chunks} chunks"
290            );
291            assert_eq!(chunks[9].op, expected_op, "{ptype:?} hash opcode mismatch");
292        }
293    }
294
295    // -----------------------------------------------------------------------
296    // RPuzzle: sign with known k produces valid signature
297    // -----------------------------------------------------------------------
298
299    #[test]
300    fn test_rpuzzle_unlock_with_k() {
301        let key = PrivateKey::from_hex("1").unwrap();
302        let k = BigNumber::from_number(42);
303
304        // Compute the R-value that this k produces: R = k * G
305        let base_point = BasePoint::instance();
306        let r_point = base_point.mul(&k);
307        let r_bytes = r_point.get_x().to_array(Endian::Big, Some(32));
308
309        // Use raw R-value as the puzzle value
310        let rp = RPuzzle::from_k(RPuzzleType::Raw, r_bytes, k, key);
311
312        let unlock_script = rp.unlock(b"test preimage").unwrap();
313        assert_eq!(unlock_script.chunks().len(), 1);
314
315        let sig_data = unlock_script.chunks()[0].data.as_ref().unwrap();
316        assert!(sig_data.len() >= 70 && sig_data.len() <= 74);
317    }
318
319    // -----------------------------------------------------------------------
320    // RPuzzle: round-trip verification
321    // -----------------------------------------------------------------------
322
323    #[test]
324    fn test_rpuzzle_roundtrip_raw() {
325        let key = PrivateKey::from_hex("ff").unwrap();
326        let k = BigNumber::from_number(12345);
327
328        // Compute R-value from k
329        let base_point = BasePoint::instance();
330        let r_point = base_point.mul(&k);
331        let r_value = r_point.get_x().to_array(Endian::Big, Some(32));
332
333        // Create puzzle with the raw R-value
334        let rp = RPuzzle::from_k(RPuzzleType::Raw, r_value.clone(), k.clone(), key.clone());
335
336        // Lock should contain the R-value
337        let lock_script = rp.lock().unwrap();
338        let lock_chunks = lock_script.chunks();
339
340        // The value chunk (after extraction opcodes, index 9 for Raw)
341        let embedded_value = lock_chunks[9].data.as_ref().unwrap();
342        assert_eq!(
343            embedded_value, &r_value,
344            "embedded value should match R-value"
345        );
346
347        // Unlock should produce a valid signature
348        let unlock_script = rp.unlock(b"test roundtrip").unwrap();
349        assert_eq!(unlock_script.chunks().len(), 1);
350
351        // Extract R from the produced signature's DER encoding
352        let sig_with_sighash = unlock_script.chunks()[0].data.as_ref().unwrap();
353        let sig_der = &sig_with_sighash[..sig_with_sighash.len() - 1]; // strip sighash byte
354
355        // Parse DER to extract R
356        // DER: 0x30 <len> 0x02 <r_len> <r_bytes> ...
357        assert_eq!(sig_der[0], 0x30);
358        assert_eq!(sig_der[2], 0x02);
359        let r_len = sig_der[3] as usize;
360        let r_bytes = &sig_der[4..4 + r_len];
361
362        // Strip leading zero if present (DER positive encoding)
363        let r_trimmed = if !r_bytes.is_empty() && r_bytes[0] == 0x00 {
364            &r_bytes[1..]
365        } else {
366            r_bytes
367        };
368
369        // Pad to 32 bytes for comparison
370        let mut r_padded = vec![0u8; 32];
371        let start = 32 - r_trimmed.len();
372        r_padded[start..].copy_from_slice(r_trimmed);
373
374        assert_eq!(
375            r_padded, r_value,
376            "signature R-value should match the puzzle value"
377        );
378    }
379
380    // -----------------------------------------------------------------------
381    // RPuzzle: round-trip with SHA256 hash
382    // -----------------------------------------------------------------------
383
384    #[test]
385    fn test_rpuzzle_roundtrip_sha256() {
386        let key = PrivateKey::from_hex("abcd").unwrap();
387        let k = BigNumber::from_number(9999);
388
389        // Compute R-value from k
390        let base_point = BasePoint::instance();
391        let r_point = base_point.mul(&k);
392        let r_value = r_point.get_x().to_array(Endian::Big, Some(32));
393
394        // Hash the R-value with SHA256
395        let r_hash = sha256(&r_value);
396
397        // Create puzzle with SHA256 hash of R-value
398        let rp = RPuzzle::from_k(RPuzzleType::SHA256, r_hash.to_vec(), k, key);
399
400        // Lock should contain the SHA256 hash
401        let lock_script = rp.lock().unwrap();
402        let lock_chunks = lock_script.chunks();
403
404        // After extraction (9) + OP_SHA256 (1) = index 10
405        let embedded_hash = lock_chunks[10].data.as_ref().unwrap();
406        assert_eq!(embedded_hash, &r_hash.to_vec());
407
408        // Unlock should work
409        let unlock_script = rp.unlock(b"sha256 test").unwrap();
410        assert_eq!(unlock_script.chunks().len(), 1);
411    }
412
413    // -----------------------------------------------------------------------
414    // RPuzzle: error cases
415    // -----------------------------------------------------------------------
416
417    #[test]
418    fn test_rpuzzle_lock_empty_value() {
419        let rp = RPuzzle::from_value(RPuzzleType::Raw, vec![]);
420        assert!(rp.lock().is_err());
421    }
422
423    #[test]
424    fn test_rpuzzle_unlock_no_key() {
425        let rp = RPuzzle::from_value(RPuzzleType::Raw, vec![0xaa; 32]);
426        assert!(rp.unlock(b"test").is_err());
427    }
428
429    #[test]
430    fn test_rpuzzle_unlock_no_k() {
431        let rp = RPuzzle {
432            puzzle_type: RPuzzleType::Raw,
433            value: vec![0xaa; 32],
434            k_value: None,
435            private_key: Some(PrivateKey::from_hex("1").unwrap()),
436            sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
437        };
438        assert!(rp.unlock(b"test").is_err());
439    }
440
441    // -----------------------------------------------------------------------
442    // RPuzzle: estimate length
443    // -----------------------------------------------------------------------
444
445    #[test]
446    fn test_rpuzzle_estimate_length() {
447        let rp = RPuzzle::from_value(RPuzzleType::Raw, vec![0xaa; 32]);
448        assert_eq!(rp.estimate_unlock_length(), 74);
449    }
450
451    // -----------------------------------------------------------------------
452    // RPuzzle: binary roundtrip
453    // -----------------------------------------------------------------------
454
455    #[test]
456    fn test_rpuzzle_lock_binary_roundtrip() {
457        let value = vec![0xde, 0xad, 0xbe, 0xef];
458        let rp = RPuzzle::from_value(RPuzzleType::SHA256, value);
459
460        let lock_script = rp.lock().unwrap();
461        let binary = lock_script.to_binary();
462
463        let reparsed = Script::from_binary(&binary);
464        assert_eq!(
465            reparsed.to_binary(),
466            binary,
467            "binary roundtrip should match"
468        );
469    }
470}