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