From 7d5b0e6c107d8495ae34f00c039c7169ba655e14 Mon Sep 17 00:00:00 2001 From: rob-stacks Date: Wed, 10 Jun 2026 08:18:06 +0200 Subject: [PATCH 1/6] unify secp256k1 api to use just pure-rust libsecp256k1 --- Cargo.lock | 1 - clarity/src/vm/tests/crypto.rs | 22 + stacks-common/Cargo.toml | 5 +- stacks-common/src/util/secp256k1/mod.rs | 9 - stacks-common/src/util/secp256k1/native.rs | 940 ++++++++++++++---- stacks-common/src/util/secp256k1/wasm.rs | 382 ------- .../burnchains/bitcoin_regtest_controller.rs | 30 +- 7 files changed, 779 insertions(+), 610 deletions(-) delete mode 100644 stacks-common/src/util/secp256k1/wasm.rs diff --git a/Cargo.lock b/Cargo.lock index 5dfade348fd..de5c7cb60ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4423,7 +4423,6 @@ dependencies = [ "rand 0.8.5", "ripemd", "rusqlite", - "secp256k1", "serde", "serde_derive", "serde_json", diff --git a/clarity/src/vm/tests/crypto.rs b/clarity/src/vm/tests/crypto.rs index df3b8974589..190fc4b4e82 100644 --- a/clarity/src/vm/tests/crypto.rs +++ b/clarity/src/vm/tests/crypto.rs @@ -557,6 +557,28 @@ fn test_secp256k1_verify_valid_signature_returns_true() { ); } +#[test] +fn test_secp256k1_recover_high_s_signature_succeeds() { + // secp256k1-recover? must succeed (return ok) for high-S signatures even though + // secp256k1-verify rejects the same signature. + let message = "0x89171d7815da4bc1f644665a3234bc99d1680afa0b3285eff4878f4275fbfa89"; + let signature = "0x54cd3f378a424a3e50ff1c911b7d80cf424e1b86dddecadbcf39077e62fa1e54ee6514347c1608df2c3995e7356f2d60a1fab60878214642134d78cd923ce27a01"; + + let program = format!("(is-ok (secp256k1-recover? {message} {signature}))"); + + assert_eq!( + Value::Bool(true), + execute_with_parameters( + program.as_str(), + ClarityVersion::latest(), + StacksEpochId::latest(), + false + ) + .expect("execution should succeed") + .expect("should return a value") + ); +} + #[test] fn test_secp256k1_verify_valid_high_s_signature_returns_false() { let message = "0x89171d7815da4bc1f644665a3234bc99d1680afa0b3285eff4878f4275fbfa89"; diff --git a/stacks-common/Cargo.toml b/stacks-common/Cargo.toml index 16d0622a479..8e739030383 100644 --- a/stacks-common/Cargo.toml +++ b/stacks-common/Cargo.toml @@ -33,6 +33,7 @@ curve25519-dalek = { version = "4.1.3", default-features = false, features = ["s ed25519-dalek = { workspace = true } hashbrown = { workspace = true } lazy_static = { workspace = true } +libsecp256k1 = { version = "0.7.2", default-features = false, features = ["hmac", "lazy-static-context"] } ripemd = { version = "0.1.1", default-features = false } serde = { workspace = true , features = ["derive"] } serde_derive = { workspace = true } @@ -62,12 +63,8 @@ winapi = { version = "0.3", features = [ ], optional = true } [target.'cfg(not(target_family = "wasm"))'.dependencies] -secp256k1 = { version = "0.24.3", default-features = false, features = ["std","serde", "recovery"] } rusqlite = { workspace = true, optional = true } -[target.'cfg(target_family = "wasm")'.dependencies] -libsecp256k1 = { version = "0.7.2", default-features = false, features = ["hmac", "lazy-static-context"] } - [target.'cfg(all(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64"), not(any(target_os="windows"))))'.dependencies] sha2 = { version = "0.10", features = ["asm"] } diff --git a/stacks-common/src/util/secp256k1/mod.rs b/stacks-common/src/util/secp256k1/mod.rs index 50ee281e306..f2be8716588 100644 --- a/stacks-common/src/util/secp256k1/mod.rs +++ b/stacks-common/src/util/secp256k1/mod.rs @@ -1,15 +1,6 @@ -#[cfg(not(target_family = "wasm"))] mod native; - -#[cfg(not(target_family = "wasm"))] pub use self::native::*; -#[cfg(target_family = "wasm")] -mod wasm; - -#[cfg(target_family = "wasm")] -pub use self::wasm::*; - pub const MESSAGE_SIGNATURE_ENCODED_SIZE: u32 = 65; pub struct MessageSignature(pub [u8; 65]); diff --git a/stacks-common/src/util/secp256k1/native.rs b/stacks-common/src/util/secp256k1/native.rs index 49d969311b5..4e4bc88bae5 100644 --- a/stacks-common/src/util/secp256k1/native.rs +++ b/stacks-common/src/util/secp256k1/native.rs @@ -14,16 +14,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use ::secp256k1::ecdsa::{ - RecoverableSignature as LibSecp256k1RecoverableSignature, RecoveryId as LibSecp256k1RecoveryID, - Signature as LibSecp256k1Signature, -}; -pub use ::secp256k1::Error; -use ::secp256k1::{ - self, constants as LibSecp256k1Constants, Error as LibSecp256k1Error, - Message as LibSecp256k1Message, PublicKey as LibSecp256k1PublicKey, Secp256k1, - SecretKey as LibSecp256k1PrivateKey, +#[cfg(all(any(test, feature = "testing"), not(feature = "wasm-deterministic")))] +use ::libsecp256k1::curve::Scalar; +pub use ::libsecp256k1::Error; +use ::libsecp256k1::{ + self, PublicKey as LibSecp256k1PublicKey, RecoveryId as LibSecp256k1RecoveryId, + SecretKey as LibSecp256k1PrivateKey, Signature as LibSecp256k1Signature, ECMULT_GEN_CONTEXT, }; +#[cfg(not(feature = "wasm-deterministic"))] +use ::libsecp256k1::{Error as LibSecp256k1Error, Message as LibSecp256k1Message}; use serde::de::{Deserialize, Error as de_Error}; use serde::Serialize; @@ -31,10 +30,9 @@ use super::MessageSignature; use crate::types::{PrivateKey, PublicKey}; use crate::util::hash::{hex_bytes, to_hex, Sha256Sum}; -// per-thread Secp256k1 context -thread_local!(static _secp256k1: Secp256k1 = Secp256k1::new()); +pub const PUBLIC_KEY_SIZE: usize = 33; -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Hash)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct Secp256k1PublicKey { // serde is broken for secp256k1, so do it ourselves #[serde( @@ -45,7 +43,14 @@ pub struct Secp256k1PublicKey { compressed: bool, } -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +impl std::hash::Hash for Secp256k1PublicKey { + fn hash(&self, state: &mut H) { + self.key.serialize_compressed().hash(state); + self.compressed.hash(state); + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct Secp256k1PrivateKey { // serde is broken for secp256k1, so do it ourselves #[serde( @@ -63,43 +68,73 @@ impl MessageSignature { } #[cfg(any(test, feature = "testing"))] - // test method for generating place-holder data pub fn from_raw(sig: &[u8]) -> MessageSignature { let mut buf = [0u8; 65]; if sig.len() < 65 { - buf.copy_from_slice(sig); + buf[..sig.len()].copy_from_slice(sig); } else { buf.copy_from_slice(&sig[..65]); } MessageSignature(buf) } - pub fn from_secp256k1_recoverable(sig: &LibSecp256k1RecoverableSignature) -> MessageSignature { - let (recid, bytes) = sig.serialize_compact(); + pub fn from_secp256k1_recoverable( + sig: &LibSecp256k1Signature, + recid: LibSecp256k1RecoveryId, + ) -> MessageSignature { + let bytes = sig.serialize(); let mut ret_bytes = [0u8; 65]; - let recovery_id_byte = recid.to_i32() as u8; // recovery ID will be 0, 1, 2, or 3 - ret_bytes[0] = recovery_id_byte; + ret_bytes[0] = recid.serialize(); ret_bytes[1..=64].copy_from_slice(&bytes[..64]); MessageSignature(ret_bytes) } - pub fn to_secp256k1_recoverable(&self) -> Option { - let recid = match LibSecp256k1RecoveryID::from_i32(self.0[0] as i32) { - Ok(rid) => rid, - Err(_) => { - return None; - } - }; - let mut sig_bytes = [0u8; 64]; - sig_bytes[..64].copy_from_slice(&self.0[1..=64]); - - LibSecp256k1RecoverableSignature::from_compact(&sig_bytes, recid).ok() + pub fn to_secp256k1_recoverable( + &self, + ) -> Option<(LibSecp256k1Signature, LibSecp256k1RecoveryId)> { + let recovery_id = LibSecp256k1RecoveryId::parse(self.0[0]).ok()?; + let signature = LibSecp256k1Signature::parse_standard_slice(&self.0[1..65]).ok()?; + Some((signature, recovery_id)) } /// Convert from VRS to RSV pub fn to_rsv(&self) -> Vec { [&self.0[1..], &self.0[0..1]].concat() } + + /// DER-encode the non-recoverable portion of this signature. + /// Returns None if the signature bytes are malformed. + pub fn to_der_signature(&self) -> Option> { + let (sig, _) = self.to_secp256k1_recoverable()?; + Some(secp256k1_der_encode(&sig.serialize())) + } +} + +/// Encode a 64-byte compact ECDSA signature (r||s, big-endian) in DER format. +pub fn secp256k1_der_encode(compact: &[u8; 64]) -> Vec { + fn encode_int(n: &[u8]) -> Vec { + let start = n.iter().position(|&b| b != 0).unwrap_or(n.len()); + let n = &n[start..]; + let needs_pad = !n.is_empty() && (n[0] & 0x80 != 0); + let int_len = n.len() + usize::from(needs_pad); + let mut v = Vec::with_capacity(int_len + 2); + v.push(0x02); + v.push(int_len as u8); + if needs_pad { + v.push(0x00); + } + v.extend_from_slice(n); + v + } + + let r = encode_int(&compact[..32]); + let s = encode_int(&compact[32..]); + let mut out = Vec::with_capacity(r.len() + s.len() + 2); + out.push(0x30); + out.push((r.len() + s.len()) as u8); + out.extend(r); + out.extend(s); + out } #[cfg(any(test, feature = "testing"))] @@ -121,23 +156,24 @@ impl Secp256k1PublicKey { } pub fn from_slice(data: &[u8]) -> Result { - match LibSecp256k1PublicKey::from_slice(data) { - Ok(pubkey_res) => Ok(Secp256k1PublicKey { - key: pubkey_res, - compressed: data.len() == LibSecp256k1Constants::PUBLIC_KEY_SIZE, - }), - Err(_e) => Err("Invalid public key: failed to load"), - } + let (format, compressed) = if data.len() == PUBLIC_KEY_SIZE { + (libsecp256k1::PublicKeyFormat::Compressed, true) + } else { + (libsecp256k1::PublicKeyFormat::Full, false) + }; + LibSecp256k1PublicKey::parse_slice(data, Some(format)) + .map(|key| Secp256k1PublicKey { key, compressed }) + .map_err(|_e| "Invalid public key: failed to load") } + #[cfg(not(feature = "wasm-deterministic"))] pub fn from_private(privk: &Secp256k1PrivateKey) -> Secp256k1PublicKey { - _secp256k1.with(|ctx| { - let pubk = LibSecp256k1PublicKey::from_secret_key(ctx, &privk.key); - Secp256k1PublicKey { - key: pubk, - compressed: privk.compress_public, - } - }) + let key = + LibSecp256k1PublicKey::from_secret_key_with_context(&privk.key, &ECMULT_GEN_CONTEXT); + Secp256k1PublicKey { + key, + compressed: privk.compress_public, + } } pub fn to_hex(&self) -> String { @@ -145,7 +181,7 @@ impl Secp256k1PublicKey { } pub fn to_bytes_compressed(&self) -> Vec { - self.key.serialize().to_vec() + self.key.serialize_compressed().to_vec() } pub fn compressed(&self) -> bool { @@ -156,88 +192,67 @@ impl Secp256k1PublicKey { self.compressed = value; } + #[cfg(not(feature = "wasm-deterministic"))] /// recover message and signature to public key (will be compressed) pub fn recover_to_pubkey( msg: &[u8], sig: &MessageSignature, ) -> Result { - _secp256k1.with(|ctx| { - let msg = LibSecp256k1Message::from_slice(msg).map_err(|_e| { - "Invalid message: failed to decode data hash: must be a 32-byte hash" - })?; - - let secp256k1_sig = sig - .to_secp256k1_recoverable() - .ok_or("Invalid signature: failed to decode recoverable signature")?; - - let recovered_pubkey = ctx - .recover_ecdsa(&msg, &secp256k1_sig) - .map_err(|_e| "Invalid signature: failed to recover public key")?; - - Ok(Secp256k1PublicKey { - key: recovered_pubkey, - compressed: true, - }) - }) - } - - // for benchmarking - #[cfg(test)] - pub fn recover_benchmark( - msg: &LibSecp256k1Message, - sig: &LibSecp256k1RecoverableSignature, - ) -> Result { - _secp256k1.with(|ctx| { - ctx.recover_ecdsa(msg, sig) - .map_err(|_e| "Invalid signature: failed to recover public key") - }) + // secp256k1_recover expects RSV order; MessageSignature is stored as VRS + let secp256k1_sig = secp256k1_recover(msg, &sig.to_rsv()) + .map_err(|_e| "Invalid signature: failed to recover public key")?; + Secp256k1PublicKey::from_slice(&secp256k1_sig) } } impl PublicKey for Secp256k1PublicKey { fn to_bytes(&self) -> Vec { if self.compressed { - self.key.serialize().to_vec() + self.key.serialize_compressed().to_vec() } else { - self.key.serialize_uncompressed().to_vec() + self.key.serialize().to_vec() } } - fn verify(&self, data_hash: &[u8], sig: &MessageSignature) -> Result { - _secp256k1.with(|ctx| { - let msg = LibSecp256k1Message::from_slice(data_hash).map_err(|_e| { - "Invalid message: failed to decode data hash: must be a 32-byte hash" - })?; - - let secp256k1_sig = sig - .to_secp256k1_recoverable() - .ok_or("Invalid signature: failed to decode recoverable signature")?; - - let recovered_pubkey = ctx - .recover_ecdsa(&msg, &secp256k1_sig) - .map_err(|_e| "Invalid signature: failed to recover public key")?; - - if recovered_pubkey != self.key { - test_debug!("{:?} != {:?}", &recovered_pubkey, &self.key); - return Ok(false); - } + #[cfg(feature = "wasm-deterministic")] + fn verify(&self, _data_hash: &[u8], _sig: &MessageSignature) -> Result { + Err("Not implemented for wasm-deterministic") + } - // NOTE: libsecp256k1 _should_ ensure that the S is low, - // but add this check just to be safe. - let secp256k1_sig_standard = secp256k1_sig.to_standard(); + #[cfg(not(feature = "wasm-deterministic"))] + fn verify(&self, data_hash: &[u8], sig: &MessageSignature) -> Result { + let recovered = Secp256k1PublicKey::recover_to_pubkey(data_hash, sig)?; + if recovered.key != self.key { + test_debug!("{:?} != {:?}", &recovered.key, &self.key); + return Ok(false); + } - // must be low-S - let mut secp256k1_sig_low_s = secp256k1_sig_standard; - secp256k1_sig_low_s.normalize_s(); - if secp256k1_sig_low_s != secp256k1_sig_standard { - return Err("Invalid signature: high-S"); - } + // NOTE: libsecp256k1 _should_ ensure that the S is low, + // but add this check just to be safe. + let (standard_sig, _) = sig + .to_secp256k1_recoverable() + .ok_or("Invalid signature: failed to decode recoverable signature")?; + if !is_low_s(&standard_sig) { + return Err("Invalid signature: high-S"); + } - Ok(true) - }) + Ok(true) } } +/// Returns true if the signature's S value is in the lower half of the secp256k1 group order. +#[cfg(not(feature = "wasm-deterministic"))] +fn is_low_s(sig: &LibSecp256k1Signature) -> bool { + // secp256k1 group order n divided by 2 (big-endian) + const HALF_ORDER: [u8; 32] = [ + 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x5d, 0x57, 0x6e, 0x73, 0x57, 0xa4, 0x50, 0x1d, 0xdf, 0xe9, 0x2f, 0x46, 0x68, + 0x1b, 0x20, 0xa0, + ]; + let bytes = sig.serialize(); + bytes[32..] <= HALF_ORDER[..] +} + impl Secp256k1PrivateKey { #[cfg(feature = "rand")] pub fn random() -> Secp256k1PrivateKey { @@ -248,17 +263,11 @@ impl Secp256k1PrivateKey { // keep trying to generate valid bytes let mut random_32_bytes = [0u8; 32]; rng.fill_bytes(&mut random_32_bytes); - let pk_res = LibSecp256k1PrivateKey::from_slice(&random_32_bytes); - match pk_res { - Ok(pk) => { - return Secp256k1PrivateKey { - key: pk, - compress_public: true, - }; - } - Err(_) => { - continue; - } + if let Ok(pk) = LibSecp256k1PrivateKey::parse_slice(&random_32_bytes) { + return Secp256k1PrivateKey { + key: pk, + compress_public: true, + }; } } } @@ -305,13 +314,12 @@ impl Secp256k1PrivateKey { } else { false }; - match LibSecp256k1PrivateKey::from_slice(&data[0..32]) { - Ok(privkey_res) => Ok(Secp256k1PrivateKey { - key: privkey_res, + LibSecp256k1PrivateKey::parse_slice(&data[0..32]) + .map(|key| Secp256k1PrivateKey { + key, compress_public, - }), - Err(_e) => Err("Invalid private key: failed to load"), - } + }) + .map_err(|_e| "Invalid private key: failed to load") } pub fn compress_public(&self) -> bool { @@ -323,52 +331,74 @@ impl Secp256k1PrivateKey { } pub fn to_hex(&self) -> String { - let mut bytes = self.key[..].to_vec(); + let mut bytes = self.key.serialize().to_vec(); if self.compress_public { bytes.push(1); } to_hex(&bytes) } - - pub fn as_slice(&self) -> &[u8; 32] { - self.key.as_ref() - } } impl PrivateKey for Secp256k1PrivateKey { fn to_bytes(&self) -> Vec { - let mut bits = self.key[..].to_vec(); + let mut bits = self.key.serialize().to_vec(); if self.compress_public { bits.push(0x01); } bits } + #[cfg(feature = "wasm-deterministic")] + fn sign(&self, _data_hash: &[u8]) -> Result { + Err("Not implemented for wasm-deterministic") + } + + #[cfg(not(feature = "wasm-deterministic"))] fn sign(&self, data_hash: &[u8]) -> Result { - _secp256k1.with(|ctx| { - let msg = LibSecp256k1Message::from_slice(data_hash).map_err(|_e| { - "Invalid message: failed to decode data hash: must be a 32-byte hash" - })?; + let message = LibSecp256k1Message::parse_slice(data_hash).map_err(|_e| { + "Invalid message: failed to decode data hash: must be a 32-byte hash" + })?; + let (sig, recid) = libsecp256k1::sign(&message, &self.key); + Ok(MessageSignature::from_secp256k1_recoverable(&sig, recid)) + } - let sig = ctx.sign_ecdsa_recoverable(&msg, &self.key); - Ok(MessageSignature::from_secp256k1_recoverable(&sig)) - }) + #[cfg(all(feature = "wasm-deterministic", any(test, feature = "testing")))] + fn sign_with_noncedata( + &self, + _data_hash: &[u8], + _noncedata: &[u8; 32], + ) -> Result { + Err("Not implemented for wasm-deterministic") } - #[cfg(any(test, feature = "testing"))] + #[cfg(all(any(test, feature = "testing"), not(feature = "wasm-deterministic")))] fn sign_with_noncedata( &self, data_hash: &[u8], noncedata: &[u8; 32], ) -> Result { - _secp256k1.with(|ctx| { - let msg = LibSecp256k1Message::from_slice(data_hash).map_err(|_e| { - "Invalid message: failed to decode data hash: must be a 32-byte hash" - })?; + let message = LibSecp256k1Message::parse_slice(data_hash).map_err(|_e| { + "Invalid message: failed to decode data hash: must be a 32-byte hash" + })?; + let mut nonce = Scalar::default(); + let _ = nonce.set_b32(noncedata); + + // we need this as the key raw data are private + let mut key = Scalar::default(); + let _ = key.set_b32(&self.key.serialize()); + + let (sigr, sigs, recid) = match ECMULT_GEN_CONTEXT.sign_raw(&key, &message.0, &nonce) { + Ok(result) => result, + Err(_) => return Err("unable to sign message"), + }; - let sig = ctx.sign_ecdsa_recoverable_with_noncedata(&msg, &self.key, noncedata); - Ok(MessageSignature::from_secp256k1_recoverable(&sig)) - }) + let recid = match LibSecp256k1RecoveryId::parse(recid) { + Ok(recid) => recid, + Err(_) => return Err("invalid recovery id"), + }; + + let sig = LibSecp256k1Signature { r: sigr, s: sigs }; + Ok(MessageSignature::from_secp256k1_recoverable(&sig, recid)) } } @@ -376,7 +406,7 @@ fn secp256k1_pubkey_serialize( pubk: &LibSecp256k1PublicKey, s: S, ) -> Result { - let key_hex = to_hex(&pubk.serialize()); + let key_hex = to_hex(&pubk.serialize_compressed()); s.serialize_str(key_hex.as_str()) } @@ -386,14 +416,14 @@ fn secp256k1_pubkey_deserialize<'de, D: serde::Deserializer<'de>>( let key_hex = String::deserialize(d)?; let key_bytes = hex_bytes(&key_hex).map_err(de_Error::custom)?; - LibSecp256k1PublicKey::from_slice(&key_bytes).map_err(de_Error::custom) + LibSecp256k1PublicKey::parse_slice(&key_bytes[..], None).map_err(de_Error::custom) } fn secp256k1_privkey_serialize( privk: &LibSecp256k1PrivateKey, s: S, ) -> Result { - let key_hex = to_hex(&privk[..]); + let key_hex = to_hex(&privk.serialize()); s.serialize_str(key_hex.as_str()) } @@ -403,45 +433,129 @@ fn secp256k1_privkey_deserialize<'de, D: serde::Deserializer<'de>>( let key_hex = String::deserialize(d)?; let key_bytes = hex_bytes(&key_hex).map_err(de_Error::custom)?; - LibSecp256k1PrivateKey::from_slice(&key_bytes[..]).map_err(de_Error::custom) + LibSecp256k1PrivateKey::parse_slice(&key_bytes[..]).map_err(de_Error::custom) } +#[cfg(not(feature = "wasm-deterministic"))] pub fn secp256k1_recover( message_arr: &[u8], serialized_signature_arr: &[u8], ) -> Result<[u8; 33], LibSecp256k1Error> { - _secp256k1.with(|ctx| { - let message = LibSecp256k1Message::from_slice(message_arr)?; - - let rec_id = LibSecp256k1RecoveryID::from_i32(serialized_signature_arr[64] as i32)?; - let recovered_sig = LibSecp256k1RecoverableSignature::from_compact( - &serialized_signature_arr[..64], - rec_id, - )?; - let recovered_pub = ctx.recover_ecdsa(&message, &recovered_sig)?; - let recovered_serialized = recovered_pub.serialize(); // 33 bytes version - - Ok(recovered_serialized) - }) + let recovery_id = libsecp256k1::RecoveryId::parse(serialized_signature_arr[64] as u8)?; + let message = LibSecp256k1Message::parse_slice(message_arr)?; + let signature = + LibSecp256k1Signature::parse_standard_slice(&serialized_signature_arr[..64])?; + let recovered_pub = libsecp256k1::recover(&message, &signature, &recovery_id)?; + Ok(recovered_pub.serialize_compressed()) } +#[cfg(not(feature = "wasm-deterministic"))] pub fn secp256k1_verify( message_arr: &[u8], serialized_signature_arr: &[u8], pubkey_arr: &[u8], ) -> Result<(), LibSecp256k1Error> { - _secp256k1.with(|ctx| { - let message = LibSecp256k1Message::from_slice(message_arr)?; - let expanded_sig = LibSecp256k1Signature::from_compact(&serialized_signature_arr[..64])?; // ignore 65th byte if present - let pubkey = LibSecp256k1PublicKey::from_slice(pubkey_arr)?; - ctx.verify_ecdsa(&message, &expanded_sig, &pubkey) - }) + let message = LibSecp256k1Message::parse_slice(message_arr)?; + let signature = + LibSecp256k1Signature::parse_standard_slice(&serialized_signature_arr[..64])?; // ignore 65th byte if present + let pubkey = LibSecp256k1PublicKey::parse_slice( + pubkey_arr, + Some(libsecp256k1::PublicKeyFormat::Compressed), + )?; + // Reject high-S signatures to prevent malleability (consistent with Bitcoin secp256k1) + if !is_low_s(&signature) { + return Err(LibSecp256k1Error::InvalidSignature); + } + if libsecp256k1::verify(&message, &signature, &pubkey) { + Ok(()) + } else { + Err(LibSecp256k1Error::InvalidSignature) + } } #[cfg(test)] mod tests { use rand::RngCore as _; - use secp256k1::{self, PublicKey as LibSecp256k1PublicKey, Secp256k1}; + + /// Negate a secp256k1 scalar: returns `n - s` (mod n), giving the complementary S value. + /// Negating S and flipping the recovery-id bit produces a valid signature for the same + /// (message, key) pair that has the opposite S parity. + fn negate_secp256k1_scalar(s: &[u8; 32]) -> [u8; 32] { + // secp256k1 group order n + const N: [u8; 32] = [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFE, 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C, + 0xD0, 0x36, 0x41, 0x41, + ]; + let mut result = [0u8; 32]; + let mut borrow: i32 = 0; + for i in (0..32).rev() { + let diff = N[i] as i32 - s[i] as i32 - borrow; + result[i] = diff.rem_euclid(256) as u8; + borrow = if diff < 0 { 1 } else { 0 }; + } + result + } + + #[test] + fn test_recover_high_s() { + // Sign a message, convert the signature to its high-S equivalent, and verify that + // secp256k1_recover returns the original public key. This is the invariant that + // allows secp256k1-recover? to work with high-S signatures. + use crate::util::hash::Sha256Sum; + + let privk = Secp256k1PrivateKey::from_seed(b"test-recover-high-s"); + let pubk = Secp256k1PublicKey::from_private(&privk); + let msg = Sha256Sum::from_data(b"hello world"); + + let sig = privk.sign(msg.as_bytes()).expect("sign should succeed"); + let (low_sig, recid) = sig + .to_secp256k1_recoverable() + .expect("signature must be parseable"); + let compact = low_sig.serialize(); // [r (32) || s (32)] + + // Build the complementary high-S RSV form: + // s_complement = n - s (always has the opposite S-parity) + // recovery_id = old_id XOR 1 (flips the y-parity bit of R) + let s_comp = negate_secp256k1_scalar(compact[32..].try_into().unwrap()); + + // Confirm the complement is actually high-S (sanity: exactly one of s / s_comp is high) + const HALF_ORDER: [u8; 32] = [ + 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x5d, 0x57, 0x6e, 0x73, 0x57, 0xa4, 0x50, 0x1d, 0xdf, 0xe9, 0x2f, 0x46, + 0x68, 0x1b, 0x20, 0xa0, + ]; + let orig_is_low = compact[32..] <= HALF_ORDER[..]; + let comp_is_high = s_comp > HALF_ORDER; + assert_eq!( + orig_is_low, comp_is_high, + "exactly one of (s, n-s) must be high-S" + ); + + // Use whichever form is high-S + let (high_s, high_v) = if comp_is_high { + (s_comp, recid.serialize() ^ 1) + } else { + // original s is already high-S + let mut orig = [0u8; 32]; + orig.copy_from_slice(&compact[32..]); + (orig, recid.serialize()) + }; + + let mut sig_rsv = [0u8; 65]; + sig_rsv[..32].copy_from_slice(&compact[..32]); // R + sig_rsv[32..64].copy_from_slice(&high_s); // high-S + sig_rsv[64] = high_v; // recovery id + + let recovered = super::secp256k1_recover(msg.as_bytes(), &sig_rsv) + .expect("secp256k1_recover must succeed for high-S"); + + assert_eq!( + recovered.to_vec(), + pubk.to_bytes_compressed(), + "high-S recovery must return the original signer's public key" + ); + } use super::*; use crate::util::get_epoch_time_ms; @@ -516,21 +630,32 @@ mod tests { #[test] fn test_parse_serialize() { - let ctx: Secp256k1 = Secp256k1::new(); let fixtures = vec![ KeyFixture { input: "0233d78f74de8ef4a1de815b6d5c5c129c073786305c0826c499b1811c9a12cee5", result: Some(Secp256k1PublicKey { - key: LibSecp256k1PublicKey::from_slice(&hex_bytes("0233d78f74de8ef4a1de815b6d5c5c129c073786305c0826c499b1811c9a12cee5").unwrap()[..]).unwrap(), - compressed: true - }) + key: LibSecp256k1PublicKey::parse_slice( + &hex_bytes( + "0233d78f74de8ef4a1de815b6d5c5c129c073786305c0826c499b1811c9a12cee5", + ) + .unwrap()[..], + Some(libsecp256k1::PublicKeyFormat::Compressed), + ) + .unwrap(), + compressed: true, + }), }, KeyFixture { input: "044a83ad59dbae1e2335f488dbba5f8604d00f612a43ebaae784b5b7124cc38c3aaf509362787e1a8e25131724d57fec81b87889aabb4edf7bd89f5c4daa4f8aa7", result: Some(Secp256k1PublicKey { - key: LibSecp256k1PublicKey::from_slice(&hex_bytes("044a83ad59dbae1e2335f488dbba5f8604d00f612a43ebaae784b5b7124cc38c3aaf509362787e1a8e25131724d57fec81b87889aabb4edf7bd89f5c4daa4f8aa7").unwrap()[..]).unwrap(), - compressed: false - }) + key: LibSecp256k1PublicKey::parse_slice( + &hex_bytes("044a83ad59dbae1e2335f488dbba5f8604d00f612a43ebaae784b5b7124cc38c3aaf509362787e1a8e25131724d57fec81b87889aabb4edf7bd89f5c4daa4f8aa7") + .unwrap()[..], + Some(libsecp256k1::PublicKeyFormat::Full), + ) + .unwrap(), + compressed: false, + }), }, KeyFixture { input: "0233d78f74de8ef4a1de815b6d5c5c129c073786305c0826c499b1811c9a12ce", @@ -539,7 +664,7 @@ mod tests { KeyFixture { input: "044a83ad59dbae1e2335f488dbba5f8604d00f612a43ebaae784b5b7124cc38c3aaf509362787e1a8e25131724d57fec81b87889aabb4edf7bd89f5c4daa4f8a", result: None, - } + }, ]; for fixture in fixtures { @@ -558,8 +683,6 @@ mod tests { } (Err(_e), None) => {} (_, _) => { - // either got a key when we didn't expect one, or didn't get a key when we did - // expect one. panic!("Unexpected result: we either got a key when we didn't expect one, or didn't get a key when we did expect one."); } } @@ -568,8 +691,7 @@ mod tests { #[test] fn test_verify() { - let _ctx: Secp256k1 = Secp256k1::new(); - let fixtures : Vec>> = vec![ + let fixtures: Vec>> = vec![ VerifyFixture { public_key: "0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219", signature: "00354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb445b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", @@ -653,7 +775,7 @@ mod tests { let mut runtime_recover = 0; let mut rng = rand::thread_rng(); - for i in 0..100 { + for _i in 0..100 { let privk = Secp256k1PrivateKey::random(); let pubk = Secp256k1PublicKey::from_private(&privk); @@ -661,29 +783,22 @@ mod tests { rng.fill_bytes(&mut msg); let sign_start = get_epoch_time_ms(); - for i in 0..1000 { - let sig = privk.sign(&msg).unwrap(); + for _j in 0..1000 { + let _sig = privk.sign(&msg).unwrap(); } let sign_end = get_epoch_time_ms(); let sig = privk.sign(&msg).unwrap(); - let secp256k1_msg = LibSecp256k1Message::from_slice(&msg).unwrap(); - let secp256k1_sig = sig.to_secp256k1_recoverable().unwrap(); - - let recovered_pubk = - Secp256k1PublicKey::recover_benchmark(&secp256k1_msg, &secp256k1_sig).unwrap(); - assert_eq!(recovered_pubk, pubk.key); let recover_start = get_epoch_time_ms(); - for i in 0..1000 { - let recovered_pubk = - Secp256k1PublicKey::recover_benchmark(&secp256k1_msg, &secp256k1_sig).unwrap(); + for _j in 0..1000 { + let _recovered_pubk = Secp256k1PublicKey::recover_to_pubkey(&msg, &sig).unwrap(); } let recover_end = get_epoch_time_ms(); let verify_start = get_epoch_time_ms(); - for i in 0..1000 { - let valid = pubk.verify(&msg, &sig).unwrap(); + for _j in 0..1000 { + let _valid = pubk.verify(&msg, &sig).unwrap(); } let verify_end = get_epoch_time_ms(); @@ -710,4 +825,441 @@ mod tests { runtime_verify - runtime_recover ); } + + // ----------------------------------------------------------------------- + // MessageSignature::empty + // ----------------------------------------------------------------------- + + #[test] + fn test_message_signature_empty() { + let sig = MessageSignature::empty(); + // empty() is the all-zeros sentinel; recovery must fail because (r=0, s=0) + // is not a valid ECDSA signature for any message. + assert_eq!(sig.0, [0u8; 65]); + let rsv = sig.to_rsv(); + assert_eq!(rsv[64], 0, "recovery ID of empty() must be 0"); + assert!( + rsv[..64].iter().all(|&b| b == 0), + "RS of empty() must be all zeros" + ); + // secp256k1_recover must fail for the zero (r, s) pair + assert!( + super::secp256k1_recover(&[0x11u8; 32], &rsv).is_err(), + "recovery on empty() must fail" + ); + } + + // ----------------------------------------------------------------------- + // MessageSignature: from_secp256k1_recoverable / to_secp256k1_recoverable + // ----------------------------------------------------------------------- + + #[test] + fn test_message_signature_recoverable_roundtrip() { + let privk = Secp256k1PrivateKey::random(); + let msg = [0x11u8; 32]; + let sig = privk.sign(&msg).expect("sign should succeed"); + + // to_secp256k1_recoverable must parse the VRS bytes correctly + let (recovered_sig, recovered_recid) = sig + .to_secp256k1_recoverable() + .expect("to_secp256k1_recoverable must succeed for a freshly signed message"); + + // round-trip: re-build MessageSignature and compare bytes + let rebuilt = MessageSignature::from_secp256k1_recoverable(&recovered_sig, recovered_recid); + assert_eq!( + sig.as_bytes(), + rebuilt.as_bytes(), + "from_secp256k1_recoverable/to_secp256k1_recoverable round-trip must be identity" + ); + } + + // ----------------------------------------------------------------------- + // MessageSignature::to_rsv + // ----------------------------------------------------------------------- + + #[test] + fn test_message_signature_to_rsv() { + // Known VRS vector: V=00, R=354445...cb44, S=5b97...3fe6 + let vrs = hex_bytes( + "00354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb44\ + 5b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", + ) + .unwrap(); + let sig = MessageSignature::from_raw(&vrs); + let rsv = sig.to_rsv(); + + assert_eq!(rsv.len(), 65); + // First 64 bytes of RSV == bytes 1..65 of VRS (the RS part) + assert_eq!(&rsv[..64], &vrs[1..65]); + // Last byte of RSV == byte 0 of VRS (the recovery ID) + assert_eq!(rsv[64], vrs[0]); + } + + // ----------------------------------------------------------------------- + // secp256k1_der_encode / MessageSignature::to_der_signature + // ----------------------------------------------------------------------- + + #[test] + fn test_secp256k1_der_encode_no_padding() { + // R and S both start with a byte < 0x80 — no zero-padding needed. + // R: 354445...cb44 (first byte 0x35) + // S: 5b97...3fe6 (first byte 0x5b) + let rs = hex_bytes( + "354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb44\ + 5b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", + ) + .unwrap(); + let compact: &[u8; 64] = rs.as_slice().try_into().unwrap(); + let der = super::secp256k1_der_encode(compact); + + // Structure: 30 02 02 + assert_eq!(der[0], 0x30, "SEQUENCE tag"); + let total_inner = der[1] as usize; + assert_eq!(total_inner + 2, der.len(), "outer length consistent"); + + assert_eq!(der[2], 0x02, "R INTEGER tag"); + let r_len = der[3] as usize; + assert_eq!(&der[4..4 + r_len], &rs[..32], "R value"); + + let s_offset = 4 + r_len; + assert_eq!(der[s_offset], 0x02, "S INTEGER tag"); + let s_len = der[s_offset + 1] as usize; + assert_eq!(&der[s_offset + 2..], &rs[32..], "S value"); + + // Both are 32 bytes with no leading-zero padding + assert_eq!(r_len, 32); + assert_eq!(s_len, 32); + assert_eq!(der.len(), 70); // 2 + 2 + 32 + 2 + 32 + } + + #[test] + fn test_secp256k1_der_encode_with_padding() { + // R starts with 0xff (>= 0x80) → needs a 0x00 prefix in DER. + // S is the integer 1: stored as [0x00, 0x00, …, 0x00, 0x01] (leading zeros + // are stripped to produce a single-byte DER INTEGER 0x01). + let mut compact = [0u8; 64]; + compact[0] = 0xff; // R high-bit set + compact[63] = 0x01; // S = 1 (only the last byte is non-zero) + + let der = super::secp256k1_der_encode(&compact); + + assert_eq!(der[0], 0x30); + assert_eq!(der[2], 0x02); // R tag + let r_len = der[3] as usize; + assert_eq!(r_len, 33, "R must be padded to 33 bytes"); + assert_eq!(der[4], 0x00, "leading zero pad for R"); + assert_eq!(der[5], 0xff, "first real R byte"); + + let s_offset = 4 + r_len; + assert_eq!(der[s_offset], 0x02); // S tag + let s_len = der[s_offset + 1] as usize; + assert_eq!(s_len, 1, "leading zeros stripped: S=1 encodes as 1 byte"); + assert_eq!(der[s_offset + 2], 0x01, "S value"); + } + + #[test] + fn test_to_der_signature_matches_der_encode() { + let privk = Secp256k1PrivateKey::random(); + let msg = [0x22u8; 32]; + let sig = privk.sign(&msg).expect("sign should succeed"); + + // to_der_signature must produce the same bytes as manually calling secp256k1_der_encode + let der_via_method = sig + .to_der_signature() + .expect("to_der_signature must succeed for a valid signature"); + + let (raw_sig, _recid) = sig.to_secp256k1_recoverable().unwrap(); + let der_via_fn = super::secp256k1_der_encode(&raw_sig.serialize()); + + assert_eq!(der_via_method, der_via_fn); + + // Structural sanity: starts with 0x30 SEQUENCE tag + assert_eq!(der_via_method[0], 0x30); + assert!( + der_via_method.len() >= 70 && der_via_method.len() <= 72, + "DER-encoded secp256k1 sig is 70-72 bytes" + ); + } + + #[test] + fn test_to_der_signature_structure() { + // to_der_signature must produce a valid SEQUENCE header for any parsed signature. + let privk = Secp256k1PrivateKey::random(); + let sig = privk.sign(&[0x44u8; 32]).expect("sign must succeed"); + let der = sig.to_der_signature().expect("must return Some for valid sig"); + assert_eq!(der[0], 0x30, "DER SEQUENCE tag"); + assert_eq!(der.len(), der[1] as usize + 2, "DER length field consistent"); + } + + // ----------------------------------------------------------------------- + // Secp256k1PublicKey: to_bytes_compressed / compressed / set_compressed + // ----------------------------------------------------------------------- + + #[test] + fn test_pubkey_compression_flags() { + let privk = Secp256k1PrivateKey::random(); + let mut pubk = Secp256k1PublicKey::from_private(&privk); + + // from_private inherits the compress_public flag (default: true) + assert!(pubk.compressed()); + assert_eq!(pubk.to_bytes().len(), 33); + assert_eq!(pubk.to_bytes_compressed().len(), 33); + assert_eq!(pubk.to_bytes(), pubk.to_bytes_compressed()); + + pubk.set_compressed(false); + assert!(!pubk.compressed()); + assert_eq!(pubk.to_bytes().len(), 65); + // to_bytes_compressed must always return 33 bytes regardless of the flag + assert_eq!(pubk.to_bytes_compressed().len(), 33); + + // Re-enabling compression must restore the 33-byte output + pubk.set_compressed(true); + assert_eq!(pubk.to_bytes().len(), 33); + } + + // ----------------------------------------------------------------------- + // Secp256k1PublicKey::recover_to_pubkey + // ----------------------------------------------------------------------- + + #[test] + fn test_recover_to_pubkey() { + let privk = Secp256k1PrivateKey::random(); + let pubk = Secp256k1PublicKey::from_private(&privk); + let msg = [0x33u8; 32]; + + let sig = privk.sign(&msg).expect("sign should succeed"); + let recovered = Secp256k1PublicKey::recover_to_pubkey(&msg, &sig) + .expect("recover_to_pubkey must succeed"); + + assert_eq!( + recovered.to_bytes_compressed(), + pubk.to_bytes_compressed(), + "recovered key must equal the signer's public key" + ); + + // recovery with wrong message must give a different key (or fail) + let wrong_msg = [0x34u8; 32]; + let recovered_wrong = Secp256k1PublicKey::recover_to_pubkey(&wrong_msg, &sig) + .expect("recovery on a different message still succeeds (different key)"); + assert_ne!( + recovered_wrong.to_bytes_compressed(), + pubk.to_bytes_compressed(), + "recovery with wrong message must not return the original key" + ); + } + + // ----------------------------------------------------------------------- + // Secp256k1PrivateKey::from_slice / PrivateKey::to_bytes + // ----------------------------------------------------------------------- + + #[test] + fn test_private_key_from_slice_and_to_bytes() { + // 32-byte slice → uncompressed key + let raw = [0x12u8; 32]; + let sk = Secp256k1PrivateKey::from_slice(&raw).expect("32-byte slice must parse"); + assert!(!sk.compress_public()); + let bytes = sk.to_bytes(); + assert_eq!(bytes, raw, "to_bytes on uncompressed key must return the 32 raw bytes"); + + // 33-byte slice with 0x01 suffix → compressed key + let mut raw_comp = [0u8; 33]; + raw_comp[..32].copy_from_slice(&raw); + raw_comp[32] = 0x01; + let sk_comp = + Secp256k1PrivateKey::from_slice(&raw_comp).expect("33-byte slice with 0x01 must parse"); + assert!(sk_comp.compress_public()); + let bytes_comp = sk_comp.to_bytes(); + assert_eq!(bytes_comp.len(), 33); + assert_eq!(bytes_comp[32], 0x01); + + // 33-byte slice with non-0x01 suffix → error + let mut bad_suffix = raw_comp; + bad_suffix[32] = 0x02; + assert!( + Secp256k1PrivateKey::from_slice(&bad_suffix).is_err(), + "33-byte slice with non-0x01 suffix must fail" + ); + + // Too short → error + assert!( + Secp256k1PrivateKey::from_slice(&raw[..31]).is_err(), + "31-byte slice must fail" + ); + + // Too long → error + let too_long = [0x12u8; 34]; + assert!( + Secp256k1PrivateKey::from_slice(&too_long).is_err(), + "34-byte slice must fail" + ); + + // All-zero bytes are not a valid secret key + assert!( + Secp256k1PrivateKey::from_slice(&[0u8; 32]).is_err(), + "zero scalar must not be a valid private key" + ); + } + + // ----------------------------------------------------------------------- + // PrivateKey::sign (non-ignored, end-to-end) + // ----------------------------------------------------------------------- + + #[test] + fn test_sign_and_verify_roundtrip() { + let privk = Secp256k1PrivateKey::random(); + let pubk = Secp256k1PublicKey::from_private(&privk); + let msg = [0x55u8; 32]; + + let sig = privk.sign(&msg).expect("sign must succeed"); + + // Correct message → true + assert_eq!(pubk.verify(&msg, &sig), Ok(true)); + + // Tampered message → false + let mut bad_msg = msg; + bad_msg[0] ^= 0xff; + assert_eq!(pubk.verify(&bad_msg, &sig), Ok(false)); + + // Wrong key → false + let other_privk = Secp256k1PrivateKey::random(); + let other_pubk = Secp256k1PublicKey::from_private(&other_privk); + assert_eq!(other_pubk.verify(&msg, &sig), Ok(false)); + } + + // ----------------------------------------------------------------------- + // PrivateKey::sign_with_noncedata + // ----------------------------------------------------------------------- + + #[test] + fn test_sign_with_noncedata_deterministic() { + let privk = Secp256k1PrivateKey::random(); + let pubk = Secp256k1PublicKey::from_private(&privk); + let msg = [0x77u8; 32]; + let nonce = [0xaau8; 32]; + + let sig1 = privk + .sign_with_noncedata(&msg, &nonce) + .expect("sign_with_noncedata must succeed"); + let sig2 = privk + .sign_with_noncedata(&msg, &nonce) + .expect("second call must succeed"); + + // Same inputs → identical signatures + assert_eq!( + sig1.as_bytes(), + sig2.as_bytes(), + "sign_with_noncedata must be deterministic" + ); + + // The signature must verify correctly + assert_eq!( + pubk.verify(&msg, &sig1), + Ok(true), + "signature produced by sign_with_noncedata must verify" + ); + + // Different nonce → different signature + let other_nonce = [0xbbu8; 32]; + let sig3 = privk + .sign_with_noncedata(&msg, &other_nonce) + .expect("sign with different nonce must succeed"); + assert_ne!( + sig1.as_bytes(), + sig3.as_bytes(), + "different nonce must produce a different signature" + ); + } + + // ----------------------------------------------------------------------- + // secp256k1_verify + // ----------------------------------------------------------------------- + + #[test] + fn test_secp256k1_verify_function() { + // Use the same test vector as test_verify: + // pubkey = 0385f2e2... + // message = sha256("hello world") + // VRS sig = 00354445...3fe6 + let pubkey = + hex_bytes("0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219") + .unwrap(); + let message = + hex_bytes("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9") + .unwrap(); + // secp256k1_verify takes RS (first 64 bytes); recovery ID at byte 64 is ignored + let sig_rs = hex_bytes( + "354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb44\ + 5b97de5b632582d31704f86706a780886e6e381bfed65228267358262d203fe6", + ) + .unwrap(); + + // Valid signature → Ok + assert!( + super::secp256k1_verify(&message, &sig_rs, &pubkey).is_ok(), + "valid sig must verify" + ); + + // Wrong message → Err + let mut bad_msg = message.clone(); + bad_msg[0] ^= 0xff; + assert!( + super::secp256k1_verify(&bad_msg, &sig_rs, &pubkey).is_err(), + "wrong message must fail" + ); + + // Wrong pubkey → Err + let other_privk = Secp256k1PrivateKey::random(); + let other_pubk = Secp256k1PublicKey::from_private(&other_privk); + assert!( + super::secp256k1_verify(&message, &sig_rs, &other_pubk.to_bytes_compressed()).is_err(), + "wrong pubkey must fail" + ); + + // High-S signature → Err (low-S is enforced) + let high_s_sig = hex_bytes( + "54cd3f378a424a3e50ff1c911b7d80cf424e1b86dddecadbcf39077e62fa1e54\ + ee6514347c1608df2c3995e7356f2d60a1fab60878214642134d78cd923ce27a", + ) + .unwrap(); + let high_s_msg = + hex_bytes("89171d7815da4bc1f644665a3234bc99d1680afa0b3285eff4878f4275fbfa89") + .unwrap(); + let high_s_pubkey = + hex_bytes("0256b328b30c8bf5839e24058747879408bdb36241dc9c2e7c619faa12b2920967") + .unwrap(); + assert!( + super::secp256k1_verify(&high_s_msg, &high_s_sig, &high_s_pubkey).is_err(), + "high-S signature must be rejected by secp256k1_verify" + ); + } + + // ----------------------------------------------------------------------- + // Hash impl for Secp256k1PublicKey + // ----------------------------------------------------------------------- + + #[test] + fn test_pubkey_hash_usable_as_map_key() { + use std::collections::HashMap; + + let privk1 = Secp256k1PrivateKey::from_seed(b"key-one"); + let privk2 = Secp256k1PrivateKey::from_seed(b"key-two"); + let pubk1 = Secp256k1PublicKey::from_private(&privk1); + let pubk2 = Secp256k1PublicKey::from_private(&privk2); + + let mut map: HashMap = HashMap::new(); + map.insert(pubk1.clone(), 1); + map.insert(pubk2.clone(), 2); + + assert_eq!(map[&pubk1], 1); + assert_eq!(map[&pubk2], 2); + + // A key with a different compressed flag but same underlying point hashes differently + let mut pubk1_uncomp = pubk1.clone(); + pubk1_uncomp.set_compressed(false); + assert_ne!( + map.get(&pubk1_uncomp), + Some(&1), + "different compressed flag changes the hash" + ); + } } diff --git a/stacks-common/src/util/secp256k1/wasm.rs b/stacks-common/src/util/secp256k1/wasm.rs deleted file mode 100644 index 03414641485..00000000000 --- a/stacks-common/src/util/secp256k1/wasm.rs +++ /dev/null @@ -1,382 +0,0 @@ -// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020 Stacks Open Internet Foundation -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use ::libsecp256k1::curve::Scalar; -pub use ::libsecp256k1::Error; -use ::libsecp256k1::{ - self, PublicKey as LibSecp256k1PublicKey, RecoveryId as LibSecp256k1RecoveryId, - SecretKey as LibSecp256k1PrivateKey, Signature as LibSecp256k1Signature, ECMULT_GEN_CONTEXT, -}; -#[cfg(not(feature = "wasm-deterministic"))] -use ::libsecp256k1::{Error as LibSecp256k1Error, Message as LibSecp256k1Message}; -use serde::de::{Deserialize, Error as de_Error}; -use serde::Serialize; - -use super::MessageSignature; -use crate::types::{PrivateKey, PublicKey}; -use crate::util::hash::{hex_bytes, to_hex}; - -pub const PUBLIC_KEY_SIZE: usize = 33; - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct Secp256k1PublicKey { - // serde is broken for secp256k1, so do it ourselves - #[serde( - serialize_with = "secp256k1_pubkey_serialize", - deserialize_with = "secp256k1_pubkey_deserialize" - )] - key: LibSecp256k1PublicKey, - compressed: bool, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub struct Secp256k1PrivateKey { - // serde is broken for secp256k1, so do it ourselves - #[serde( - serialize_with = "secp256k1_privkey_serialize", - deserialize_with = "secp256k1_privkey_deserialize" - )] - key: LibSecp256k1PrivateKey, - compress_public: bool, -} - -impl Secp256k1PublicKey { - pub fn from_slice(data: &[u8]) -> Result { - let (format, compressed) = if data.len() == PUBLIC_KEY_SIZE { - (libsecp256k1::PublicKeyFormat::Compressed, true) - } else { - (libsecp256k1::PublicKeyFormat::Full, false) - }; - match LibSecp256k1PublicKey::parse_slice(data, Some(format)) { - Ok(pubkey_res) => Ok(Secp256k1PublicKey { - key: pubkey_res, - compressed, - }), - Err(_e) => Err("Invalid public key: failed to load"), - } - } - - pub fn to_hex(&self) -> String { - if self.compressed { - to_hex(&self.key.serialize_compressed().to_vec()) - } else { - to_hex(&self.key.serialize().to_vec()) - } - } - - pub fn to_bytes_compressed(&self) -> Vec { - self.key.serialize_compressed().to_vec() - } - - pub fn compressed(&self) -> bool { - self.compressed - } - - pub fn set_compressed(&mut self, value: bool) { - self.compressed = value; - } - - pub fn to_bytes(&self) -> Vec { - if self.compressed { - self.key.serialize_compressed().to_vec() - } else { - self.key.serialize().to_vec() - } - } - - pub fn from_hex(hex_string: &str) -> Result { - let data = hex_bytes(hex_string).map_err(|_e| "Failed to decode hex public key")?; - Secp256k1PublicKey::from_slice(&data[..]).map_err(|_e| "Invalid public key hex string") - } - - #[cfg(not(feature = "wasm-deterministic"))] - pub fn from_private(privk: &Secp256k1PrivateKey) -> Secp256k1PublicKey { - let key = - LibSecp256k1PublicKey::from_secret_key_with_context(&privk.key, &ECMULT_GEN_CONTEXT); - Secp256k1PublicKey { - key, - compressed: privk.compress_public, - } - } - - #[cfg(not(feature = "wasm-deterministic"))] - /// recover message and signature to public key (will be compressed) - pub fn recover_to_pubkey( - msg: &[u8], - sig: &MessageSignature, - ) -> Result { - let secp256k1_sig = secp256k1_recover(msg, sig.as_bytes()) - .map_err(|_e| "Invalid signature: failed to recover public key")?; - - Secp256k1PublicKey::from_slice(&secp256k1_sig) - } -} - -impl Secp256k1PrivateKey { - #[cfg(feature = "rand")] - pub fn new() -> Secp256k1PrivateKey { - use rand::RngCore as _; - - let mut rng = rand::thread_rng(); - loop { - // keep trying to generate valid bytes - let mut random_32_bytes = [0u8; 32]; - rng.fill_bytes(&mut random_32_bytes); - let pk_res = LibSecp256k1PrivateKey::parse_slice(&random_32_bytes); - match pk_res { - Ok(pk) => { - return Secp256k1PrivateKey { - key: pk, - compress_public: true, - }; - } - Err(_) => { - continue; - } - } - } - } - - pub fn from_slice(data: &[u8]) -> Result { - if data.len() < 32 { - return Err("Invalid private key: shorter than 32 bytes"); - } - if data.len() > 33 { - return Err("Invalid private key: greater than 33 bytes"); - } - let compress_public = if data.len() == 33 { - // compressed byte tag? - if data[32] != 0x01 { - return Err("Invalid private key: invalid compressed byte marker"); - } - true - } else { - false - }; - - match LibSecp256k1PrivateKey::parse_slice(&data[0..32]) { - Ok(privkey_res) => Ok(Secp256k1PrivateKey { - key: privkey_res, - compress_public, - }), - Err(_e) => Err("Invalid private key: failed to load"), - } - } - - pub fn from_hex(hex_string: &str) -> Result { - let data = hex_bytes(hex_string).map_err(|_e| "Failed to decode hex private key")?; - Secp256k1PrivateKey::from_slice(&data[..]).map_err(|_e| "Invalid private key hex string") - } - - pub fn compress_public(&self) -> bool { - self.compress_public - } - - pub fn set_compress_public(&mut self, value: bool) { - self.compress_public = value; - } -} - -#[cfg(not(feature = "wasm-deterministic"))] -pub fn secp256k1_recover( - message_arr: &[u8], - serialized_signature: &[u8], -) -> Result<[u8; 33], LibSecp256k1Error> { - let recovery_id = libsecp256k1::RecoveryId::parse(serialized_signature[64] as u8)?; - let message = LibSecp256k1Message::parse_slice(message_arr)?; - let signature = LibSecp256k1Signature::parse_standard_slice(&serialized_signature[..64])?; - let recovered_pub_key = libsecp256k1::recover(&message, &signature, &recovery_id)?; - Ok(recovered_pub_key.serialize_compressed()) -} - -#[cfg(not(feature = "wasm-deterministic"))] -pub fn secp256k1_verify( - message_arr: &[u8], - serialized_signature: &[u8], - pubkey_arr: &[u8], -) -> Result<(), LibSecp256k1Error> { - let message = LibSecp256k1Message::parse_slice(message_arr)?; - let signature = LibSecp256k1Signature::parse_standard_slice(&serialized_signature[..64])?; // ignore 65th byte if present - let pubkey = LibSecp256k1PublicKey::parse_slice( - pubkey_arr, - Some(libsecp256k1::PublicKeyFormat::Compressed), - )?; - - let res = libsecp256k1::verify(&message, &signature, &pubkey); - if res { - Ok(()) - } else { - Err(LibSecp256k1Error::InvalidPublicKey) - } -} - -fn secp256k1_pubkey_serialize( - pubk: &LibSecp256k1PublicKey, - s: S, -) -> Result { - let key_hex = to_hex(&pubk.serialize().to_vec()); - s.serialize_str(&key_hex.as_str()) -} - -fn secp256k1_pubkey_deserialize<'de, D: serde::Deserializer<'de>>( - d: D, -) -> Result { - let key_hex = String::deserialize(d)?; - let key_bytes = hex_bytes(&key_hex).map_err(de_Error::custom)?; - - LibSecp256k1PublicKey::parse_slice(&key_bytes[..], None).map_err(de_Error::custom) -} - -fn secp256k1_privkey_serialize( - privk: &LibSecp256k1PrivateKey, - s: S, -) -> Result { - let key_hex = to_hex(&privk.serialize().to_vec()); - s.serialize_str(key_hex.as_str()) -} - -fn secp256k1_privkey_deserialize<'de, D: serde::Deserializer<'de>>( - d: D, -) -> Result { - let key_hex = String::deserialize(d)?; - let key_bytes = hex_bytes(&key_hex).map_err(de_Error::custom)?; - - LibSecp256k1PrivateKey::parse_slice(&key_bytes[..]).map_err(de_Error::custom) -} - -impl MessageSignature { - pub fn empty() -> MessageSignature { - // NOTE: this cannot be a valid signature - MessageSignature([0u8; 65]) - } - - #[cfg(test)] - // test method for generating place-holder data - pub fn from_raw(sig: &Vec) -> MessageSignature { - let mut buf = [0u8; 65]; - if sig.len() < 65 { - buf.copy_from_slice(&sig[..]); - } else { - buf.copy_from_slice(&sig[..65]); - } - MessageSignature(buf) - } - - pub fn from_secp256k1_recoverable( - sig: &LibSecp256k1Signature, - recid: LibSecp256k1RecoveryId, - ) -> MessageSignature { - let bytes = sig.serialize(); - let mut ret_bytes = [0u8; 65]; - let recovery_id_byte = recid.serialize(); // recovery ID will be 0, 1, 2, or 3 - ret_bytes[0] = recovery_id_byte; - ret_bytes[1..=64].copy_from_slice(&bytes[..64]); - MessageSignature(ret_bytes) - } - - pub fn to_secp256k1_recoverable( - &self, - ) -> Option<(LibSecp256k1Signature, LibSecp256k1RecoveryId)> { - let recovery_id = match LibSecp256k1RecoveryId::parse(self.0[0]) { - Ok(rid) => rid, - Err(_) => { - return None; - } - }; - let signature = LibSecp256k1Signature::parse_standard_slice(&self.0[1..65]).ok()?; - Some((signature, recovery_id)) - } -} - -impl PublicKey for Secp256k1PublicKey { - fn to_bytes(&self) -> Vec { - self.to_bytes() - } - - #[cfg(feature = "wasm-deterministic")] - fn verify(&self, _data_hash: &[u8], _sig: &MessageSignature) -> Result { - Err("Not implemented for wasm-deterministic") - } - - #[cfg(not(feature = "wasm-deterministic"))] - fn verify(&self, data_hash: &[u8], sig: &MessageSignature) -> Result { - let pub_key = Secp256k1PublicKey::recover_to_pubkey(data_hash, sig)?; - Ok(self.eq(&pub_key)) - } -} - -impl PrivateKey for Secp256k1PrivateKey { - fn to_bytes(&self) -> Vec { - let mut bits = self.key.serialize().to_vec(); - if self.compress_public { - bits.push(0x01); - } - bits - } - - #[cfg(feature = "wasm-deterministic")] - fn sign(&self, _data_hash: &[u8]) -> Result { - Err("Not implemented for wasm-deterministic") - } - - #[cfg(not(feature = "wasm-deterministic"))] - fn sign(&self, data_hash: &[u8]) -> Result { - let message = LibSecp256k1Message::parse_slice(data_hash) - .map_err(|_e| "Invalid message: failed to decode data hash: must be a 32-byte hash")?; - let (sig, recid) = libsecp256k1::sign(&message, &self.key); - let rec_sig = MessageSignature::from_secp256k1_recoverable(&sig, recid); - Ok(rec_sig) - } - - #[cfg(all(feature = "wasm-deterministic", any(test, feature = "testing")))] - fn sign_with_noncedata( - &self, - data_hash: &[u8], - noncedata: &[u8; 32], - ) -> Result { - Err("Not implemented for wasm-deterministic") - } - - #[cfg(all(any(test, feature = "testing"), not(feature = "wasm-deterministic")))] - fn sign_with_noncedata( - &self, - data_hash: &[u8], - noncedata: &[u8; 32], - ) -> Result { - let message = LibSecp256k1Message::parse_slice(data_hash) - .map_err(|_e| "Invalid message: failed to decode data hash: must be a 32-byte hash")?; - let mut nonce = Scalar::default(); - let _ = nonce.set_b32(&noncedata); - - // we need this as the key raw data are private - let mut key = Scalar::default(); - let _ = key.set_b32(&self.key.serialize()); - - let (sigr, sigs, recid) = match ECMULT_GEN_CONTEXT.sign_raw(&key, &message.0, &nonce) { - Ok(result) => result, - Err(_) => return Err("unable to sign message"), - }; - - let recid = match LibSecp256k1RecoveryId::parse(recid) { - Ok(recid) => recid, - Err(_) => return Err("invalid recovery id"), - }; - - let (sig, recid) = (LibSecp256k1Signature { r: sigr, s: sigs }, recid); - let rec_sig = MessageSignature::from_secp256k1_recoverable(&sig, recid); - Ok(rec_sig) - } -} diff --git a/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 9c051d3ddd7..f5af1dd9c91 100644 --- a/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -1838,16 +1838,11 @@ impl BitcoinRegtestController { (tx.signature_hash(i, &script_pub_key, sig_hash_all), false) }; - let sig1_der = { - let message = signer - .sign_message(sig_hash.as_bytes()) - .expect("Unable to sign message"); - message - .to_secp256k1_recoverable() - .expect("Unable to get recoverable signature") - .to_standard() - .serialize_der() - }; + let sig1_der = signer + .sign_message(sig_hash.as_bytes()) + .expect("Unable to sign message") + .to_der_signature() + .expect("Unable to DER-encode signature"); if is_segwit { // segwit @@ -2669,16 +2664,11 @@ mod tests { (tx.signature_hash(i, &script_pub_key, sig_hash_all), false) }; - let sig1_der = { - let message = signer - .sign_message(sig_hash.as_bytes()) - .expect("Unable to sign message"); - message - .to_secp256k1_recoverable() - .expect("Unable to get recoverable signature") - .to_standard() - .serialize_der() - }; + let sig1_der = signer + .sign_message(sig_hash.as_bytes()) + .expect("Unable to sign message") + .to_der_signature() + .expect("Unable to DER-encode signature"); if is_segwit { // segwit From 72cb3c78920fa25f1f9005640c6d1ff64d61583d Mon Sep 17 00:00:00 2001 From: rob-stacks Date: Wed, 10 Jun 2026 13:41:34 +0200 Subject: [PATCH 2/6] fmt-stacks --- stacks-common/src/util/secp256k1/native.rs | 41 ++++++++++++---------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/stacks-common/src/util/secp256k1/native.rs b/stacks-common/src/util/secp256k1/native.rs index 4e4bc88bae5..962264c5b93 100644 --- a/stacks-common/src/util/secp256k1/native.rs +++ b/stacks-common/src/util/secp256k1/native.rs @@ -246,8 +246,8 @@ fn is_low_s(sig: &LibSecp256k1Signature) -> bool { // secp256k1 group order n divided by 2 (big-endian) const HALF_ORDER: [u8; 32] = [ 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0x5d, 0x57, 0x6e, 0x73, 0x57, 0xa4, 0x50, 0x1d, 0xdf, 0xe9, 0x2f, 0x46, 0x68, - 0x1b, 0x20, 0xa0, + 0xff, 0x5d, 0x57, 0x6e, 0x73, 0x57, 0xa4, 0x50, 0x1d, 0xdf, 0xe9, 0x2f, 0x46, 0x68, 0x1b, + 0x20, 0xa0, ]; let bytes = sig.serialize(); bytes[32..] <= HALF_ORDER[..] @@ -355,9 +355,8 @@ impl PrivateKey for Secp256k1PrivateKey { #[cfg(not(feature = "wasm-deterministic"))] fn sign(&self, data_hash: &[u8]) -> Result { - let message = LibSecp256k1Message::parse_slice(data_hash).map_err(|_e| { - "Invalid message: failed to decode data hash: must be a 32-byte hash" - })?; + let message = LibSecp256k1Message::parse_slice(data_hash) + .map_err(|_e| "Invalid message: failed to decode data hash: must be a 32-byte hash")?; let (sig, recid) = libsecp256k1::sign(&message, &self.key); Ok(MessageSignature::from_secp256k1_recoverable(&sig, recid)) } @@ -377,9 +376,8 @@ impl PrivateKey for Secp256k1PrivateKey { data_hash: &[u8], noncedata: &[u8; 32], ) -> Result { - let message = LibSecp256k1Message::parse_slice(data_hash).map_err(|_e| { - "Invalid message: failed to decode data hash: must be a 32-byte hash" - })?; + let message = LibSecp256k1Message::parse_slice(data_hash) + .map_err(|_e| "Invalid message: failed to decode data hash: must be a 32-byte hash")?; let mut nonce = Scalar::default(); let _ = nonce.set_b32(noncedata); @@ -443,8 +441,7 @@ pub fn secp256k1_recover( ) -> Result<[u8; 33], LibSecp256k1Error> { let recovery_id = libsecp256k1::RecoveryId::parse(serialized_signature_arr[64] as u8)?; let message = LibSecp256k1Message::parse_slice(message_arr)?; - let signature = - LibSecp256k1Signature::parse_standard_slice(&serialized_signature_arr[..64])?; + let signature = LibSecp256k1Signature::parse_standard_slice(&serialized_signature_arr[..64])?; let recovered_pub = libsecp256k1::recover(&message, &signature, &recovery_id)?; Ok(recovered_pub.serialize_compressed()) } @@ -456,8 +453,7 @@ pub fn secp256k1_verify( pubkey_arr: &[u8], ) -> Result<(), LibSecp256k1Error> { let message = LibSecp256k1Message::parse_slice(message_arr)?; - let signature = - LibSecp256k1Signature::parse_standard_slice(&serialized_signature_arr[..64])?; // ignore 65th byte if present + let signature = LibSecp256k1Signature::parse_standard_slice(&serialized_signature_arr[..64])?; // ignore 65th byte if present let pubkey = LibSecp256k1PublicKey::parse_slice( pubkey_arr, Some(libsecp256k1::PublicKeyFormat::Compressed), @@ -986,9 +982,15 @@ mod tests { // to_der_signature must produce a valid SEQUENCE header for any parsed signature. let privk = Secp256k1PrivateKey::random(); let sig = privk.sign(&[0x44u8; 32]).expect("sign must succeed"); - let der = sig.to_der_signature().expect("must return Some for valid sig"); + let der = sig + .to_der_signature() + .expect("must return Some for valid sig"); assert_eq!(der[0], 0x30, "DER SEQUENCE tag"); - assert_eq!(der.len(), der[1] as usize + 2, "DER length field consistent"); + assert_eq!( + der.len(), + der[1] as usize + 2, + "DER length field consistent" + ); } // ----------------------------------------------------------------------- @@ -1059,7 +1061,10 @@ mod tests { let sk = Secp256k1PrivateKey::from_slice(&raw).expect("32-byte slice must parse"); assert!(!sk.compress_public()); let bytes = sk.to_bytes(); - assert_eq!(bytes, raw, "to_bytes on uncompressed key must return the 32 raw bytes"); + assert_eq!( + bytes, raw, + "to_bytes on uncompressed key must return the 32 raw bytes" + ); // 33-byte slice with 0x01 suffix → compressed key let mut raw_comp = [0u8; 33]; @@ -1184,8 +1189,7 @@ mod tests { hex_bytes("0385f2e2867524289d6047d0d9c5e764c5d413729fc32291ad2c353fbc396a4219") .unwrap(); let message = - hex_bytes("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9") - .unwrap(); + hex_bytes("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9").unwrap(); // secp256k1_verify takes RS (first 64 bytes); recovery ID at byte 64 is ignored let sig_rs = hex_bytes( "354445a1dc98a1bd27984dbe69979a5cd77886b4d9134af5c40e634d96e1cb44\ @@ -1222,8 +1226,7 @@ mod tests { ) .unwrap(); let high_s_msg = - hex_bytes("89171d7815da4bc1f644665a3234bc99d1680afa0b3285eff4878f4275fbfa89") - .unwrap(); + hex_bytes("89171d7815da4bc1f644665a3234bc99d1680afa0b3285eff4878f4275fbfa89").unwrap(); let high_s_pubkey = hex_bytes("0256b328b30c8bf5839e24058747879408bdb36241dc9c2e7c619faa12b2920967") .unwrap(); From 9b3c50d1cbf958d57788be70895ca4392cbdba09 Mon Sep 17 00:00:00 2001 From: rob-stacks Date: Wed, 10 Jun 2026 14:16:27 +0200 Subject: [PATCH 3/6] fixed wasm-non-deterministic --- stacks-common/src/util/secp256k1/native.rs | 26 ++++++++++++++-------- stacks-signer/src/client/mod.rs | 4 ++-- stacks-signer/src/client/stackerdb.rs | 2 +- stacks-signer/src/main.rs | 2 +- stacks-signer/src/runloop.rs | 2 +- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/stacks-common/src/util/secp256k1/native.rs b/stacks-common/src/util/secp256k1/native.rs index 962264c5b93..8abe057cbda 100644 --- a/stacks-common/src/util/secp256k1/native.rs +++ b/stacks-common/src/util/secp256k1/native.rs @@ -21,7 +21,6 @@ use ::libsecp256k1::{ self, PublicKey as LibSecp256k1PublicKey, RecoveryId as LibSecp256k1RecoveryId, SecretKey as LibSecp256k1PrivateKey, Signature as LibSecp256k1Signature, ECMULT_GEN_CONTEXT, }; -#[cfg(not(feature = "wasm-deterministic"))] use ::libsecp256k1::{Error as LibSecp256k1Error, Message as LibSecp256k1Message}; use serde::de::{Deserialize, Error as de_Error}; use serde::Serialize; @@ -166,7 +165,6 @@ impl Secp256k1PublicKey { .map_err(|_e| "Invalid public key: failed to load") } - #[cfg(not(feature = "wasm-deterministic"))] pub fn from_private(privk: &Secp256k1PrivateKey) -> Secp256k1PublicKey { let key = LibSecp256k1PublicKey::from_secret_key_with_context(&privk.key, &ECMULT_GEN_CONTEXT); @@ -192,7 +190,6 @@ impl Secp256k1PublicKey { self.compressed = value; } - #[cfg(not(feature = "wasm-deterministic"))] /// recover message and signature to public key (will be compressed) pub fn recover_to_pubkey( msg: &[u8], @@ -241,7 +238,6 @@ impl PublicKey for Secp256k1PublicKey { } /// Returns true if the signature's S value is in the lower half of the secp256k1 group order. -#[cfg(not(feature = "wasm-deterministic"))] fn is_low_s(sig: &LibSecp256k1Signature) -> bool { // secp256k1 group order n divided by 2 (big-endian) const HALF_ORDER: [u8; 32] = [ @@ -434,19 +430,17 @@ fn secp256k1_privkey_deserialize<'de, D: serde::Deserializer<'de>>( LibSecp256k1PrivateKey::parse_slice(&key_bytes[..]).map_err(de_Error::custom) } -#[cfg(not(feature = "wasm-deterministic"))] pub fn secp256k1_recover( message_arr: &[u8], serialized_signature_arr: &[u8], ) -> Result<[u8; 33], LibSecp256k1Error> { - let recovery_id = libsecp256k1::RecoveryId::parse(serialized_signature_arr[64] as u8)?; + let recovery_id = libsecp256k1::RecoveryId::parse(serialized_signature_arr[64])?; let message = LibSecp256k1Message::parse_slice(message_arr)?; let signature = LibSecp256k1Signature::parse_standard_slice(&serialized_signature_arr[..64])?; let recovered_pub = libsecp256k1::recover(&message, &signature, &recovery_id)?; Ok(recovered_pub.serialize_compressed()) } -#[cfg(not(feature = "wasm-deterministic"))] pub fn secp256k1_verify( message_arr: &[u8], serialized_signature_arr: &[u8], @@ -471,6 +465,7 @@ pub fn secp256k1_verify( #[cfg(test)] mod tests { + #[cfg(not(feature = "wasm-deterministic"))] use rand::RngCore as _; /// Negate a secp256k1 scalar: returns `n - s` (mod n), giving the complementary S value. @@ -493,6 +488,7 @@ mod tests { result } + #[cfg(not(feature = "wasm-deterministic"))] #[test] fn test_recover_high_s() { // Sign a message, convert the signature to its high-S equivalent, and verify that @@ -554,8 +550,9 @@ mod tests { } use super::*; - use crate::util::get_epoch_time_ms; use crate::util::hash::hex_bytes; + #[cfg(not(feature = "wasm-deterministic"))] + use crate::util::get_epoch_time_ms; struct KeyFixture { input: I, @@ -593,13 +590,14 @@ mod tests { .unwrap() .compress_public()); - assert_eq!(Secp256k1PrivateKey::from_hex(&h_uncomp), Ok(t1.clone())); + assert_eq!(Secp256k1PrivateKey::from_hex(&h_uncomp), Ok(t1)); t1.set_compress_public(true); assert_eq!(Secp256k1PrivateKey::from_hex(&h_comp), Ok(t1)); } + #[cfg(not(feature = "wasm-deterministic"))] #[test] /// Test the behavior of from_seed using hard-coded values from previous existing integration tests fn sk_from_seed() { @@ -685,6 +683,7 @@ mod tests { } } + #[cfg(not(feature = "wasm-deterministic"))] #[test] fn test_verify() { let fixtures: Vec>> = vec![ @@ -763,6 +762,7 @@ mod tests { } } + #[cfg(not(feature = "wasm-deterministic"))] #[test] #[ignore] fn test_verify_benchmark_roundtrip() { @@ -826,6 +826,7 @@ mod tests { // MessageSignature::empty // ----------------------------------------------------------------------- + #[cfg(not(feature = "wasm-deterministic"))] #[test] fn test_message_signature_empty() { let sig = MessageSignature::empty(); @@ -849,6 +850,7 @@ mod tests { // MessageSignature: from_secp256k1_recoverable / to_secp256k1_recoverable // ----------------------------------------------------------------------- + #[cfg(not(feature = "wasm-deterministic"))] #[test] fn test_message_signature_recoverable_roundtrip() { let privk = Secp256k1PrivateKey::random(); @@ -953,6 +955,7 @@ mod tests { assert_eq!(der[s_offset + 2], 0x01, "S value"); } + #[cfg(not(feature = "wasm-deterministic"))] #[test] fn test_to_der_signature_matches_der_encode() { let privk = Secp256k1PrivateKey::random(); @@ -977,6 +980,7 @@ mod tests { ); } + #[cfg(not(feature = "wasm-deterministic"))] #[test] fn test_to_der_signature_structure() { // to_der_signature must produce a valid SEQUENCE header for any parsed signature. @@ -1023,6 +1027,7 @@ mod tests { // Secp256k1PublicKey::recover_to_pubkey // ----------------------------------------------------------------------- + #[cfg(not(feature = "wasm-deterministic"))] #[test] fn test_recover_to_pubkey() { let privk = Secp256k1PrivateKey::random(); @@ -1109,6 +1114,7 @@ mod tests { // PrivateKey::sign (non-ignored, end-to-end) // ----------------------------------------------------------------------- + #[cfg(not(feature = "wasm-deterministic"))] #[test] fn test_sign_and_verify_roundtrip() { let privk = Secp256k1PrivateKey::random(); @@ -1135,6 +1141,7 @@ mod tests { // PrivateKey::sign_with_noncedata // ----------------------------------------------------------------------- + #[cfg(not(feature = "wasm-deterministic"))] #[test] fn test_sign_with_noncedata_deterministic() { let privk = Secp256k1PrivateKey::random(); @@ -1179,6 +1186,7 @@ mod tests { // secp256k1_verify // ----------------------------------------------------------------------- + #[cfg(not(feature = "wasm-deterministic"))] #[test] fn test_secp256k1_verify_function() { // Use the same test vector as test_verify: diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index 6c09d84624c..7329ab3b443 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -433,7 +433,7 @@ pub(crate) mod tests { for signer_id in 0..num_signers { let private_key = if signer_id == 0 { - config.stacks_private_key.clone() + config.stacks_private_key } else { StacksPrivateKey::random() }; @@ -466,7 +466,7 @@ pub(crate) mod tests { signer_addresses, }, signer_slot_ids, - stacks_private_key: config.stacks_private_key.clone(), + stacks_private_key: config.stacks_private_key, node_host: config.node_host.to_string(), mainnet: config.network.is_mainnet(), db_path: config.db_path.clone(), diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index d6e5582274f..ad942a31ae7 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -77,7 +77,7 @@ impl From<&SignerConfig> for StackerDB { Self::new( &config.node_host, - config.stacks_private_key.clone(), + config.stacks_private_key, config.mainnet, config.reward_cycle, signer_db, diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index c4931c3ab55..c242b131d44 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -128,7 +128,7 @@ fn handle_generate_stacking_signature( ) -> MessageSignature { let config = GlobalConfig::try_from(&args.config).unwrap(); - let private_key = config.stacks_private_key.clone(); + let private_key = config.stacks_private_key; let public_key = StacksPublicKey::from_private(&private_key); let pk_hex = to_hex(&public_key.to_bytes_compressed()); diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index fd2c81fba1a..b491496de0b 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -316,7 +316,7 @@ impl, T: StacksMessageCodec + Clone + Send + Debug> RunLo signer_entries, signer_slot_ids: signer_slot_ids.into_values().collect(), first_proposal_burn_block_timing: self.config.first_proposal_burn_block_timing, - stacks_private_key: self.config.stacks_private_key.clone(), + stacks_private_key: self.config.stacks_private_key, node_host: self.config.node_host.to_string(), mainnet: self.config.network.is_mainnet(), db_path: self.config.db_path.clone(), From 49448dd3eea50d687aa9ed22bc30c562dc80ce65 Mon Sep 17 00:00:00 2001 From: rob-stacks Date: Wed, 10 Jun 2026 14:37:57 +0200 Subject: [PATCH 4/6] added changelog --- changelog.d/unified_secp256k1.added | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/unified_secp256k1.added diff --git a/changelog.d/unified_secp256k1.added b/changelog.d/unified_secp256k1.added new file mode 100644 index 00000000000..93ebb07c500 --- /dev/null +++ b/changelog.d/unified_secp256k1.added @@ -0,0 +1 @@ +Unified the secp256k1 api to use pure-rust libsecp256k1 From 6e5b03a5c86277c1e80b908d1610580d69cff9d3 Mon Sep 17 00:00:00 2001 From: rob-stacks Date: Wed, 10 Jun 2026 14:39:25 +0200 Subject: [PATCH 5/6] fmt-stacks --- stacks-common/src/util/secp256k1/native.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stacks-common/src/util/secp256k1/native.rs b/stacks-common/src/util/secp256k1/native.rs index 8abe057cbda..3e10aecd926 100644 --- a/stacks-common/src/util/secp256k1/native.rs +++ b/stacks-common/src/util/secp256k1/native.rs @@ -18,10 +18,10 @@ use ::libsecp256k1::curve::Scalar; pub use ::libsecp256k1::Error; use ::libsecp256k1::{ - self, PublicKey as LibSecp256k1PublicKey, RecoveryId as LibSecp256k1RecoveryId, + self, Error as LibSecp256k1Error, Message as LibSecp256k1Message, + PublicKey as LibSecp256k1PublicKey, RecoveryId as LibSecp256k1RecoveryId, SecretKey as LibSecp256k1PrivateKey, Signature as LibSecp256k1Signature, ECMULT_GEN_CONTEXT, }; -use ::libsecp256k1::{Error as LibSecp256k1Error, Message as LibSecp256k1Message}; use serde::de::{Deserialize, Error as de_Error}; use serde::Serialize; @@ -550,9 +550,9 @@ mod tests { } use super::*; - use crate::util::hash::hex_bytes; #[cfg(not(feature = "wasm-deterministic"))] use crate::util::get_epoch_time_ms; + use crate::util::hash::hex_bytes; struct KeyFixture { input: I, From b411eff4b7398aca8c44fec444980d2e966324c6 Mon Sep 17 00:00:00 2001 From: rob-stacks Date: Wed, 10 Jun 2026 14:52:21 +0200 Subject: [PATCH 6/6] more clippy checks --- stackslib/src/chainstate/stacks/miner.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stackslib/src/chainstate/stacks/miner.rs b/stackslib/src/chainstate/stacks/miner.rs index 7c57822b7ed..b8ad95d14c2 100644 --- a/stackslib/src/chainstate/stacks/miner.rs +++ b/stackslib/src/chainstate/stacks/miner.rs @@ -1544,7 +1544,7 @@ impl StacksBlockBuilder { proof, &pubkh, ); - builder.miner_privkey = microblock_privkey.clone(); + builder.miner_privkey = *microblock_privkey; builder } @@ -1604,7 +1604,7 @@ impl StacksBlockBuilder { proof, &pubkh, ); - builder.miner_privkey = microblock_privkey.clone(); + builder.miner_privkey = *microblock_privkey; builder }