From 1221e2a0590332df061b14f28e04cb6c98c93a9d Mon Sep 17 00:00:00 2001 From: Zhen Lu Date: Tue, 9 Apr 2024 13:40:55 -0700 Subject: [PATCH] Add a derive_and_tweak_pubkey util function (#56) --- src/crypto.rs | 92 ++++++++++++++++++++++++++++++++++++++- src/lib.rs | 2 + src/lightspark_crypto.udl | 5 +++ 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/crypto.rs b/src/crypto.rs index d3f7e77..9a9b72c 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,9 +1,11 @@ use std::fmt; +use std::str::FromStr as _; use std::sync::Arc; +use bitcoin::bip32::{DerivationPath, Xpub}; use bitcoin::hashes::sha256; use bitcoin::secp256k1::ecdsa::Signature; -use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, SecretKey}; +use bitcoin::secp256k1::{Message, PublicKey, Scalar, Secp256k1, SecretKey}; use bitcoin::{ blockdata::{opcodes::all, script::Builder}, PublicKey as BitcoinPublicKey, @@ -18,6 +20,8 @@ pub enum CryptoError { Secp256k1Error(bitcoin::secp256k1::Error), RustSecp256k1Error(ecies::SecpError), InvalidPublicKeyScriptError, + KeyDerivationError, + KeyTweakError, } #[derive(Clone)] @@ -42,6 +46,8 @@ impl fmt::Display for CryptoError { Self::Secp256k1Error(err) => write!(f, "Secp256k1 error {}", err), Self::RustSecp256k1Error(err) => write!(f, "Rust Secp256k1 error {}", err), Self::InvalidPublicKeyScriptError => write!(f, "Invalid public key script"), + Self::KeyDerivationError => write!(f, "Key derivation error"), + Self::KeyTweakError => write!(f, "Key tweak error"), } } } @@ -89,6 +95,41 @@ pub fn generate_keypair() -> Result, CryptoError> { Ok(keypair.into()) } +pub fn derive_and_tweak_pubkey( + pubkey: String, + derivation_path: String, + add_tweak: Option>, + mul_tweak: Option>, +) -> Result, CryptoError> { + let secp = Secp256k1::new(); + let path = + DerivationPath::from_str(&derivation_path).map_err(|_| CryptoError::KeyDerivationError)?; + let xpub = Xpub::from_str(&pubkey).map_err(|_| CryptoError::KeyDerivationError)?; + let derived_pubkey = xpub + .derive_pub(&secp, &path) + .map_err(|_| CryptoError::KeyDerivationError)?; + + let mut pubkey = derived_pubkey.public_key; + if let Some(tweak) = mul_tweak { + let tweak_bytes: [u8; 32] = tweak.try_into().map_err(|_| CryptoError::KeyTweakError)?; + let tweak_scalar = + Scalar::from_be_bytes(tweak_bytes).map_err(|_| CryptoError::KeyTweakError)?; + pubkey = pubkey + .mul_tweak(&secp, &tweak_scalar) + .map_err(|_| CryptoError::KeyTweakError)?; + } + + if let Some(tweak) = add_tweak { + let tweak_bytes: [u8; 32] = tweak.try_into().map_err(|_| CryptoError::KeyTweakError)?; + let tweak_scalar = + Scalar::from_be_bytes(tweak_bytes).map_err(|_| CryptoError::KeyTweakError)?; + pubkey = pubkey + .add_exp_tweak(&secp, &tweak_scalar) + .map_err(|_| CryptoError::KeyTweakError)?; + } + Ok(pubkey.serialize().to_vec()) +} + pub fn generate_multisig_address( network: Network, pk1: Vec, @@ -139,6 +180,8 @@ fn _generate_multisig_address( mod tests { use ecies::utils::generate_keypair; + use crate::signer::{LightsparkSigner, Seed}; + use super::*; #[test] @@ -194,4 +237,51 @@ mod tests { "bcrt1qwgpja522vatddf0vfggrej8pcjrzvzcpkl5yvxzzq4djwr0gj9asrk86y9" ) } + + #[test] + fn test_derive_and_tweak_pubkey() { + let seed_hex_string = "000102030405060708090a0b0c0d0e0f"; + let seed_bytes = hex::decode(seed_hex_string).unwrap(); + let seed = Seed::new(seed_bytes); + + let signer = LightsparkSigner::new(&seed, Network::Bitcoin).unwrap(); + let xpub = signer.derive_public_key("m".to_owned()).unwrap(); + + let message = + hex::decode("9a0c7185121c40850e3e40d3170a5b408374217dc617067f3d7760c522733cef") + .unwrap(); + + let derivation_path = "m/3/1234856/4"; + let add_tweak = + hex::decode("a66cd04862ae9041906f027db9cd43783dad06615fdf9001c5369b315fbef90a") + .unwrap(); + let mul_tweak = + hex::decode("d273f16519917211ffee805216b7cb5ae14600eeca5fbc84cefae62cf6a011a4") + .unwrap(); + + let signature = signer + .derive_key_and_sign( + message.clone(), + derivation_path.to_string(), + true, + Some(add_tweak.clone()), + Some(mul_tweak.clone()), + ) + .unwrap(); + + let pubkey = derive_and_tweak_pubkey( + xpub, + derivation_path.to_string(), + Some(add_tweak.clone()), + Some(mul_tweak.clone()), + ) + .unwrap(); + + let verify_message = Message::from_digest_slice(message.as_slice()).unwrap(); + let secp = Secp256k1::new(); + let sig = Signature::from_compact(&signature).unwrap(); + let pk = PublicKey::from_slice(&pubkey).unwrap(); + + assert!(secp.verify_ecdsa(&verify_message, &sig, &pk).is_ok()); + } } diff --git a/src/lib.rs b/src/lib.rs index 2b358c1..0104f0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,8 @@ pub mod crypto; pub mod remote_signing; pub mod signer; +#[cfg(not(target_arch = "wasm32"))] +use crate::crypto::derive_and_tweak_pubkey; #[cfg(not(target_arch = "wasm32"))] use crate::crypto::generate_multisig_address; #[cfg(not(target_arch = "wasm32"))] diff --git a/src/lightspark_crypto.udl b/src/lightspark_crypto.udl index 164df6b..ea828bd 100644 --- a/src/lightspark_crypto.udl +++ b/src/lightspark_crypto.udl @@ -17,6 +17,9 @@ namespace lightspark_crypto { [Throws=CryptoError] KeyPair generate_keypair(); + [Throws=CryptoError] + sequence derive_and_tweak_pubkey(string pubkey, string derivation_path, sequence? add_tweak, sequence? mul_tweak); + [Throws=RemoteSigningError] RemoteSigningResponse? handle_remote_signing_webhook_event( sequence webhook_data, @@ -55,6 +58,8 @@ enum CryptoError { "Secp256k1Error", "RustSecp256k1Error", "InvalidPublicKeyScriptError", + "KeyDerivationError", + "KeyTweakError", }; [Error]