use crate::primitives::big_number::BigNumber;
use crate::primitives::ecdsa::ecdsa_sign_with_k;
use crate::primitives::hash::sha256;
use crate::primitives::private_key::PrivateKey;
use crate::primitives::transaction_signature::{SIGHASH_ALL, SIGHASH_FORKID};
use crate::script::error::ScriptError;
use crate::script::locking_script::LockingScript;
use crate::script::op::Op;
use crate::script::script::Script;
use crate::script::script_chunk::ScriptChunk;
use crate::script::templates::{ScriptTemplateLock, ScriptTemplateUnlock};
use crate::script::unlocking_script::UnlockingScript;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RPuzzleType {
Raw,
SHA1,
SHA256,
Hash256,
RIPEMD160,
Hash160,
}
#[derive(Clone, Debug)]
pub struct RPuzzle {
pub puzzle_type: RPuzzleType,
pub value: Vec<u8>,
pub k_value: Option<BigNumber>,
pub private_key: Option<PrivateKey>,
pub sighash_type: u32,
}
impl RPuzzle {
pub fn from_value(puzzle_type: RPuzzleType, value: Vec<u8>) -> Self {
RPuzzle {
puzzle_type,
value,
k_value: None,
private_key: None,
sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
}
}
pub fn from_k(puzzle_type: RPuzzleType, value: Vec<u8>, k: BigNumber, key: PrivateKey) -> Self {
RPuzzle {
puzzle_type,
value,
k_value: Some(k),
private_key: Some(key),
sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
}
}
pub fn unlock(&self, preimage: &[u8]) -> Result<UnlockingScript, ScriptError> {
let key = self.private_key.as_ref().ok_or_else(|| {
ScriptError::InvalidScript("RPuzzle: no private key for unlock".into())
})?;
let k = self
.k_value
.as_ref()
.ok_or_else(|| ScriptError::InvalidScript("RPuzzle: no k-value for unlock".into()))?;
let msg_hash = sha256(preimage);
let sig = ecdsa_sign_with_k(&msg_hash, key.bn(), k, true).map_err(|e| {
ScriptError::InvalidSignature(format!("ECDSA sign with k failed: {}", e))
})?;
let mut sig_bytes = sig.to_der();
sig_bytes.push(self.sighash_type as u8);
let chunks = vec![ScriptChunk::new_raw(sig_bytes.len() as u8, Some(sig_bytes))];
Ok(UnlockingScript::from_script(Script::from_chunks(chunks)))
}
pub fn estimate_unlock_length(&self) -> usize {
74
}
fn r_extraction_chunks() -> Vec<ScriptChunk> {
vec![
ScriptChunk::new_opcode(Op::OpDup), ScriptChunk::new_opcode(Op::Op3), ScriptChunk::new_opcode(Op::OpSplit), ScriptChunk::new_opcode(Op::OpNip), ScriptChunk::new_opcode(Op::Op1), ScriptChunk::new_opcode(Op::OpSplit), ScriptChunk::new_opcode(Op::OpSwap), ScriptChunk::new_opcode(Op::OpSplit), ScriptChunk::new_opcode(Op::OpDrop), ]
}
fn hash_opcode(&self) -> Option<Op> {
match self.puzzle_type {
RPuzzleType::Raw => None,
RPuzzleType::SHA1 => Some(Op::OpSha1),
RPuzzleType::SHA256 => Some(Op::OpSha256),
RPuzzleType::Hash256 => Some(Op::OpHash256),
RPuzzleType::RIPEMD160 => Some(Op::OpRipemd160),
RPuzzleType::Hash160 => Some(Op::OpHash160),
}
}
}
impl ScriptTemplateLock for RPuzzle {
fn lock(&self) -> Result<LockingScript, ScriptError> {
if self.value.is_empty() {
return Err(ScriptError::InvalidScript(
"RPuzzle: value must not be empty".into(),
));
}
let mut chunks = Self::r_extraction_chunks();
if let Some(hash_op) = self.hash_opcode() {
chunks.push(ScriptChunk::new_opcode(hash_op));
}
let val_len = self.value.len();
if val_len < 0x4c {
chunks.push(ScriptChunk::new_raw(
val_len as u8,
Some(self.value.clone()),
));
} else {
chunks.push(ScriptChunk::new_raw(
Op::OpPushData1.to_byte(),
Some(self.value.clone()),
));
}
chunks.push(ScriptChunk::new_opcode(Op::OpEqualVerify));
chunks.push(ScriptChunk::new_opcode(Op::OpCheckSig));
Ok(LockingScript::from_script(Script::from_chunks(chunks)))
}
}
impl ScriptTemplateUnlock for RPuzzle {
fn sign(&self, preimage: &[u8]) -> Result<UnlockingScript, ScriptError> {
self.unlock(preimage)
}
fn estimate_length(&self) -> Result<usize, ScriptError> {
Ok(self.estimate_unlock_length())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::base_point::BasePoint;
use crate::primitives::big_number::Endian;
use crate::primitives::hash::{hash160, hash256, ripemd160, sha1, sha256};
fn bytes_to_hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
#[test]
fn test_rpuzzle_lock_raw() {
let value = vec![0xaa; 32];
let rp = RPuzzle::from_value(RPuzzleType::Raw, value.clone());
let lock_script = rp.lock().unwrap();
let chunks = lock_script.chunks();
assert_eq!(chunks.len(), 12, "Raw RPuzzle should have 12 chunks");
assert_eq!(chunks[0].op, Op::OpDup);
assert_eq!(chunks[1].op, Op::Op3);
assert_eq!(chunks[2].op, Op::OpSplit);
assert_eq!(chunks[3].op, Op::OpNip);
assert_eq!(chunks[4].op, Op::Op1);
assert_eq!(chunks[5].op, Op::OpSplit);
assert_eq!(chunks[6].op, Op::OpSwap);
assert_eq!(chunks[7].op, Op::OpSplit);
assert_eq!(chunks[8].op, Op::OpDrop);
assert_eq!(chunks[9].data.as_ref().unwrap(), &value);
assert_eq!(chunks[10].op, Op::OpEqualVerify);
assert_eq!(chunks[11].op, Op::OpCheckSig);
}
#[test]
fn test_rpuzzle_lock_sha256() {
let value = vec![0xbb; 32];
let rp = RPuzzle::from_value(RPuzzleType::SHA256, value.clone());
let lock_script = rp.lock().unwrap();
let chunks = lock_script.chunks();
assert_eq!(chunks.len(), 13, "SHA256 RPuzzle should have 13 chunks");
assert_eq!(chunks[9].op, Op::OpSha256);
assert_eq!(chunks[10].data.as_ref().unwrap(), &value);
}
#[test]
fn test_rpuzzle_lock_hash_types() {
let value = vec![0xcc; 20];
let test_cases = vec![
(RPuzzleType::SHA1, Op::OpSha1, 13),
(RPuzzleType::Hash256, Op::OpHash256, 13),
(RPuzzleType::RIPEMD160, Op::OpRipemd160, 13),
(RPuzzleType::Hash160, Op::OpHash160, 13),
];
for (ptype, expected_op, expected_chunks) in test_cases {
let rp = RPuzzle::from_value(ptype, value.clone());
let lock_script = rp.lock().unwrap();
let chunks = lock_script.chunks();
assert_eq!(
chunks.len(),
expected_chunks,
"{:?} should have {} chunks",
ptype,
expected_chunks
);
assert_eq!(
chunks[9].op, expected_op,
"{:?} hash opcode mismatch",
ptype
);
}
}
#[test]
fn test_rpuzzle_unlock_with_k() {
let key = PrivateKey::from_hex("1").unwrap();
let k = BigNumber::from_number(42);
let base_point = BasePoint::instance();
let r_point = base_point.mul(&k);
let r_bytes = r_point.get_x().to_array(Endian::Big, Some(32));
let rp = RPuzzle::from_k(RPuzzleType::Raw, r_bytes, k, key);
let unlock_script = rp.unlock(b"test preimage").unwrap();
assert_eq!(unlock_script.chunks().len(), 1);
let sig_data = unlock_script.chunks()[0].data.as_ref().unwrap();
assert!(sig_data.len() >= 70 && sig_data.len() <= 74);
}
#[test]
fn test_rpuzzle_roundtrip_raw() {
let key = PrivateKey::from_hex("ff").unwrap();
let k = BigNumber::from_number(12345);
let base_point = BasePoint::instance();
let r_point = base_point.mul(&k);
let r_value = r_point.get_x().to_array(Endian::Big, Some(32));
let rp = RPuzzle::from_k(RPuzzleType::Raw, r_value.clone(), k.clone(), key.clone());
let lock_script = rp.lock().unwrap();
let lock_chunks = lock_script.chunks();
let embedded_value = lock_chunks[9].data.as_ref().unwrap();
assert_eq!(
embedded_value, &r_value,
"embedded value should match R-value"
);
let unlock_script = rp.unlock(b"test roundtrip").unwrap();
assert_eq!(unlock_script.chunks().len(), 1);
let sig_with_sighash = unlock_script.chunks()[0].data.as_ref().unwrap();
let sig_der = &sig_with_sighash[..sig_with_sighash.len() - 1];
assert_eq!(sig_der[0], 0x30);
assert_eq!(sig_der[2], 0x02);
let r_len = sig_der[3] as usize;
let r_bytes = &sig_der[4..4 + r_len];
let r_trimmed = if !r_bytes.is_empty() && r_bytes[0] == 0x00 {
&r_bytes[1..]
} else {
r_bytes
};
let mut r_padded = vec![0u8; 32];
let start = 32 - r_trimmed.len();
r_padded[start..].copy_from_slice(r_trimmed);
assert_eq!(
r_padded, r_value,
"signature R-value should match the puzzle value"
);
}
#[test]
fn test_rpuzzle_roundtrip_sha256() {
let key = PrivateKey::from_hex("abcd").unwrap();
let k = BigNumber::from_number(9999);
let base_point = BasePoint::instance();
let r_point = base_point.mul(&k);
let r_value = r_point.get_x().to_array(Endian::Big, Some(32));
let r_hash = sha256(&r_value);
let rp = RPuzzle::from_k(RPuzzleType::SHA256, r_hash.to_vec(), k, key);
let lock_script = rp.lock().unwrap();
let lock_chunks = lock_script.chunks();
let embedded_hash = lock_chunks[10].data.as_ref().unwrap();
assert_eq!(embedded_hash, &r_hash.to_vec());
let unlock_script = rp.unlock(b"sha256 test").unwrap();
assert_eq!(unlock_script.chunks().len(), 1);
}
#[test]
fn test_rpuzzle_lock_empty_value() {
let rp = RPuzzle::from_value(RPuzzleType::Raw, vec![]);
assert!(rp.lock().is_err());
}
#[test]
fn test_rpuzzle_unlock_no_key() {
let rp = RPuzzle::from_value(RPuzzleType::Raw, vec![0xaa; 32]);
assert!(rp.unlock(b"test").is_err());
}
#[test]
fn test_rpuzzle_unlock_no_k() {
let rp = RPuzzle {
puzzle_type: RPuzzleType::Raw,
value: vec![0xaa; 32],
k_value: None,
private_key: Some(PrivateKey::from_hex("1").unwrap()),
sighash_type: SIGHASH_ALL | SIGHASH_FORKID,
};
assert!(rp.unlock(b"test").is_err());
}
#[test]
fn test_rpuzzle_estimate_length() {
let rp = RPuzzle::from_value(RPuzzleType::Raw, vec![0xaa; 32]);
assert_eq!(rp.estimate_unlock_length(), 74);
}
#[test]
fn test_rpuzzle_lock_binary_roundtrip() {
let value = vec![0xde, 0xad, 0xbe, 0xef];
let rp = RPuzzle::from_value(RPuzzleType::SHA256, value);
let lock_script = rp.lock().unwrap();
let binary = lock_script.to_binary();
let reparsed = Script::from_binary(&binary);
assert_eq!(
reparsed.to_binary(),
binary,
"binary roundtrip should match"
);
}
}