Skip to main content

bsv_rs/script/templates/
rpuzzle.rs

1//! R-Puzzle script template.
2//!
3//! This module provides the [`RPuzzle`] template for creating and spending
4//! R-Puzzle scripts. R-Puzzles lock funds using the R-value component of
5//! an ECDSA signature.
6//!
7//! # Overview
8//!
9//! An R-Puzzle allows anyone who knows a specific K-value (ECDSA nonce) to
10//! spend the output, regardless of which private key they use. This creates
11//! a form of "knowledge-based" locking where the secret is the K-value.
12//!
13//! # Locking Script Pattern (raw)
14//!
15//! ```text
16//! OP_OVER OP_3 OP_SPLIT OP_NIP OP_1 OP_SPLIT OP_SWAP OP_SPLIT OP_DROP
17//! <r-value> OP_EQUALVERIFY OP_CHECKSIG
18//! ```
19//!
20//! # Locking Script Pattern (hashed)
21//!
22//! ```text
23//! OP_OVER OP_3 OP_SPLIT OP_NIP OP_1 OP_SPLIT OP_SWAP OP_SPLIT OP_DROP
24//! OP_<HASH> <r-hash> OP_EQUALVERIFY OP_CHECKSIG
25//! ```
26//!
27//! # Unlocking Script Pattern
28//!
29//! ```text
30//! <signature> <publicKey>
31//! ```
32//!
33//! # Example
34//!
35//! ```rust,ignore
36//! use bsv_rs::script::templates::{RPuzzle, RPuzzleType};
37//! use bsv_rs::script::template::SignOutputs;
38//! use bsv_rs::primitives::ec::PrivateKey;
39//! use bsv_rs::primitives::BigNumber;
40//!
41//! // Generate a random K value
42//! let k = BigNumber::from_bytes_be(&random_32_bytes);
43//! let r_value = compute_r_from_k(&k);
44//!
45//! // Create locking script with raw R value
46//! let template = RPuzzle::new(RPuzzleType::Raw);
47//! let locking = template.lock(&r_value);
48//!
49//! // Create locking script with hashed R value
50//! let template = RPuzzle::new(RPuzzleType::Hash160);
51//! let r_hash = hash160(&r_value);
52//! let locking = template.lock(&r_hash);
53//!
54//! // Unlock using the K value
55//! let private_key = PrivateKey::random();
56//! let unlock = RPuzzle::unlock(&k, &private_key, SignOutputs::All, false);
57//! ```
58
59use crate::error::Error;
60use crate::primitives::bsv::TransactionSignature;
61use crate::primitives::ec::PrivateKey;
62use crate::primitives::hash::{hash160, ripemd160, sha1, sha256, sha256d};
63use crate::primitives::BigNumber;
64use crate::script::op::*;
65use crate::script::template::{
66    compute_sighash_scope, ScriptTemplate, ScriptTemplateUnlock, SignOutputs, SigningContext,
67};
68use crate::script::{LockingScript, Script, ScriptChunk, UnlockingScript};
69use crate::Result;
70
71use k256::elliptic_curve::ops::Reduce;
72use k256::elliptic_curve::point::AffineCoordinates;
73use k256::elliptic_curve::subtle::CtOption;
74use k256::{FieldBytes, NonZeroScalar, ProjectivePoint, Scalar, U256};
75
76/// The type of R-Puzzle to create.
77///
78/// Determines whether the R value is stored raw or hashed in the locking script.
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
80pub enum RPuzzleType {
81    /// Store the raw R value (largest script, but simplest).
82    #[default]
83    Raw,
84    /// Hash the R value with SHA-1 (20 bytes).
85    Sha1,
86    /// Hash the R value with SHA-256 (32 bytes).
87    Sha256,
88    /// Hash the R value with HASH256 (double SHA-256, 32 bytes).
89    Hash256,
90    /// Hash the R value with RIPEMD-160 (20 bytes).
91    Ripemd160,
92    /// Hash the R value with HASH160 (RIPEMD-160 of SHA-256, 20 bytes).
93    Hash160,
94}
95
96impl RPuzzleType {
97    /// Returns the opcode for this hash type, if any.
98    fn hash_opcode(self) -> Option<u8> {
99        match self {
100            RPuzzleType::Raw => None,
101            RPuzzleType::Sha1 => Some(OP_SHA1),
102            RPuzzleType::Sha256 => Some(OP_SHA256),
103            RPuzzleType::Hash256 => Some(OP_HASH256),
104            RPuzzleType::Ripemd160 => Some(OP_RIPEMD160),
105            RPuzzleType::Hash160 => Some(OP_HASH160),
106        }
107    }
108
109    /// Computes the hash of the given data using this hash type.
110    pub fn hash(self, data: &[u8]) -> Vec<u8> {
111        match self {
112            RPuzzleType::Raw => data.to_vec(),
113            RPuzzleType::Sha1 => sha1(data).to_vec(),
114            RPuzzleType::Sha256 => sha256(data).to_vec(),
115            RPuzzleType::Hash256 => sha256d(data).to_vec(),
116            RPuzzleType::Ripemd160 => ripemd160(data).to_vec(),
117            RPuzzleType::Hash160 => hash160(data).to_vec(),
118        }
119    }
120}
121
122/// R-Puzzle script template.
123///
124/// R-Puzzles lock funds using the R-value component of an ECDSA signature.
125/// Anyone who knows the K-value (ECDSA nonce) that produces a specific R-value
126/// can spend the output.
127///
128/// # Example
129///
130/// ```rust,ignore
131/// use bsv_rs::script::templates::{RPuzzle, RPuzzleType};
132///
133/// // Create a raw R-Puzzle
134/// let template = RPuzzle::new(RPuzzleType::Raw);
135/// let locking = template.lock(&r_value);
136///
137/// // Create a hashed R-Puzzle (smaller script)
138/// let template = RPuzzle::new(RPuzzleType::Hash160);
139/// let locking = template.lock(&r_hash);
140/// ```
141#[derive(Debug, Clone, Copy)]
142pub struct RPuzzle {
143    /// The type of puzzle (raw or hashed).
144    pub puzzle_type: RPuzzleType,
145}
146
147impl Default for RPuzzle {
148    fn default() -> Self {
149        Self::new(RPuzzleType::Raw)
150    }
151}
152
153impl RPuzzle {
154    /// Creates a new R-Puzzle template with the specified type.
155    ///
156    /// # Arguments
157    ///
158    /// * `puzzle_type` - Whether to use raw R value or a hashed variant
159    pub fn new(puzzle_type: RPuzzleType) -> Self {
160        Self { puzzle_type }
161    }
162
163    /// Computes the R value from a K value.
164    ///
165    /// The R value is the x-coordinate of the point k*G on the secp256k1 curve.
166    ///
167    /// # Arguments
168    ///
169    /// * `k` - The K value (ECDSA nonce)
170    ///
171    /// # Returns
172    ///
173    /// The R value as 32 bytes (big-endian)
174    pub fn compute_r_from_k(k: &BigNumber) -> Result<[u8; 32]> {
175        // Get k as bytes and convert to scalar
176        let k_bytes = k.to_bytes_be(32);
177        let k_uint = U256::from_be_slice(&k_bytes);
178        let k_scalar: CtOption<NonZeroScalar> = NonZeroScalar::from_uint(k_uint);
179
180        let k_scalar = k_scalar
181            .into_option()
182            .ok_or_else(|| Error::CryptoError("Invalid K value (zero or >= order)".to_string()))?;
183
184        // Compute R = k * G
185        let point = ProjectivePoint::GENERATOR * k_scalar.as_ref();
186        let point_affine = point.to_affine();
187
188        // Get x-coordinate (R value)
189        let x: FieldBytes = point_affine.x();
190        let mut r_bytes = [0u8; 32];
191        r_bytes.copy_from_slice(&x);
192
193        Ok(r_bytes)
194    }
195
196    /// Creates an unlock template for spending an R-Puzzle output.
197    ///
198    /// # Arguments
199    ///
200    /// * `k` - The K value that produces the expected R value
201    /// * `private_key` - A private key for signing (any key works)
202    /// * `sign_outputs` - Which outputs to sign
203    /// * `anyone_can_pay` - Whether to allow other inputs to be added
204    ///
205    /// # Returns
206    ///
207    /// A [`ScriptTemplateUnlock`] that can sign transaction inputs.
208    ///
209    /// # Example
210    ///
211    /// ```rust,ignore
212    /// use bsv_rs::script::templates::RPuzzle;
213    /// use bsv_rs::script::template::SignOutputs;
214    /// use bsv_rs::primitives::ec::PrivateKey;
215    /// use bsv_rs::primitives::BigNumber;
216    ///
217    /// let k = BigNumber::from_hex("...")?;
218    /// let private_key = PrivateKey::random();
219    ///
220    /// let unlock = RPuzzle::unlock(&k, &private_key, SignOutputs::All, false);
221    /// let unlocking = unlock.sign(&context)?;
222    /// ```
223    pub fn unlock(
224        k: &BigNumber,
225        private_key: &PrivateKey,
226        sign_outputs: SignOutputs,
227        anyone_can_pay: bool,
228    ) -> ScriptTemplateUnlock {
229        let k_value = k.clone();
230        let key = private_key.clone();
231        let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
232
233        ScriptTemplateUnlock::new(
234            move |context: &SigningContext| {
235                // Compute the sighash
236                let sighash = context.compute_sighash(scope)?;
237
238                // Sign with the specific K value
239                let signature = sign_with_k(&key, &sighash, &k_value)?;
240                let tx_sig = TransactionSignature::new(signature, scope);
241
242                // Build the unlocking script
243                let sig_bytes = tx_sig.to_checksig_format();
244                let pubkey_bytes = key.public_key().to_compressed();
245
246                let mut script = Script::new();
247                script.write_bin(&sig_bytes).write_bin(&pubkey_bytes);
248
249                Ok(UnlockingScript::from_script(script))
250            },
251            || {
252                // Estimate length: signature (1 push + 73 bytes max) + pubkey (1 push + 33 bytes)
253                108
254            },
255        )
256    }
257
258    /// Signs with a precomputed sighash using a specific K value.
259    ///
260    /// # Arguments
261    ///
262    /// * `k` - The K value to use for signing
263    /// * `private_key` - The private key for signing
264    /// * `sighash` - The precomputed sighash
265    /// * `sign_outputs` - Which outputs to sign (for the scope byte)
266    /// * `anyone_can_pay` - Whether to allow other inputs to be added
267    ///
268    /// # Returns
269    ///
270    /// The unlocking script.
271    pub fn sign_with_sighash(
272        k: &BigNumber,
273        private_key: &PrivateKey,
274        sighash: &[u8; 32],
275        sign_outputs: SignOutputs,
276        anyone_can_pay: bool,
277    ) -> Result<UnlockingScript> {
278        let scope = compute_sighash_scope(sign_outputs, anyone_can_pay);
279
280        // Sign with the specific K value
281        let signature = sign_with_k(private_key, sighash, k)?;
282        let tx_sig = TransactionSignature::new(signature, scope);
283
284        // Build the unlocking script
285        let sig_bytes = tx_sig.to_checksig_format();
286        let pubkey_bytes = private_key.public_key().to_compressed();
287
288        let mut script = Script::new();
289        script.write_bin(&sig_bytes).write_bin(&pubkey_bytes);
290
291        Ok(UnlockingScript::from_script(script))
292    }
293}
294
295impl ScriptTemplate for RPuzzle {
296    /// Creates an R-Puzzle locking script.
297    ///
298    /// # Arguments
299    ///
300    /// * `params` - The R value or its hash (depending on puzzle type)
301    ///
302    /// # Returns
303    ///
304    /// The locking script.
305    ///
306    /// # Script Structure
307    ///
308    /// The script extracts the R value from the signature and compares it:
309    ///
310    /// ```text
311    /// OP_OVER OP_3 OP_SPLIT OP_NIP OP_1 OP_SPLIT OP_SWAP OP_SPLIT OP_DROP
312    /// [OP_<HASH>] <value> OP_EQUALVERIFY OP_CHECKSIG
313    /// ```
314    fn lock(&self, params: &[u8]) -> Result<LockingScript> {
315        // Build the R-extraction prefix:
316        // OP_OVER OP_3 OP_SPLIT OP_NIP OP_1 OP_SPLIT OP_SWAP OP_SPLIT OP_DROP
317        //
318        // This extracts the R value from a DER-encoded signature on the stack.
319        //
320        // Stack operations:
321        // Initial: <sig> <pubkey>
322        // OP_OVER: <sig> <pubkey> <sig>
323        // OP_3: <sig> <pubkey> <sig> 3
324        // OP_SPLIT: <sig> <pubkey> <3 bytes> <rest of sig>
325        // OP_NIP: <sig> <pubkey> <rest of sig starting at R-length byte>
326        // OP_1: <sig> <pubkey> <rest> 1
327        // OP_SPLIT: <sig> <pubkey> <R-length byte> <rest>
328        // OP_SWAP: <sig> <pubkey> <rest> <R-length byte>
329        // OP_SPLIT: <sig> <pubkey> <R value> <rest>
330        // OP_DROP: <sig> <pubkey> <R value>
331
332        let mut chunks = vec![
333            ScriptChunk::new_opcode(OP_OVER),
334            ScriptChunk::new_opcode(OP_3),
335            ScriptChunk::new_opcode(OP_SPLIT),
336            ScriptChunk::new_opcode(OP_NIP),
337            ScriptChunk::new_opcode(OP_1),
338            ScriptChunk::new_opcode(OP_SPLIT),
339            ScriptChunk::new_opcode(OP_SWAP),
340            ScriptChunk::new_opcode(OP_SPLIT),
341            ScriptChunk::new_opcode(OP_DROP),
342        ];
343
344        // Add hash opcode if not raw
345        if let Some(hash_op) = self.puzzle_type.hash_opcode() {
346            chunks.push(ScriptChunk::new_opcode(hash_op));
347        }
348
349        // Add the expected value
350        let op = if params.len() < OP_PUSHDATA1 as usize {
351            params.len() as u8
352        } else if params.len() < 256 {
353            OP_PUSHDATA1
354        } else {
355            OP_PUSHDATA2
356        };
357        chunks.push(ScriptChunk::new(op, Some(params.to_vec())));
358
359        // Add comparison and checksig
360        chunks.push(ScriptChunk::new_opcode(OP_EQUALVERIFY));
361        chunks.push(ScriptChunk::new_opcode(OP_CHECKSIG));
362
363        Ok(LockingScript::from_chunks(chunks))
364    }
365}
366
367/// Signs a message hash with a specific K value.
368///
369/// This is a low-level function that creates an ECDSA signature using
370/// a predetermined nonce (K value) instead of RFC 6979.
371fn sign_with_k(
372    private_key: &PrivateKey,
373    msg_hash: &[u8; 32],
374    k: &BigNumber,
375) -> Result<crate::primitives::ec::Signature> {
376    use k256::ecdsa::Signature as K256Signature;
377
378    // Get the private key as a scalar
379    let secret_key = k256::SecretKey::from_slice(&private_key.to_bytes())
380        .map_err(|e| Error::CryptoError(format!("Invalid private key: {}", e)))?;
381    let d = secret_key.to_nonzero_scalar();
382
383    // Get k as a scalar
384    let k_bytes = k.to_bytes_be(32);
385    let k_uint = U256::from_be_slice(&k_bytes);
386    let k_scalar: CtOption<NonZeroScalar> = NonZeroScalar::from_uint(k_uint);
387    let k_nonzero = k_scalar
388        .into_option()
389        .ok_or_else(|| Error::CryptoError("Invalid K value".to_string()))?;
390
391    // Compute R = k * G
392    let r_point = (ProjectivePoint::GENERATOR * k_nonzero.as_ref()).to_affine();
393    let r_bytes: FieldBytes = r_point.x();
394    let r = <Scalar as Reduce<U256>>::reduce_bytes(&r_bytes);
395
396    // Compute s = k^-1 * (z + r * d) mod n
397    // Note: from_slice deprecation is from generic-array 0.x, a transitive dependency of k256
398    #[allow(deprecated)]
399    let z_bytes: FieldBytes = *FieldBytes::from_slice(msg_hash);
400    let z = <Scalar as Reduce<U256>>::reduce_bytes(&z_bytes);
401    let k_inv_opt: CtOption<Scalar> = k_nonzero.invert();
402    let k_inv: Scalar = k_inv_opt
403        .into_option()
404        .ok_or_else(|| Error::CryptoError("K has no inverse".to_string()))?;
405    let s = k_inv * (z + r * d.as_ref());
406
407    // Convert to non-zero scalar for signature
408    let r_nonzero: CtOption<NonZeroScalar> = NonZeroScalar::new(r);
409    let s_nonzero: CtOption<NonZeroScalar> = NonZeroScalar::new(s);
410
411    let r_nz = r_nonzero
412        .into_option()
413        .ok_or_else(|| Error::CryptoError("R is zero".to_string()))?;
414    let s_nz = s_nonzero
415        .into_option()
416        .ok_or_else(|| Error::CryptoError("S is zero".to_string()))?;
417
418    // Create signature
419    let k256_sig = K256Signature::from_scalars(r_nz, s_nz)
420        .map_err(|e| Error::CryptoError(format!("Failed to create signature: {}", e)))?;
421
422    // Convert to our Signature type
423    let r_bytes = k256_sig.r().to_bytes();
424    let s_bytes = k256_sig.s().to_bytes();
425
426    let mut r_arr = [0u8; 32];
427    let mut s_arr = [0u8; 32];
428    r_arr.copy_from_slice(&r_bytes);
429    s_arr.copy_from_slice(&s_bytes);
430
431    let signature = crate::primitives::ec::Signature::new(r_arr, s_arr);
432
433    // Ensure low-S (BIP 62)
434    Ok(signature.to_low_s())
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn test_rpuzzle_lock_raw() {
443        let r_value = [0x42u8; 32];
444        let template = RPuzzle::new(RPuzzleType::Raw);
445        let locking = template.lock(&r_value).unwrap();
446
447        let asm = locking.to_asm();
448
449        // Should contain the R-extraction prefix
450        assert!(asm.contains("OP_OVER"));
451        assert!(asm.contains("OP_3"));
452        assert!(asm.contains("OP_SPLIT"));
453        assert!(asm.contains("OP_NIP"));
454        assert!(asm.contains("OP_SWAP"));
455        assert!(asm.contains("OP_DROP"));
456
457        // Should contain EQUALVERIFY and CHECKSIG
458        assert!(asm.contains("OP_EQUALVERIFY"));
459        assert!(asm.contains("OP_CHECKSIG"));
460
461        // Should NOT contain hash opcodes
462        assert!(!asm.contains("OP_HASH160"));
463        assert!(!asm.contains("OP_SHA256"));
464    }
465
466    #[test]
467    fn test_rpuzzle_lock_hash160() {
468        let r_hash = [0x42u8; 20];
469        let template = RPuzzle::new(RPuzzleType::Hash160);
470        let locking = template.lock(&r_hash).unwrap();
471
472        let asm = locking.to_asm();
473
474        // Should contain OP_HASH160
475        assert!(asm.contains("OP_HASH160"));
476    }
477
478    #[test]
479    fn test_rpuzzle_lock_sha256() {
480        let r_hash = [0x42u8; 32];
481        let template = RPuzzle::new(RPuzzleType::Sha256);
482        let locking = template.lock(&r_hash).unwrap();
483
484        let asm = locking.to_asm();
485
486        // Should contain OP_SHA256
487        assert!(asm.contains("OP_SHA256"));
488    }
489
490    #[test]
491    fn test_rpuzzle_type_hash() {
492        let data = b"test data";
493
494        // Test each hash type
495        assert_eq!(RPuzzleType::Raw.hash(data), data.to_vec());
496        assert_eq!(RPuzzleType::Sha1.hash(data).len(), 20);
497        assert_eq!(RPuzzleType::Sha256.hash(data).len(), 32);
498        assert_eq!(RPuzzleType::Hash256.hash(data).len(), 32);
499        assert_eq!(RPuzzleType::Ripemd160.hash(data).len(), 20);
500        assert_eq!(RPuzzleType::Hash160.hash(data).len(), 20);
501    }
502
503    #[test]
504    fn test_compute_r_from_k() {
505        // Use a known K value
506        let k =
507            BigNumber::from_hex("0000000000000000000000000000000000000000000000000000000000000001")
508                .unwrap();
509
510        let r = RPuzzle::compute_r_from_k(&k).unwrap();
511
512        // k=1 means R = G, which has a known x-coordinate
513        // The generator point G has x = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
514        let expected_r =
515            hex::decode("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798")
516                .unwrap();
517        assert_eq!(r.to_vec(), expected_r);
518    }
519
520    #[test]
521    fn test_rpuzzle_unlock_produces_valid_script() {
522        let k =
523            BigNumber::from_hex("0000000000000000000000000000000000000000000000000000000000000002")
524                .unwrap();
525        let private_key = PrivateKey::random();
526        let sighash = [1u8; 32];
527
528        let unlocking =
529            RPuzzle::sign_with_sighash(&k, &private_key, &sighash, SignOutputs::All, false)
530                .unwrap();
531
532        // The unlocking script should have 2 chunks: signature and pubkey
533        let chunks = unlocking.chunks();
534        assert_eq!(chunks.len(), 2);
535
536        // First chunk should be the signature
537        assert!(chunks[0].data.is_some());
538        let sig_data = chunks[0].data.as_ref().unwrap();
539
540        // Parse the signature and verify the R value matches k*G
541        let expected_r = RPuzzle::compute_r_from_k(&k).unwrap();
542
543        // The signature is in DER format + sighash byte
544        // DER: 0x30 <len> 0x02 <r_len> <r> 0x02 <s_len> <s>
545        // Skip the DER header and extract R
546        let r_len = sig_data[3] as usize;
547        let r_start = 4;
548        let r_bytes = &sig_data[r_start..r_start + r_len];
549
550        // R may have a leading zero if high bit is set
551        let r_trimmed: Vec<u8> = r_bytes.iter().copied().skip_while(|&b| b == 0).collect();
552        let expected_trimmed: Vec<u8> =
553            expected_r.iter().copied().skip_while(|&b| b == 0).collect();
554
555        assert_eq!(r_trimmed, expected_trimmed);
556    }
557
558    #[test]
559    fn test_rpuzzle_estimate_length() {
560        let k = BigNumber::from_i64(1);
561        let private_key = PrivateKey::random();
562
563        let unlock = RPuzzle::unlock(&k, &private_key, SignOutputs::All, false);
564
565        // Should estimate 108 bytes
566        assert_eq!(unlock.estimate_length(), 108);
567    }
568
569    #[test]
570    fn test_rpuzzle_all_hash_types_lock() {
571        let data = [0x42u8; 32];
572
573        // Test each puzzle type
574        for puzzle_type in [
575            RPuzzleType::Raw,
576            RPuzzleType::Sha1,
577            RPuzzleType::Sha256,
578            RPuzzleType::Hash256,
579            RPuzzleType::Ripemd160,
580            RPuzzleType::Hash160,
581        ] {
582            let hash = puzzle_type.hash(&data);
583            let template = RPuzzle::new(puzzle_type);
584            let locking = template.lock(&hash);
585            assert!(locking.is_ok(), "Failed for {:?}", puzzle_type);
586        }
587    }
588}