diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c0dbb9b..2c0e94911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added - Make `PublicKey::with_serialized` public +- `EcdsaPublicKey`, `EcdsaSecretKey`, `EcdsaSignature`, `EcdsaRecSignature`, + `EcdsaError` and the `ECDSA_*_LEN` constants in `dashcore::ecdsa` +- `EcdsaSecretKey::generate` with the `rand` feature +- `TryFrom<&[u8]>` for `PublicKey` ### Changed @@ -41,6 +45,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **Breaking:** the taproot types come from `bitcoin-crypto`. `Signature` fields are renamed to `signature` and `sighash_type`, and `taproot::Error` is `SigFromSliceError`, without a `Secp256k1` variant +- **Breaking:** `ecdsa::Signature` and `ecdsa::SerializedSignature` come from + `bitcoin-crypto`. `Signature` fields and human-readable serde keys are + renamed from `sig` and `hash_ty` to `signature` and `sighash_type`. + `signature` is a `secp256k1` signature. + `DecodeError` and `ParseSignatureError` replace `ecdsa::Error`. Formatting + flags apply to the whole encoding instead of being ignored +- **Breaking:** the `dashcore::ecdsa` newtypes replace the `secp256k1` keys and + signatures in `PublicKey`, `PrivateKey`, `ExtendedPrivKey`, `ExtendedPubKey`, + `RootExtendedPrivKey`, `RootExtendedPubKey`, `Wallet` key derivation, + `AssetLockCreditKeys` and `Signer`. `EcdsaPublicKey` converts into + `XOnlyPublicKey` and `EcdsaSecretKey` into `Keypair` +- **Breaking:** `key::XOnlyPublicKey`, `Keypair`, `Parity`, `TweakedPublicKey` + and `UntweakedPublicKey` come from `bitcoin-crypto`, whose `XOnlyPublicKey` + keeps its parity. `TweakedKeyPair` and `UntweakedKeyPair` are renamed to + `TweakedKeypair` and `UntweakedKeypair` +- **Breaking:** `TaprootError::InvalidInternalKey` and + `TaprootBuilderError::InvalidInternalKey` no longer hold a `secp256k1::Error` +- **Breaking:** `PrivateKey` is no longer `Copy`. `to_bytes` and `to_wif` take + `&self` instead of `self` +- **Breaking:** `Ecdsa(EcdsaError)` replaces `key::Error::Secp256k1` and + `bip32::Error::Secp256k1`. `MessageSignatureError::InvalidEncoding` holds an + `EcdsaError` instead of a `secp256k1::Error`. Failed verification reads + "signature verification failed" instead of "signature failed verification" - **Breaking:** `dashcore::eddsa` uses the `dash-pkc` types, so `EddsaPkBytes` and `EddsaSkBytes` no longer implement `BaseCodec` and `EddsaError` changes shape. Use `EddsaPublicKey::try_from`, `EddsaPkHash::from` and @@ -59,10 +86,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **Breaking:** `ExtendedPrivKey::new_master` rejects seeds outside 16 to 64 bytes with `bip32::Error::InvalidSeedLength`, which `RootExtendedPrivKey::new_master` returns instead of `InvalidParameter` -- **Breaking:** `MessageSignature::from_slice` rejects headers above 34 +- **Breaking:** `EcdsaRecSignature` rejects headers above 34 when read - Relicense `dash-network`, `dash-network-seeds`, `dash-spv`, `dash-spv-ffi`, `dash-spv-bench`, `git-state` and `masternode-seeds-fetcher` to CC0-1.0 - All workspace crates declare a MSRV of 1.89 +- Human-readable serde of `PublicKey`, `ShortId` and `PlatformNodeId` also + accepts raw bytes, and their deserialization errors read differently +- `RootExtendedPubKey` now supports hybrid SEC1 public keys in human-readable + serde, and writes them back compressed ### Fixed @@ -102,8 +133,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `Witness::push_bitcoin_signature` - **Breaking:** remove `key::Error::NotSupported`, `bip32::Error::NotSupported` and `dashcore_rpc::Error::Secp256k1` -- **Breaking:** remove `signer::CompactSignature`. Use `MessageSignature` +- **Breaking:** remove `signer::CompactSignature`. Use `EcdsaRecSignature` instead +- **Breaking:** remove `bip32::PublicKey`, `bip32::PrivateKey`, + `key_wallet::Error::Secp256k1` and `Index` for `PrivateKey` +- **Breaking:** remove the `secp256k1` re-exports from `dashcore` and + `dashcore-crypto`, `key::Secp256k1`, `key::Verification` and + `TaprootError::InvalidParity` +- **Breaking:** remove the `secp-recovery` and `secp-lowmemory` features. + Message signature recovery is always available +- **Breaking:** remove `MessageSignature`. Signed messages are + `EcdsaRecSignature`s, which carry the key compression, and the + `sign_message` functions `from_slice`, `from_base64`, `to_base64`, + `recover_pubkey` and `is_signed_by_address` replace its methods ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 66179b6b7..8693ad218 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,14 +8,13 @@ bincode = { package = "grovedb-bincode", version = "=2.1.0" } bincode_derive = { package = "grovedb-bincode-derive", version = "=2.1.0" } futures = "0.3" rayon = "1.11" -secp256k1 = "0.33.1" # must match with version used in `bitcoin-crypto` and `dash-pkc` serde = { version = "1.0.219", default-features = false } zeroize = { version = "1.8", features = ["derive"] } # Packages from `dashpay/base-sdk`, MUST share same version ACROSS crates -dash-num = { git = "https://github.com/dashpay/base-sdk", rev = "33fcc59e1868c803f9608e0e78ee8bddf3abcee7", default-features = false } -dash-pkc = { git = "https://github.com/dashpay/base-sdk", rev = "33fcc59e1868c803f9608e0e78ee8bddf3abcee7", default-features = false } -dash-types = { git = "https://github.com/dashpay/base-sdk", rev = "33fcc59e1868c803f9608e0e78ee8bddf3abcee7", default-features = false } +dash-num = { git = "https://github.com/dashpay/base-sdk", rev = "d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5", default-features = false } +dash-pkc = { git = "https://github.com/dashpay/base-sdk", rev = "d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5", default-features = false } +dash-types = { git = "https://github.com/dashpay/base-sdk", rev = "d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5", default-features = false } # Packages from `rust-bitcoin/rust-bitcoin`, `bitcoin-crypto` uses revision due to `secp256k1` base58ck = { version = "0.5.0" } diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index e1868a5a0..c33950676 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -14,9 +14,10 @@ version.workspace = true default = [] bincode = ["dep:bincode", "dep:bincode_derive", "dashcore_hashes/bincode", "dash-network/bincode"] -bls = ["dash-pkc/bls", "dep:tracing"] +bls = ["dash-pkc/bls", "dash-pkc/portable", "dep:tracing"] eddsa = ["dash-pkc/eddsa"] -serde = ["dep:serde", "bitcoin-crypto/serde", "dash-types/serde", "dashcore_hashes/serde", "dash-network/serde", "dash-pkc/serde", "secp256k1/serde"] +rand = ["bitcoin-crypto/rand"] +serde = ["dep:serde", "bitcoin-crypto/serde", "dash-types/serde", "dashcore_hashes/serde", "dash-network/serde", "dash-pkc/serde"] [dependencies] base58ck = { workspace = true } @@ -24,18 +25,20 @@ bitcoin-crypto = { workspace = true, features = ["std", "hex"] } bincode = { workspace = true, optional = true } bincode_derive = { workspace = true, optional = true } dash-network = { path = "../dash-network" } -dash-pkc = { workspace = true, features = ["std"] } -dash-types = { workspace = true, features = ["codec"] } +dash-pkc = { workspace = true, features = ["ecdsa", "std"] } +dash-types = { workspace = true } +delegate = "0.13" dashcore_hashes = { path = "../hashes" } hex-conservative = { workspace = true } internals = { path = "../internals", package = "dashcore-private" } -secp256k1 = { workspace = true } serde = { workspace = true, features = ["derive", "alloc"], optional = true } thiserror = "2" tracing = { version = "0.1", optional = true } +zeroize = { workspace = true } [lints.rust] unexpected_cfgs = { level = "deny", check-cfg = ['cfg(bench)', 'cfg(fuzzing)', 'cfg(kani)'] } [dev-dependencies] +bincode = { workspace = true, features = ["serde"] } serde_json = { version = "1.0" } diff --git a/crypto/src/ecdsa.rs b/crypto/src/ecdsa.rs index 7f8efcd8c..4fe4ea361 100644 --- a/crypto/src/ecdsa.rs +++ b/crypto/src/ecdsa.rs @@ -9,266 +9,287 @@ //! //! This module provides ECDSA signatures used Bitcoin that can be roundtrip (de)serialized. +use core::fmt; use core::str::FromStr; -use core::{fmt, iter}; +#[cfg(feature = "rand")] +use bitcoin_crypto::key::rand; +use bitcoin_crypto::key::{Keypair, XOnlyPublicKey}; +use dash_pkc::__deps::secp256k1; +use dash_pkc::ecdsa::{ + Compression, EcdsaPublicKey as PkcPublicKey, EcdsaRecSigBytes as PkcRecSigBytes, + EcdsaRecSignature as PkcRecSignature, EcdsaSecretKey as PkcSecretKey, + EcdsaSignature as PkcSignature, +}; +use dash_types::type_cvrt; +use delegate::delegate; use hex_conservative::DisplayHex; -use internals::write_err; -use secp256k1; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; +use internals::{debug_from_display, impl_array_newtype, impl_bytes_newtype}; +use zeroize::{Zeroize, Zeroizing}; -use crate::sighash::EcdsaSighashType; +pub use bitcoin_crypto::ecdsa::{DecodeError, ParseSignatureError, SerializedSignature, Signature}; +pub use dash_pkc::ecdsa::EcdsaError; -const MAX_SIG_LEN: usize = 73; +/// Compressed SEC1 public key length. +pub const ECDSA_PK_LEN: usize = 33; -/// An ECDSA signature with the corresponding hash type. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Signature { - /// The underlying ECDSA Signature - pub sig: secp256k1::ecdsa::Signature, - /// The corresponding hash type - pub hash_ty: EcdsaSighashType, -} +/// Uncompressed SEC1 public key length. +pub const ECDSA_PK_UNCOMPRESSED_LEN: usize = 65; -impl Signature { - /// Constructs an ECDSA dash signature for [`EcdsaSighashType::All`]. - pub fn sighash_all(sig: secp256k1::ecdsa::Signature) -> Signature { - Signature { - sig, - hash_ty: EcdsaSighashType::All, - } - } +/// Scalar (secret key or tweak) length. +pub const ECDSA_SK_LEN: usize = 32; - /// Deserializes from slice following the standardness rules for [`EcdsaSighashType`]. - pub fn from_slice(sl: &[u8]) -> Result { - let (hash_ty, sig) = sl.split_last().ok_or(Error::EmptySignature)?; - let hash_ty = EcdsaSighashType::from_standard(*hash_ty as u32) - .map_err(|_| Error::NonStandardSighashType(*hash_ty as u32))?; - let sig = secp256k1::ecdsa::Signature::from_der(sig).map_err(Error::Secp256k1)?; - Ok(Signature { - sig, - hash_ty, - }) - } +/// Compact (`r || s`) signature length. +pub const ECDSA_SIG_LEN: usize = 64; - /// Serializes an ECDSA signature (inner secp256k1 signature in DER format). - /// - /// This does **not** perform extra heap allocation. - pub fn serialize(&self) -> SerializedSignature { - let mut buf = [0u8; MAX_SIG_LEN]; - let signature = self.sig.serialize_der(); - buf[..signature.len()].copy_from_slice(&signature); - buf[signature.len()] = self.hash_ty.to_consensus_u8(); - SerializedSignature { - data: buf, - len: signature.len() + 1, +/// Compact recoverable signature length (a header byte, then `r || s`). +pub const ECDSA_REC_SIG_LEN: usize = ECDSA_SIG_LEN + 1; + +/// A secp256k1 public key. +#[derive(Clone, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(from = "secp256k1::PublicKey"))] +#[cfg_attr(feature = "serde", serde(into = "secp256k1::PublicKey"))] +pub struct EcdsaPublicKey(PkcPublicKey); + +impl EcdsaPublicKey { + delegate! { + to PkcPublicKey { + /// Parses a compressed, uncompressed or hybrid SEC1 encoding. + #[expr($.map(Self))] + pub fn from_bytes(bytes: &[u8]) -> Result; + } + to self.0 { + /// The compressed SEC1 encoding. + pub fn to_compressed(&self) -> [u8; ECDSA_PK_LEN]; + + /// The uncompressed SEC1 encoding. + pub fn to_uncompressed(&self) -> [u8; ECDSA_PK_UNCOMPRESSED_LEN]; + + /// Adds `tweak * G` to the point. + #[expr($.map(Self))] + pub fn add_tweak(&self, tweak: &[u8; ECDSA_SK_LEN]) -> Result; + + /// Checks `sig` over the 32-byte prehashed message. + pub fn verify( + &self, + msg_hash: &[u8; 32], + #[newtype] sig: EcdsaSignature, + ) -> Result<(), EcdsaError>; } } - /// Serializes an ECDSA signature (inner secp256k1 signature in DER format) into `Vec`. - /// - /// Note: this performs an extra heap allocation, you might prefer the - /// [`serialize`](Self::serialize) method instead. - pub fn to_vec(self) -> Vec { - // TODO: add support to serialize to a writer to SerializedSig - self.sig - .serialize_der() - .iter() - .copied() - .chain(iter::once(self.hash_ty.to_consensus_u8())) - .collect() + /// Recovers the signing key from a recoverable signature over the 32-byte + /// prehashed message. + pub fn recover(msg_hash: &[u8; 32], sig: &EcdsaRecSignature) -> Result { + PkcPublicKey::recover(msg_hash, &sig.parse()?).map(Self) } } -impl fmt::Display for Signature { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:x}", self.sig.serialize_der().as_hex())?; - write!(f, "{:x}", [self.hash_ty.to_consensus_u8()].as_hex()) +/// Compared as curve points, whichever encoding the key was parsed from. +impl PartialEq for EcdsaPublicKey { + fn eq(&self, other: &Self) -> bool { + self.to_compressed() == other.to_compressed() } } -impl FromStr for Signature { - type Err = Error; +impl Eq for EcdsaPublicKey {} - fn from_str(s: &str) -> Result { - let bytes = hex_conservative::decode_to_vec(s)?; - let (sighash_byte, signature) = bytes.split_last().ok_or(Error::EmptySignature)?; - Ok(Signature { - sig: secp256k1::ecdsa::Signature::from_der(signature)?, - hash_ty: EcdsaSighashType::from_standard(*sighash_byte as u32) - .map_err(|_| Error::NonStandardSighashType(*sighash_byte as u32))?, - }) +impl core::hash::Hash for EcdsaPublicKey { + fn hash(&self, state: &mut H) { + self.to_compressed().hash(state) } } -/// Holds signature serialized in-line (not in `Vec`). -/// -/// This avoids allocation and allows proving maximum size of the signature (73 bytes). -/// The type can be used largely as a byte slice. It implements all standard traits one would -/// expect and has familiar methods. -/// However, the usual use case is to push it into a script. This can be done directly passing it -/// into `ScriptBuf::push_slice`. -#[derive(Copy, Clone)] -pub struct SerializedSignature { - data: [u8; MAX_SIG_LEN], - len: usize, -} +debug_from_display!(EcdsaPublicKey); -impl SerializedSignature { - /// Returns an iterator over bytes of the signature. - #[inline] - pub fn iter(&self) -> core::slice::Iter<'_, u8> { - self.into_iter() +/// Hex of the compressed encoding. +impl fmt::Display for EcdsaPublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:x}", self.to_compressed().as_hex()) } } -impl core::ops::Deref for SerializedSignature { - type Target = [u8]; +/// Hex of either SEC1 encoding. +impl FromStr for EcdsaPublicKey { + type Err = EcdsaError; - #[inline] - fn deref(&self) -> &Self::Target { - &self.data[..self.len] + fn from_str(s: &str) -> Result { + Self::from_bytes( + &hex_conservative::decode_to_vec(s).map_err(|_| EcdsaError::InvalidPublicKey)?, + ) } } -impl core::ops::DerefMut for SerializedSignature { - #[inline] - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.data[..self.len] - } -} +type_cvrt!(From for EcdsaPublicKey, |pk| Self(pk.into())); +type_cvrt!(From for secp256k1::PublicKey, |pk| pk.0.into()); +type_cvrt!(From for XOnlyPublicKey, |pk| { + Self::from(secp256k1::PublicKey::from(pk)) +}); + +/// A secp256k1 secret key. +#[derive(Clone, Eq, PartialEq, Zeroize)] +pub struct EcdsaSecretKey(PkcSecretKey); + +impl EcdsaSecretKey { + delegate! { + to PkcSecretKey { + /// Wraps a big-endian scalar. + #[expr($.map(Self))] + pub fn from_bytes( + bytes: &[u8; ECDSA_SK_LEN], + [Compression::Compressed], + ) -> Result; + } + to self.0 { + /// Copies out the big-endian scalar. + pub fn to_bytes(&self) -> Zeroizing<[u8; ECDSA_SK_LEN]>; -impl AsRef<[u8]> for SerializedSignature { - #[inline] - fn as_ref(&self) -> &[u8] { - self - } -} + /// Derives the corresponding public key. + #[expr(EcdsaPublicKey($))] + pub fn public_key(&self) -> EcdsaPublicKey; -impl AsMut<[u8]> for SerializedSignature { - #[inline] - fn as_mut(&mut self) -> &mut [u8] { - self - } -} + /// Adds `tweak` to the scalar. + #[expr($.map(Self))] + pub fn add_tweak(&self, tweak: &[u8; ECDSA_SK_LEN]) -> Result; -impl core::borrow::Borrow<[u8]> for SerializedSignature { - #[inline] - fn borrow(&self) -> &[u8] { - self - } -} + /// Signs a 32-byte prehashed message (RFC 6979, low-S, low-R ground). + #[expr(EcdsaSignature($))] + pub fn sign(&self, msg_hash: &[u8; 32]) -> EcdsaSignature; -impl core::borrow::BorrowMut<[u8]> for SerializedSignature { - #[inline] - fn borrow_mut(&mut self) -> &mut [u8] { - self + /// Signs a 32-byte prehashed message recoverably (RFC 6979, low-S). + #[expr(EcdsaRecSignature::from_pkc($))] + pub fn sign_recoverable(&self, msg_hash: &[u8; 32]) -> EcdsaRecSignature; + } } -} -impl fmt::Debug for SerializedSignature { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(self, f) + /// Draws a random scalar from the thread RNG. + #[cfg(feature = "rand")] + pub fn generate() -> Self { + Self(secp256k1::SecretKey::new(&mut rand::rng()).into()) } } -impl fmt::Display for SerializedSignature { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::LowerHex::fmt(self, f) +impl fmt::Debug for EcdsaSecretKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("EcdsaSecretKey(..)") } } -impl fmt::LowerHex for SerializedSignature { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:x}", (**self).as_hex()) - } -} +type_cvrt!(From for Keypair, |sk| { + Self::from(secp256k1::Keypair::from_secret_key(&(&sk.0).into())) +}); -impl fmt::UpperHex for SerializedSignature { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:X}", (**self).as_hex()) +#[cfg(feature = "serde")] +impl serde::Serialize for EcdsaSecretKey { + fn serialize(&self, s: S) -> Result { + serde::Serialize::serialize(&secp256k1::SecretKey::from(&self.0), s) } } -impl PartialEq for SerializedSignature { - #[inline] - fn eq(&self, other: &SerializedSignature) -> bool { - **self == **other +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for EcdsaSecretKey { + fn deserialize>(d: D) -> Result { + ::deserialize(d).map(|sk| Self(sk.into())) } } -impl Eq for SerializedSignature {} +/// A secp256k1 ECDSA signature (the `r` and `s` scalars, without a sighash +/// type). +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(from = "secp256k1::ecdsa::Signature"))] +#[cfg_attr(feature = "serde", serde(into = "secp256k1::ecdsa::Signature"))] +pub struct EcdsaSignature(PkcSignature); + +impl EcdsaSignature { + delegate! { + to PkcSignature { + /// Parses the compact (`r || s`) encoding. + #[expr($.map(Self))] + pub fn from_bytes(bytes: &[u8; ECDSA_SIG_LEN]) -> Result; + + /// Parses a DER encoding. + #[expr($.map(Self))] + pub fn from_der(bytes: &[u8]) -> Result; + } + to self.0 { + /// The compact (`r || s`) encoding. + pub fn to_bytes(&self) -> [u8; ECDSA_SIG_LEN]; + } + } -impl core::hash::Hash for SerializedSignature { - fn hash(&self, state: &mut H) { - core::hash::Hash::hash(&**self, state) + /// Calls `f` with the DER encoding, without allocating. + pub fn with_der R>(&self, f: F) -> R { + f(self.0.to_der().as_bytes()) } } -impl<'a> IntoIterator for &'a SerializedSignature { - type IntoIter = core::slice::Iter<'a, u8>; - type Item = &'a u8; +debug_from_display!(EcdsaSignature); - #[inline] - fn into_iter(self) -> Self::IntoIter { - (*self).iter() +/// Hex of the DER encoding. +impl fmt::Display for EcdsaSignature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.with_der(|der| write!(f, "{:x}", der.as_hex())) } } -/// A key-related error. -#[derive(Clone, PartialEq, Eq, Debug)] -#[non_exhaustive] -pub enum Error { - /// Hex encoding error - HexEncoding(hex_conservative::DecodeVariableLengthBytesError), - /// Non-standard sighash type - NonStandardSighashType(u32), - /// Empty Signature - EmptySignature, - /// secp256k1-related error - Secp256k1(secp256k1::Error), -} +/// Hex of the DER encoding. +impl FromStr for EcdsaSignature { + type Err = EcdsaError; -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Error::HexEncoding(ref e) => write_err!(f, "Signature hex encoding error"; e), - Error::NonStandardSighashType(hash_ty) => { - write!(f, "Non standard signature hash type {}", hash_ty) - } - Error::EmptySignature => write!(f, "Empty ECDSA signature"), - Error::Secp256k1(ref e) => write_err!(f, "invalid ECDSA signature"; e), - } + fn from_str(s: &str) -> Result { + Self::from_der( + &hex_conservative::decode_to_vec(s).map_err(|_| EcdsaError::InvalidSignature)?, + ) } } -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use self::Error::*; +type_cvrt!(From for EcdsaSignature, |sig| Self(PkcSignature::from(*sig))); +type_cvrt!(From for secp256k1::ecdsa::Signature, |sig| Self::from(sig.0)); + +/// A compact recoverable signature as Dash Core writes it, a header byte +/// carrying the recovery id and key compression, then `r || s`. Checked by +/// the operations that read it. +#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "bincode", derive(bincode_derive::Encode, bincode_derive::Decode))] +pub struct EcdsaRecSignature([u8; ECDSA_REC_SIG_LEN]); + +impl_array_newtype!(EcdsaRecSignature, u8, ECDSA_REC_SIG_LEN); +impl_bytes_newtype!(EcdsaRecSignature, ECDSA_REC_SIG_LEN); + +impl EcdsaRecSignature { + /// Pairs a signature with its recovery id and key compression. + pub fn from_parts( + sig: EcdsaSignature, + recovery_id: u8, + compressed: bool, + ) -> Result { + PkcRecSignature::from_parts(sig.0, recovery_id, compressed.into()).map(Self::from_pkc) + } - match self { - HexEncoding(e) => Some(e), - Secp256k1(e) => Some(e), - NonStandardSighashType(_) | EmptySignature => None, - } + fn from_pkc(sig: PkcRecSignature) -> Self { + Self(PkcRecSigBytes::from(sig).to_bytes()) } -} -impl From for Error { - fn from(e: secp256k1::Error) -> Error { - Error::Secp256k1(e) + fn parse(&self) -> Result { + PkcRecSigBytes::from_raw(self.0).ok_or(EcdsaError::InvalidRecoveryId)?.try_into() } -} -impl From for Error { - fn from(err: hex_conservative::DecodeVariableLengthBytesError) -> Self { - Error::HexEncoding(err) + delegate! { + to self.parse()? { + /// Whether the signing key serializes compressed. + #[expr(Ok($))] + pub fn is_compressed(&self) -> Result; + + /// The recovery id, in `0..=3`. + #[expr(Ok($))] + pub fn recovery_id(&self) -> Result; + + /// The signature without its recovery id. + #[expr(Ok(EcdsaSignature(*$)))] + pub fn signature(&self) -> Result; + } } } @@ -279,23 +300,168 @@ mod tests { // A DER signature with `SIGHASH_ALL` appended. const SIG_HEX: &str = "3045022100e85425f6d7c589972ee061413bcf08dc8c8e589ce37b217535a42af924f0e4d602205c9ba9cb14ef15513c9d946fa1c4b797883e748e8c32171bdf6166583946e35c01"; - /// Formatting flags are ignored, as `hex-conservative` 1.x would otherwise - /// pad and prefix each part of the signature separately. + /// Formatting flags apply to the whole encoding, as for a string. #[test] - fn display_ignores_format_flags() { + fn display_applies_format_flags() { let sig = Signature::from_str(SIG_HEX).unwrap(); - for out in [ - format!("{}", sig), - format!("{:#}", sig), - format!("{:>200}", sig), - format!("{:.4}", sig), - ] { - assert_eq!(out, SIG_HEX); - } + assert_eq!(format!("{}", sig), SIG_HEX); + assert_eq!(format!("{:#}", sig), format!("0x{}", SIG_HEX)); + assert_eq!(format!("{:>200}", sig), format!("{:>200}", SIG_HEX)); + assert_eq!(format!("{:.4}", sig), SIG_HEX[..4]); let serialized = sig.serialize(); assert_eq!(format!("{:x}", serialized), SIG_HEX); - assert_eq!(format!("{:#x}", serialized), SIG_HEX); + assert_eq!(format!("{:#x}", serialized), format!("0x{}", SIG_HEX)); assert_eq!(format!("{:X}", serialized), SIG_HEX.to_uppercase()); } + + /// Signing grinds a low R, as Dash Core does, so DER signatures fit in 70 + /// bytes; the result verifies and survives both encodings. + #[test] + fn sign_is_low_r_and_round_trips() { + let sk = EcdsaSecretKey::from_bytes(&[0x42; ECDSA_SK_LEN]).expect("valid secret key"); + let pk = sk.public_key(); + for i in 0..32u8 { + let msg = [i; 32]; + let sig = sk.sign(&msg); + assert!(sig.to_bytes()[0] < 0x80, "R must be low"); + assert!(sig.with_der(<[u8]>::len) <= 70); + assert_eq!(sig, sk.sign(&msg), "signing is deterministic"); + pk.verify(&msg, sig).expect("signature verifies"); + assert_eq!(pk.verify(&[i ^ 1; 32], sig), Err(EcdsaError::VerifyFailed)); + assert_eq!(EcdsaSignature::from_bytes(&sig.to_bytes()), Ok(sig)); + assert_eq!(sig.with_der(EcdsaSignature::from_der), Ok(sig)); + } + } + + #[test] + fn rec_signature_keeps_compression() { + let sk = EcdsaSecretKey::from_bytes(&[0x42; ECDSA_SK_LEN]).expect("valid secret key"); + let rec = sk.sign_recoverable(&[0x07; 32]); + assert_eq!(rec.is_compressed(), Ok(true)); + + let sig = rec.signature().unwrap(); + let id = rec.recovery_id().unwrap(); + assert_eq!(EcdsaRecSignature::from_parts(sig, id, true), Ok(rec)); + let legacy = EcdsaRecSignature::from_parts(sig, id, false).unwrap(); + assert_eq!(legacy.is_compressed(), Ok(false)); + assert_ne!(legacy, rec); + assert_eq!(EcdsaRecSignature::from_parts(sig, 4, true), Err(EcdsaError::InvalidRecoveryId)); + } + + #[test] + fn rec_signature_carries_the_core_header() { + let sk = EcdsaSecretKey::from_bytes(&[0x42; ECDSA_SK_LEN]).expect("valid secret key"); + let rec = sk.sign_recoverable(&[0x07; 32]); + let id = rec.recovery_id().unwrap(); + assert_eq!(rec[0], 31 + id); + assert_eq!(rec[1..], rec.signature().unwrap().to_bytes()); + + let legacy = EcdsaRecSignature::from_parts(rec.signature().unwrap(), id, false); + assert_eq!(legacy.unwrap()[0], 27 + id); + assert_eq!(rec.to_string().parse::(), Ok(rec)); + } + + #[test] + fn rec_signature_is_checked_when_read() { + let pk = EcdsaSecretKey::from_bytes(&[0x42; ECDSA_SK_LEN]).unwrap().public_key(); + for header in [26, 35] { + let mut raw = [0x01; ECDSA_REC_SIG_LEN]; + raw[0] = header; + let rec = EcdsaRecSignature::from(raw); + assert_eq!(rec.to_bytes(), raw, "raw bytes stay unchecked"); + assert_eq!(rec.recovery_id(), Err(EcdsaError::InvalidRecoveryId)); + assert_eq!( + EcdsaPublicKey::recover(&[0x07; 32], &rec), + Err(EcdsaError::InvalidRecoveryId) + ); + } + let mut raw = [0xff; ECDSA_REC_SIG_LEN]; + raw[0] = 31; + let rec = EcdsaRecSignature::from(raw); + assert_eq!(rec.signature(), Err(EcdsaError::InvalidSignature)); + assert_ne!(EcdsaPublicKey::recover(&[0x07; 32], &rec), Ok(pk)); + } + + #[cfg(feature = "serde")] + #[test] + fn rec_signature_serde_layout_is_hex_or_bytes() { + let bytes = EcdsaRecSignature::from([0x22; ECDSA_REC_SIG_LEN]); + let hex = "22".repeat(ECDSA_REC_SIG_LEN); + + let json = serde_json::to_string(&bytes).expect("serialize signature"); + assert_eq!(json, format!("\"{}\"", hex)); + assert_eq!(serde_json::from_str::(&json).expect("deserialize"), bytes); + + #[cfg(feature = "bincode")] + { + let config = bincode::config::standard(); + let raw = bincode::encode_to_vec(bytes, config).expect("encode signature"); + assert_eq!(raw, [0x22; ECDSA_REC_SIG_LEN]); + + let encoded = bincode::serde::encode_to_vec(bytes, config).expect("encode signature"); + assert_eq!(encoded, [&[65u8][..], &[0x22; ECDSA_REC_SIG_LEN]].concat()); + let (decoded, _): (EcdsaRecSignature, _) = + bincode::serde::decode_from_slice(&encoded, config).expect("decode signature"); + assert_eq!(decoded, bytes); + } + } + + #[cfg(feature = "serde")] + const DER_HEX: &str = + "3045022100d2e84c0a1a8e0d31d1b0f8b3cde37ab19ef8e6bff4f72c5ac6f0a4b8a8d1ec2d\ + 02204c7b8e3f2a5d6c1b0e9f8a7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39"; + #[cfg(feature = "serde")] + const COMPACT_HEX: &str = "d2e84c0a1a8e0d31d1b0f8b3cde37ab19ef8e6bff4f72c5ac6f0a4b8a8d1ec2d\ + 4c7b8e3f2a5d6c1b0e9f8a7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39"; + + /// The signature serializes as DER, a hex string in human-readable formats + /// and a byte string otherwise. + #[cfg(feature = "serde")] + #[test] + fn serde_layout_is_der() { + let sig = Signature::from_str(&format!("{}01", DER_HEX)).expect("parse signature"); + + let json = serde_json::to_string(&sig).expect("serialize signature"); + assert_eq!( + json, + format!("{{\"signature\":\"{}\",\"sighash_type\":\"SIGHASH_ALL\"}}", DER_HEX) + ); + assert_eq!(serde_json::from_str::(&json).expect("deserialize signature"), sig); + + let config = bincode::config::standard(); + let bytes = bincode::serde::encode_to_vec(sig, config).expect("encode signature"); + let mut expected = vec![0x47]; + expected.extend(hex_conservative::decode_to_vec(DER_HEX).expect("decode hex")); + expected.push(0x0b); + expected.extend(b"SIGHASH_ALL"); + assert_eq!(bytes, expected); + let (decoded, _): (Signature, _) = + bincode::serde::decode_from_slice(&bytes, config).expect("decode signature"); + assert_eq!(decoded, sig); + } + + /// The signature takes hex in either case. Anything other than one DER + /// encoding is rejected in both formats. + #[cfg(feature = "serde")] + #[test] + fn serde_input_is_der_only() { + let json = + |sig: &str| format!("{{\"signature\":\"{}\",\"sighash_type\":\"SIGHASH_ALL\"}}", sig); + let sig = Signature::from_str(&format!("{}01", DER_HEX)).expect("parse signature"); + + let upper = serde_json::from_str::(&json(&DER_HEX.to_uppercase())); + assert_eq!(upper.expect("uppercase hex"), sig); + let trailing = format!("{}00", DER_HEX); + for bad in [COMPACT_HEX, &trailing, &DER_HEX[..DER_HEX.len() - 1]] { + assert!(serde_json::from_str::(&json(bad)).is_err(), "{}", bad); + } + + let mut bytes = vec![0x40]; + bytes.extend(hex_conservative::decode_to_vec(COMPACT_HEX).expect("decode hex")); + bytes.push(0x0b); + bytes.extend(b"SIGHASH_ALL"); + let config = bincode::config::standard(); + assert!(bincode::serde::decode_from_slice::(&bytes, config).is_err()); + } } diff --git a/crypto/src/key.rs b/crypto/src/key.rs index bb02ad5f3..47fd32ad1 100644 --- a/crypto/src/key.rs +++ b/crypto/src/key.rs @@ -11,116 +11,72 @@ //! (de)serialized. use core::fmt::{self, Write}; -use core::ops; use core::str::FromStr; use std::io; +#[cfg(feature = "rand")] +pub use bitcoin_crypto::key::rand; +pub use bitcoin_crypto::key::{ + constants, Keypair, Parity, ParseXOnlyPublicKeyError, TweakXOnlyPublicKeyError, TweakedKeypair, + TweakedPublicKey, UntweakedKeypair, UntweakedPublicKey, XOnlyPublicKey, +}; +pub use bitcoin_crypto::secp256k1::Scalar; use dash_network::Network; +#[cfg(feature = "serde")] +use dash_types::serialize::hex as serde_hex; use hashes::{hash160, hash_newtype, Hash as _}; use hex_conservative::DisplayHex; -use internals::write_err; -pub use secp256k1::{self, constants, Keypair, Parity, Secp256k1, Verification, XOnlyPublicKey}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; +use thiserror::Error as ThisError; use crate::base58; +use crate::ecdsa::{ + EcdsaError, EcdsaPublicKey, EcdsaSecretKey, ECDSA_PK_LEN, ECDSA_PK_UNCOMPRESSED_LEN, + ECDSA_SK_LEN, +}; /// A key-related error. -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, ThisError)] #[non_exhaustive] pub enum Error { /// Base58 encoding error - Base58(base58::DecodeCheckError), - /// secp256k1-related error - Secp256k1(secp256k1::Error), + #[error("key base58 error")] + Base58(#[from] base58::DecodeCheckError), + /// ECDSA key error + #[error("key ECDSA error")] + Ecdsa(#[from] EcdsaError), /// Invalid key prefix error + #[error("key prefix invalid: {0}")] InvalidKeyPrefix(u8), /// The WIF or extended key version byte was not one we recognise. + #[error("address version {0} is invalid for this base58 type")] InvalidAddressVersion(u8), /// The base58 decoded correctly but the payload was the wrong length. + #[error("length {0} invalid for this base58 type")] InvalidBase58PayloadLength(usize), /// A 34-byte WIF payload ended in something other than the `0x01` /// compression flag. + #[error("WIF compression flag must be 0x01, got: {0:#04x}")] InvalidWifCompressionFlag(u8), /// Hex decoding error - Hex(hex_conservative::DecodeFixedLengthBytesError), + #[error("key hex decoding error")] + Hex(#[from] hex_conservative::DecodeFixedLengthBytesError), /// `PublicKey` hex should be 66 or 130 digits long. + #[error("PublicKey hex should be 66 or 130 digits long, got: {0}")] InvalidHexLength(usize), } -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::Base58(e) => write_err!(f, "key base58 error"; e), - Error::Secp256k1(e) => write_err!(f, "key secp256k1 error"; e), - Error::InvalidAddressVersion(v) => { - write!(f, "address version {} is invalid for this base58 type", v) - } - Error::InvalidBase58PayloadLength(l) => { - write!(f, "length {} invalid for this base58 type", l) - } - Error::InvalidKeyPrefix(b) => write!(f, "key prefix invalid: {}", b), - Error::InvalidWifCompressionFlag(b) => { - write!(f, "WIF compression flag must be 0x01, got: {:#04x}", b) - } - Error::Hex(e) => write_err!(f, "key hex decoding error"; e), - Error::InvalidHexLength(got) => { - write!(f, "PublicKey hex should be 66 or 130 digits long, got: {}", got) - } - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use self::Error::*; - - match self { - Base58(e) => Some(e), - Secp256k1(e) => Some(e), - Hex(e) => Some(e), - InvalidAddressVersion(_) - | InvalidBase58PayloadLength(_) - | InvalidKeyPrefix(_) - | InvalidWifCompressionFlag(_) - | InvalidHexLength(_) => None, - } - } -} - -#[doc(hidden)] -impl From for Error { - fn from(e: base58::DecodeCheckError) -> Error { - Error::Base58(e) - } -} - -#[doc(hidden)] -impl From for Error { - fn from(e: secp256k1::Error) -> Error { - Error::Secp256k1(e) - } -} - -#[doc(hidden)] -impl From for Error { - fn from(e: hex_conservative::DecodeFixedLengthBytesError) -> Self { - Error::Hex(e) - } -} - /// A Dash ECDSA public key #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct PublicKey { /// Whether this public key should be serialized as compressed pub compressed: bool, /// The actual ECDSA key - pub inner: secp256k1::PublicKey, + pub inner: EcdsaPublicKey, } impl PublicKey { /// Constructs compressed ECDSA public key from the provided generic Secp256k1 public key - pub fn new(key: impl Into) -> PublicKey { + pub fn new(key: impl Into) -> PublicKey { PublicKey { compressed: true, inner: key.into(), @@ -129,7 +85,7 @@ impl PublicKey { /// Constructs uncompressed (legacy) ECDSA public key from the provided generic Secp256k1 /// public key - pub fn new_uncompressed(key: impl Into) -> PublicKey { + pub fn new_uncompressed(key: impl Into) -> PublicKey { PublicKey { compressed: false, inner: key.into(), @@ -140,9 +96,9 @@ impl PublicKey { /// `compressed` says, without allocating. pub fn with_serialized R>(&self, f: F) -> R { if self.compressed { - f(&self.inner.serialize()) + f(&self.inner.to_compressed()) } else { - f(&self.inner.serialize_uncompressed()) + f(&self.inner.to_uncompressed()) } } @@ -160,28 +116,18 @@ impl PublicKey { /// Deserialize a public key from a slice pub fn from_slice(data: &[u8]) -> Result { - let (compressed, inner) = match data.len() { - constants::PUBLIC_KEY_SIZE => { - let data = <[u8; constants::PUBLIC_KEY_SIZE]>::try_from(data) - .map_err(|_| Error::Secp256k1(secp256k1::Error::InvalidPublicKey))?; - (true, secp256k1::PublicKey::from_byte_array_compressed(data)?) - } - constants::UNCOMPRESSED_PUBLIC_KEY_SIZE => { - if data[0] != 0x04 { - return Err(Error::InvalidKeyPrefix(data[0])); - } - let data = <[u8; constants::UNCOMPRESSED_PUBLIC_KEY_SIZE]>::try_from(data) - .map_err(|_| Error::Secp256k1(secp256k1::Error::InvalidPublicKey))?; - (false, secp256k1::PublicKey::from_byte_array_uncompressed(data)?) - } - len => { - return Err(Error::InvalidBase58PayloadLength(len)); + let compressed = match (data.len(), data.first()) { + (ECDSA_PK_LEN, _) => true, + (ECDSA_PK_UNCOMPRESSED_LEN, Some(&0x04)) => false, + (ECDSA_PK_UNCOMPRESSED_LEN, Some(&prefix)) => { + return Err(Error::InvalidKeyPrefix(prefix)); } + (len, _) => return Err(Error::InvalidBase58PayloadLength(len)), }; Ok(PublicKey { compressed, - inner, + inner: EcdsaPublicKey::from_bytes(data)?, }) } @@ -197,15 +143,7 @@ impl PublicKey { /// Returns dash 160-bit hash of the public key for witness program pub fn wpubkey_hash(&self) -> Option { - if self.compressed { - Some(WPubkeyHash::from_byte_array( - hash160::Hash::hash(&self.inner.serialize()).to_byte_array(), - )) - } else { - // We can't create witness pubkey hashes for an uncompressed - // public keys - None - } + self.compressed.then(|| WPubkeyHash::hash(&self.inner.to_compressed())) } } @@ -239,35 +177,43 @@ impl FromStr for PublicKey { } } +impl TryFrom<&[u8]> for PublicKey { + type Error = Error; + + fn try_from(data: &[u8]) -> Result { + PublicKey::from_slice(data) + } +} + /// A Dash ECDSA private key -#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub struct PrivateKey { /// Whether this private key should be serialized as compressed pub compressed: bool, /// The network on which this key should be used pub network: Network, /// The actual ECDSA key - pub inner: secp256k1::SecretKey, + pub inner: EcdsaSecretKey, } impl PrivateKey { /// Constructs compressed ECDSA private key from the provided generic Secp256k1 private key /// and the specified network - pub fn new(key: secp256k1::SecretKey, network: Network) -> PrivateKey { + pub fn new(key: impl Into, network: Network) -> PrivateKey { PrivateKey { compressed: true, network, - inner: key, + inner: key.into(), } } /// Constructs uncompressed (legacy) ECDSA private key from the provided generic Secp256k1 /// private key and the specified network - pub fn new_uncompressed(key: secp256k1::SecretKey, network: Network) -> PrivateKey { + pub fn new_uncompressed(key: impl Into, network: Network) -> PrivateKey { PrivateKey { compressed: false, network, - inner: key, + inner: key.into(), } } @@ -280,20 +226,20 @@ impl PrivateKey { } /// Serialize the private key to bytes - pub fn to_bytes(self) -> Vec { - self.inner[..].to_vec() + pub fn to_bytes(&self) -> Vec { + self.inner.to_bytes().to_vec() } /// Deserialize a private key from a slice #[deprecated(since = "0.40.0", note = "Use `from_byte_array` instead.")] pub fn from_slice(data: &[u8], network: Network) -> Result { - let data = <[u8; constants::SECRET_KEY_SIZE]>::try_from(data) - .map_err(|_| Error::Secp256k1(secp256k1::Error::InvalidSecretKey))?; + let data = <[u8; ECDSA_SK_LEN]>::try_from(data) + .map_err(|_| Error::Ecdsa(EcdsaError::InvalidSecretKey))?; PrivateKey::from_byte_array(&data, network) } pub fn from_byte_array(data: &[u8; 32], network: Network) -> Result { - Ok(PrivateKey::new(secp256k1::SecretKey::from_secret_bytes(*data)?, network)) + Ok(PrivateKey::new(EcdsaSecretKey::from_bytes(data)?, network)) } /// Format the private key to WIF format. @@ -303,7 +249,7 @@ impl PrivateKey { Network::Mainnet => 204, Network::Testnet | Network::Devnet | Network::Regtest => 239, }; - ret[1..33].copy_from_slice(&self.inner[..]); + ret[1..33].copy_from_slice(&*self.inner.to_bytes()); let privkey = if self.compressed { ret[33] = 1; base58::Base58CkString::encode_unbounded(&ret[..]) @@ -314,7 +260,7 @@ impl PrivateKey { } /// Get WIF encoding of this private key. - pub fn to_wif(self) -> String { + pub fn to_wif(&self) -> String { let mut buf = String::new(); buf.write_fmt(format_args!("{}", self)).unwrap(); buf.shrink_to_fit(); @@ -345,13 +291,13 @@ impl PrivateKey { }; let secret = data[1..] - .first_chunk::<{ constants::SECRET_KEY_SIZE }>() + .first_chunk::() .ok_or(Error::InvalidBase58PayloadLength(data.len()))?; Ok(PrivateKey { compressed, network, - inner: secp256k1::SecretKey::from_secret_bytes(*secret)?, + inner: EcdsaSecretKey::from_bytes(secret)?, }) } } @@ -369,13 +315,6 @@ impl FromStr for PrivateKey { } } -impl ops::Index for PrivateKey { - type Output = [u8]; - fn index(&self, _: ops::RangeFull) -> &[u8] { - &self.inner[..] - } -} - #[cfg(feature = "serde")] impl serde::Serialize for PrivateKey { fn serialize(&self, s: S) -> Result { @@ -422,186 +361,13 @@ impl<'de> serde::Deserialize<'de> for PrivateKey { #[allow(clippy::collapsible_else_if)] // Aids readability. impl serde::Serialize for PublicKey { fn serialize(&self, s: S) -> Result { - if s.is_human_readable() { - s.collect_str(self) - } else { - self.with_serialized(|bytes| s.serialize_bytes(bytes)) - } + self.with_serialized(|bytes| serde_hex::serialize_as(bytes, self, s)) } } #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for PublicKey { fn deserialize>(d: D) -> Result { - if d.is_human_readable() { - struct HexVisitor; - - impl<'de> serde::de::Visitor<'de> for HexVisitor { - type Value = PublicKey; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an ASCII hex string") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - PublicKey::from_str(v).map_err(E::custom) - } - - fn visit_bytes(self, v: &[u8]) -> Result - where - E: serde::de::Error, - { - if let Ok(hex) = core::str::from_utf8(v) { - PublicKey::from_str(hex).map_err(E::custom) - } else { - Err(E::invalid_value(serde::de::Unexpected::Bytes(v), &self)) - } - } - } - d.deserialize_str(HexVisitor) - } else { - struct BytesVisitor; - - impl<'de> serde::de::Visitor<'de> for BytesVisitor { - type Value = PublicKey; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a bytestring") - } - - fn visit_bytes(self, v: &[u8]) -> Result - where - E: serde::de::Error, - { - PublicKey::from_slice(v).map_err(E::custom) - } - } - - d.deserialize_bytes(BytesVisitor) - } - } -} - -/// Untweaked BIP-340 X-coord-only public key -pub type UntweakedPublicKey = XOnlyPublicKey; - -/// Tweaked BIP-340 X-coord-only public key -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -pub struct TweakedPublicKey(XOnlyPublicKey); - -impl fmt::LowerHex for TweakedPublicKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::LowerHex::fmt(&self.0, f) - } -} - -impl fmt::Display for TweakedPublicKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - -/// Untweaked BIP-340 key pair -pub type UntweakedKeyPair = Keypair; - -/// Tweaked BIP-340 key pair -/// -/// # Examples -/// ``` -/// # #[cfg(feature = "rand-std")] { -/// # use dashcore::key::{Keypair, TweakedKeyPair, TweakedPublicKey}; -/// # use dashcore::secp256k1::rand; -/// # let keypair = TweakedKeyPair::dangerous_assume_tweaked(Keypair::new(&mut rand::rng())); -/// // There are various conversion methods available to get a tweaked pubkey from a tweaked keypair. -/// let (_pk, _parity) = keypair.public_parts(); -/// let _pk = TweakedPublicKey::from_keypair(keypair); -/// let _pk = TweakedPublicKey::from(keypair); -/// # } -/// ``` -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -pub struct TweakedKeyPair(Keypair); - -impl TweakedPublicKey { - /// Returns the [`TweakedPublicKey`] for `keypair`. - #[inline] - pub fn from_keypair(keypair: TweakedKeyPair) -> Self { - let (xonly, _parity) = keypair.0.x_only_public_key(); - TweakedPublicKey(xonly) - } - - /// Creates a new [`TweakedPublicKey`] from a [`XOnlyPublicKey`]. No tweak is applied, consider - /// calling `tap_tweak` on an [`UntweakedPublicKey`] instead of using this constructor. - /// - /// This method is dangerous and can lead to loss of funds if used incorrectly. - /// Specifically, in multi-party protocols a peer can provide a value that allows them to steal. - #[inline] - pub fn dangerous_assume_tweaked(key: XOnlyPublicKey) -> TweakedPublicKey { - TweakedPublicKey(key) - } - - /// Returns the underlying public key. - pub fn to_inner(self) -> XOnlyPublicKey { - self.0 - } - - /// Serialize the key as a byte-encoded pair of values. In compressed form - /// the y-coordinate is represented by only a single bit, as x determines - /// it up to one bit. - #[inline] - pub fn serialize(&self) -> [u8; constants::SCHNORR_PUBLIC_KEY_SIZE] { - self.0.to_byte_array() - } -} - -impl TweakedKeyPair { - /// Creates a new [`TweakedKeyPair`] from a `KeyPair`. No tweak is applied, consider - /// calling `tap_tweak` on an [`UntweakedKeyPair`] instead of using this constructor. - /// - /// This method is dangerous and can lead to loss of funds if used incorrectly. - /// Specifically, in multi-party protocols a peer can provide a value that allows them to steal. - #[inline] - pub fn dangerous_assume_tweaked(pair: Keypair) -> TweakedKeyPair { - TweakedKeyPair(pair) - } - - /// Returns the underlying key pair. - #[inline] - pub fn to_inner(self) -> Keypair { - self.0 - } - - /// Returns the [`TweakedPublicKey`] and its [`Parity`] for this [`TweakedKeyPair`]. - #[inline] - pub fn public_parts(&self) -> (TweakedPublicKey, Parity) { - let (xonly, parity) = self.0.x_only_public_key(); - (TweakedPublicKey(xonly), parity) - } -} - -impl From for XOnlyPublicKey { - #[inline] - fn from(pair: TweakedPublicKey) -> Self { - pair.0 - } -} - -impl From for Keypair { - #[inline] - fn from(pair: TweakedKeyPair) -> Self { - pair.0 - } -} - -impl From for TweakedPublicKey { - #[inline] - fn from(pair: TweakedKeyPair) -> Self { - TweakedPublicKey::from_keypair(pair) + serde_hex::deserialize_as(d, PublicKey::from_str) } } diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index b1f873394..4b77dd583 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -14,7 +14,6 @@ extern crate alloc; pub extern crate base58ck as base58; pub extern crate dash_pkc; pub extern crate dashcore_hashes as hashes; -pub extern crate secp256k1; #[cfg(feature = "serde")] pub extern crate serde; diff --git a/dash-spv-ffi/src/callbacks.rs b/dash-spv-ffi/src/callbacks.rs index 44eb76986..84b539d63 100644 --- a/dash-spv-ffi/src/callbacks.rs +++ b/dash-spv-ffi/src/callbacks.rs @@ -698,7 +698,7 @@ impl FFIDerivedAddress { pool_type: FFIDerivedAddressPoolType::from(d.pool_type), derivation_index: d.derivation_index, address: c_address.into_raw(), - public_key: d.public_key.inner.serialize(), + public_key: d.public_key.inner.to_compressed(), } }) .collect() diff --git a/dash/Cargo.toml b/dash/Cargo.toml index b37366ce8..21978aa9f 100644 --- a/dash/Cargo.toml +++ b/dash/Cargo.toml @@ -18,14 +18,12 @@ version.workspace = true # Please don't forget to add relevant features to docs.rs below [features] -default = ["secp-recovery", "bincode" ] +default = ["bincode"] base64 = [ "dep:base64" ] -rand-std = ["secp256k1/rand"] -rand = ["secp256k1/rand"] -serde = ["dep:serde", "dashcore-crypto/serde", "dashcore_hashes/serde", "secp256k1/serde", "dash-network/serde"] -secp-lowmemory = ["secp256k1/lowmemory"] -secp-recovery = ["secp256k1/recovery"] -signer = ["secp-recovery", "rand", "base64"] +rand-std = ["rand"] +rand = ["dashcore-crypto/rand"] +serde = ["dep:serde", "dashcore-crypto/serde", "dashcore_hashes/serde", "dash-network/serde", "dash-types/serde"] +signer = ["rand", "base64"] core-block-hash-use-x11 = ["dashcore_hashes/x11"] bls = ["dashcore-crypto/bls"] eddsa = ["dashcore-crypto/eddsa"] @@ -45,8 +43,7 @@ dashcore_hashes = { path = "../hashes" } dashcore-crypto = { path = "../crypto" } dash-network = { path = "../dash-network" } dash-num = { workspace = true } -dash-types = { workspace = true, features = ["codec"] } -secp256k1 = { workspace = true } +dash-types = { workspace = true } serde = { workspace = true, features = ["derive", "alloc", "rc"], optional = true } base64 = { version = "0.22", optional = true } @@ -64,7 +61,7 @@ tracing = "0.1" serde_json = "1.0.140" serde_test = "1.0.177" serde_derive = "1.0.219" -secp256k1 = { workspace = true, features = ["recovery", "rand"] } +dashcore-crypto = { path = "../crypto", features = ["rand"] } bincode = { workspace = true, features = ["serde"] } assert_matches = "1.5.0" test-case = "3.3" diff --git a/dash/examples/handshake.rs b/dash/examples/handshake.rs index 2f7a5ab67..c6e5e9cd1 100644 --- a/dash/examples/handshake.rs +++ b/dash/examples/handshake.rs @@ -5,11 +5,11 @@ use std::net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, TcpStream}; use std::time::{SystemTime, UNIX_EPOCH}; use std::{env, process}; +use dashcore::Network; use dashcore::consensus::{Decodable, encode}; +use dashcore::key::rand; use dashcore::network::{address, constants, message, message_network}; -use dashcore::secp256k1::rand::Rng; -use dashcore::{Network, secp256k1}; -use secp256k1::rand; +use rand::Rng; fn main() { // This example establishes a connection to a Bitcoin node, sends the initial diff --git a/dash/src/address.rs b/dash/src/address.rs index 0a371b3a4..ad9bd68c9 100644 --- a/dash/src/address.rs +++ b/dash/src/address.rs @@ -23,11 +23,11 @@ //! //! ```rust //! # #[cfg(feature = "rand-std")] { +//! use dashcore::ecdsa::EcdsaSecretKey; //! use dashcore::{Address, PublicKey, Network}; -//! use dashcore::secp256k1::{self, rand}; //! //! // Generate random key pair. -//! let public_key = PublicKey::new(secp256k1::generate_keypair(&mut rand::rng()).1); +//! let public_key = PublicKey::new(EcdsaSecretKey::generate().public_key()); //! //! // Generate pay-to-pubkey-hash address. //! let address = Address::p2pkh(&public_key, Network::Mainnet); @@ -45,10 +45,10 @@ use core::fmt; use core::marker::PhantomData; use core::str::FromStr; +use crate::crypto::key::XOnlyPublicKey; use bech32; use hashes::{Hash, HashEngine, sha256}; use internals::write_err; -use secp256k1::XOnlyPublicKey; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -599,7 +599,7 @@ impl Payload { /// Create a pay to taproot payload from untweaked key pub fn p2tr(internal_key: UntweakedPublicKey, merkle_root: Option) -> Payload { let (output_key, _parity) = internal_key.tap_tweak(merkle_root); - let prog = WitnessProgram::new(WitnessVersion::V1, output_key.to_inner().to_byte_array()) + let prog = WitnessProgram::new(WitnessVersion::V1, output_key.serialize()) .expect("taproot output key has len 32 <= 40"); Payload::WitnessProgram(prog) } @@ -608,7 +608,7 @@ impl Payload { /// /// This method is not recommended for use and [Payload::p2tr()] should be used where possible. pub fn p2tr_tweaked(output_key: TweakedPublicKey) -> Payload { - let prog = WitnessProgram::new(WitnessVersion::V1, output_key.to_inner().to_byte_array()) + let prog = WitnessProgram::new(WitnessVersion::V1, output_key.serialize()) .expect("taproot output key has len 32 <= 40"); Payload::WitnessProgram(prog) } @@ -1227,7 +1227,7 @@ impl Address { let xonly_pubkey = XOnlyPublicKey::from(pubkey.inner); (*pubkey_hash.as_byte_array() == *payload) - || (xonly_pubkey.to_byte_array() == *payload) + || (xonly_pubkey.serialize().0 == *payload) || (*segwit_redeem_hash(&pubkey_hash).as_byte_array() == *payload) } @@ -1237,7 +1237,7 @@ impl Address { /// assumed to have already been tweaked. pub fn is_related_to_xonly_pubkey(&self, xonly_pubkey: &XOnlyPublicKey) -> bool { let payload = self.payload().inner_prog_as_bytes(); - payload == xonly_pubkey.to_byte_array() + payload == xonly_pubkey.serialize().0 } /// Returns true if the address creates a particular script @@ -1482,8 +1482,8 @@ fn segwit_redeem_hash(pubkey_hash: &PubkeyHash) -> crate::hashes::hash160::Hash mod tests { use core::str::FromStr; + use crate::crypto::key::XOnlyPublicKey; use hex_conservative::hex; - use secp256k1::XOnlyPublicKey; use super::*; use crate::Network::{Mainnet, Testnet}; diff --git a/dash/src/bip152.rs b/dash/src/bip152.rs index 58fee511b..d3313c001 100644 --- a/dash/src/bip152.rs +++ b/dash/src/bip152.rs @@ -10,10 +10,10 @@ use core::{convert, fmt, mem}; use std::error; use hashes::{Hash, sha256, siphash24}; -use internals::impl_array_newtype; +use internals::{impl_array_newtype, impl_bytes_newtype}; use crate::consensus::encode::{self, Decodable, Encodable, VarInt}; -use crate::internal_macros::{impl_bytes_newtype, impl_consensus_encoding}; +use crate::internal_macros::impl_consensus_encoding; use crate::prelude::*; use crate::{Block, BlockHash, Transaction, block, io}; diff --git a/dash/src/blockdata/script/builder.rs b/dash/src/blockdata/script/builder.rs index c6b55f9ed..25dc0c69c 100644 --- a/dash/src/blockdata/script/builder.rs +++ b/dash/src/blockdata/script/builder.rs @@ -6,7 +6,7 @@ use core::convert::From; use core::default::Default; use core::fmt; -use secp256k1::XOnlyPublicKey; +use crate::crypto::key::XOnlyPublicKey; use crate::blockdata::locktime::absolute; use crate::blockdata::opcodes::all::*; @@ -80,7 +80,7 @@ impl Builder { /// Adds instructions to push an XOnly public key onto the stack. pub fn push_x_only_key(self, x_only_key: &XOnlyPublicKey) -> Builder { - self.push_slice(x_only_key.to_byte_array()) + self.push_slice(x_only_key.serialize().0) } /// Adds a single opcode to the script. diff --git a/dash/src/blockdata/script/tests.rs b/dash/src/blockdata/script/tests.rs index b9f2762a8..5b88f077c 100644 --- a/dash/src/blockdata/script/tests.rs +++ b/dash/src/blockdata/script/tests.rs @@ -203,10 +203,10 @@ fn script_generators() { .unwrap(); assert!(ScriptBuf::new_p2pk(&pubkey).is_p2pk()); - let pubkey_hash = PubkeyHash::hash(&pubkey.inner.serialize()); + let pubkey_hash = PubkeyHash::hash(&pubkey.inner.to_compressed()); assert!(ScriptBuf::new_p2pkh(&pubkey_hash).is_p2pkh()); - let wpubkey_hash = WPubkeyHash::hash(&pubkey.inner.serialize()); + let wpubkey_hash = WPubkeyHash::hash(&pubkey.inner.to_compressed()); assert!(ScriptBuf::new_v0_p2wpkh(&wpubkey_hash).is_v0_p2wpkh()); let script = Builder::new().push_opcode(OP_NUMEQUAL).push_verify().into_script(); diff --git a/dash/src/blockdata/transaction/mod.rs b/dash/src/blockdata/transaction/mod.rs index 7dc4b7b4d..b41009835 100644 --- a/dash/src/blockdata/transaction/mod.rs +++ b/dash/src/blockdata/transaction/mod.rs @@ -1004,11 +1004,11 @@ mod tests { use super::*; use crate::blockdata::constants::WITNESS_SCALE_FACTOR; use crate::blockdata::transaction::special_transaction::provider_dissolve::ProviderDissolvePayload; - use crate::blockdata::transaction::special_transaction::provider_registration::CompactSignature; use crate::blockdata::transaction::special_transaction::provider_update_share::ProviderUpdateSharePayload; use crate::blockdata::transaction::special_transaction::provider_update_shared_registrar::ProviderUpdateSharedRegistrarPayload; use crate::bls_sig_utils::BLSPublicKey; use crate::consensus::encode::{deserialize, serialize}; + use crate::crypto::ecdsa::EcdsaRecSignature; use crate::hash_types::PubkeyHash; use crate::internal_macros::hex; use crate::network::message::{NetworkMessage, RawNetworkMessage}; @@ -1340,7 +1340,7 @@ mod tests { version: 1, pro_tx_hash: Txid::all_zeros(), actor_index: 0, - signatures: vec![CompactSignature::from([1; 65])], + signatures: vec![EcdsaRecSignature::from([1; 65])], }); "ProDisTx")] #[test_case(11, TransactionPayload::ProviderUpdateSharePayloadType(ProviderUpdateSharePayload { version: 1, @@ -1357,7 +1357,7 @@ mod tests { operator_public_key: BLSPublicKey::from([1; 48]), voting_key_hash: PubkeyHash::all_zeros(), inputs_hash: InputsHash::all_zeros(), - signatures: vec![CompactSignature::from([1; 65])], + signatures: vec![EcdsaRecSignature::from([1; 65])], } ); "ProUpSharedRegTx")] fn shared_masternode_types_decode_typed(tx_type: u16, payload: TransactionPayload) { diff --git a/dash/src/blockdata/transaction/special_transaction/provider_dissolve.rs b/dash/src/blockdata/transaction/special_transaction/provider_dissolve.rs index 90ff6e1e2..57c262bba 100644 --- a/dash/src/blockdata/transaction/special_transaction/provider_dissolve.rs +++ b/dash/src/blockdata/transaction/special_transaction/provider_dissolve.rs @@ -16,9 +16,10 @@ use bincode::{Decode, Encode}; use crate::blockdata::transaction::special_transaction::provider_registration::{ - CompactSignature, decode_compact_signatures, encode_compact_signatures, + decode_compact_signatures, encode_compact_signatures, }; use crate::consensus::{Decodable, Encodable, encode}; +use crate::crypto::ecdsa::EcdsaRecSignature; use crate::hash_types::Txid; use crate::io; use crate::prelude::*; @@ -33,7 +34,7 @@ pub struct ProviderDissolvePayload { /// Index of the share dissolving the masternode. pub actor_index: u16, /// One signature for a unilateral dissolution, one per share for a unanimous one. - pub signatures: Vec, + pub signatures: Vec, } impl ProviderDissolvePayload { @@ -82,8 +83,8 @@ mod tests { pro_tx_hash: Txid::from_byte_array([0x11; 32]), actor_index: 2, signatures: vec![ - CompactSignature::from([0x22; 65]), - CompactSignature::from([0x33; 65]), + EcdsaRecSignature::from([0x22; 65]), + EcdsaRecSignature::from([0x33; 65]), ], }; let mut want = vec![0x01, 0x00]; diff --git a/dash/src/blockdata/transaction/special_transaction/provider_registration.rs b/dash/src/blockdata/transaction/special_transaction/provider_registration.rs index fd4d7d946..9f9e2b9f5 100644 --- a/dash/src/blockdata/transaction/special_transaction/provider_registration.rs +++ b/dash/src/blockdata/transaction/special_transaction/provider_registration.rs @@ -40,15 +40,15 @@ use std::net::SocketAddr; use bincode::{Decode, Encode}; use hashes::Hash; use hex_conservative::Case::Lower; -use internals::impl_array_newtype; use crate::address::Payload; use crate::blockdata::transaction::special_transaction::SpecialTransactionBasePayloadEncodable; use crate::blockdata::transaction::special_transaction::provider_update_service::ProTxVersion; use crate::bls_sig_utils::BLSPublicKey; use crate::consensus::{Decodable, Encodable, encode}; +use crate::crypto::ecdsa::{ECDSA_REC_SIG_LEN, EcdsaRecSignature}; use crate::hash_types::{InputsHash, PubkeyHash, SpecialTransactionPayloadHash}; -use crate::internal_macros::{impl_bytes_newtype, impl_consensus_encoding}; +use crate::internal_macros::impl_consensus_encoding; use crate::platform_node_id::PlatformNodeId; use crate::prelude::*; use crate::sml::masternode_list_entry::MasternodeNetInfo; @@ -127,32 +127,25 @@ impl Decodable for ProviderMasternodeType { } } -/// A compact recoverable ECDSA signature of 65 bytes, written without a length prefix. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -pub struct CompactSignature([u8; 65]); - -impl_array_newtype!(CompactSignature, u8, 65); -impl_bytes_newtype!(CompactSignature, 65); - -impl Encodable for CompactSignature { +// Written without a length prefix, as Core does. +impl Encodable for EcdsaRecSignature { fn consensus_encode(&self, w: &mut W) -> Result { - w.write_all(&self.0)?; - Ok(self.0.len()) + w.write_all(self.as_bytes())?; + Ok(ECDSA_REC_SIG_LEN) } } -impl Decodable for CompactSignature { +impl Decodable for EcdsaRecSignature { fn consensus_decode(r: &mut R) -> Result { - let mut bytes = [0; 65]; + let mut bytes = [0; ECDSA_REC_SIG_LEN]; r.read_exact(&mut bytes).map_err(encode::Error::Io)?; - Ok(CompactSignature(bytes)) + Ok(EcdsaRecSignature::from(bytes)) } } /// Encodes signatures as Core does: a `u8` count followed by the signatures. pub(crate) fn encode_compact_signatures( - signatures: &[CompactSignature], + signatures: &[EcdsaRecSignature], w: &mut W, ) -> Result { let count = u8::try_from(signatures.len()) @@ -167,9 +160,9 @@ pub(crate) fn encode_compact_signatures( /// Decodes signatures written by [`encode_compact_signatures`]. pub(crate) fn decode_compact_signatures( r: &mut R, -) -> Result, encode::Error> { +) -> Result, encode::Error> { let count = u8::consensus_decode(r)?; - (0..count).map(|_| CompactSignature::consensus_decode(r)).collect() + (0..count).map(|_| EcdsaRecSignature::consensus_decode(r)).collect() } /// One participant's contribution to the collateral of a shared masternode. @@ -206,7 +199,7 @@ impl CollateralShare { pub struct SharedCollateral { pub shares: Vec, /// One consent signature per share, in share order. - pub join_sigs: Vec, + pub join_sigs: Vec, pub early_period_blocks: u32, /// Penalty in duffs for leaving within `early_period_blocks`. pub early_penalty: i64, @@ -216,7 +209,7 @@ impl SharedCollateral { /// The size of the terms in bytes. pub fn size(&self) -> usize { 1 + self.shares.iter().map(CollateralShare::size).sum::() - + 65 * self.join_sigs.len() + + ECDSA_REC_SIG_LEN * self.join_sigs.len() + 4 + 8 } @@ -253,7 +246,7 @@ impl Decodable for SharedCollateral { let shares = (0..count).map(|_| CollateralShare::consensus_decode(r)).collect::>()?; let join_sigs = - (0..count).map(|_| CompactSignature::consensus_decode(r)).collect::>()?; + (0..count).map(|_| EcdsaRecSignature::consensus_decode(r)).collect::>()?; Ok(SharedCollateral { shares, join_sigs, @@ -1503,7 +1496,7 @@ mod tests { }]), shared_collateral: Some(SharedCollateral { shares: (0..shares).map(share).collect(), - join_sigs: (0..shares).map(|i| CompactSignature::from([i; 65])).collect(), + join_sigs: (0..shares).map(|i| EcdsaRecSignature::from([i; 65])).collect(), early_period_blocks: 1000, early_penalty: 100_000_000, }), diff --git a/dash/src/blockdata/transaction/special_transaction/provider_update_shared_registrar.rs b/dash/src/blockdata/transaction/special_transaction/provider_update_shared_registrar.rs index 7b57bac49..3168be87f 100644 --- a/dash/src/blockdata/transaction/special_transaction/provider_update_shared_registrar.rs +++ b/dash/src/blockdata/transaction/special_transaction/provider_update_shared_registrar.rs @@ -17,10 +17,11 @@ use hashes::Hash; use crate::blockdata::transaction::special_transaction::SpecialTransactionBasePayloadEncodable; use crate::blockdata::transaction::special_transaction::provider_registration::{ - CompactSignature, decode_compact_signatures, encode_compact_signatures, + decode_compact_signatures, encode_compact_signatures, }; use crate::bls_sig_utils::BLSPublicKey; use crate::consensus::{Decodable, Encodable, encode}; +use crate::crypto::ecdsa::EcdsaRecSignature; use crate::hash_types::{InputsHash, PubkeyHash, SpecialTransactionPayloadHash, Txid}; use crate::io; use crate::prelude::*; @@ -38,7 +39,7 @@ pub struct ProviderUpdateSharedRegistrarPayload { pub voting_key_hash: PubkeyHash, pub inputs_hash: InputsHash, /// One signature per share, in share order. - pub signatures: Vec, + pub signatures: Vec, } impl ProviderUpdateSharedRegistrarPayload { @@ -104,7 +105,7 @@ mod tests { operator_public_key: BLSPublicKey::from([0x22; 48]), voting_key_hash: PubkeyHash::from_byte_array([0x33; 20]), inputs_hash: InputsHash::from_byte_array([0x44; 32]), - signatures: vec![CompactSignature::from([0x55; 65])], + signatures: vec![EcdsaRecSignature::from([0x55; 65])], }; let mut want = vec![0x01, 0x00]; want.extend([0x11; 32]); diff --git a/dash/src/bloom/filter.rs b/dash/src/bloom/filter.rs index c303fc360..9005e4600 100644 --- a/dash/src/bloom/filter.rs +++ b/dash/src/bloom/filter.rs @@ -209,7 +209,8 @@ impl Decodable for BloomFilter { #[cfg(test)] mod tests { use super::*; - use crate::crypto::key::{PrivateKey, secp256k1}; + use crate::crypto::key::PrivateKey; + use crate::ecdsa::EcdsaSecretKey; #[test] fn test_bloom_filter_basic() { @@ -357,7 +358,7 @@ mod tests { .unwrap() .try_into() .unwrap(); - let secret_key = secp256k1::SecretKey::from_secret_bytes(privkey_bytes).unwrap(); + let secret_key = EcdsaSecretKey::from_bytes(&privkey_bytes).unwrap(); let privkey = PrivateKey::new_uncompressed(secret_key, crate::network::constants::Network::Mainnet); let pubkey = privkey.public_key(); diff --git a/dash/src/consensus/encode.rs b/dash/src/consensus/encode.rs index 9b4a718da..f4c9e788e 100644 --- a/dash/src/consensus/encode.rs +++ b/dash/src/consensus/encode.rs @@ -1495,7 +1495,7 @@ mod tests { #[test] #[cfg(feature = "rand-std")] fn serialization_round_trips() { - use secp256k1::rand::{Rng, rng}; + use crate::crypto::key::rand::{Rng, rng}; macro_rules! round_trip { ($($val_type:ty),*) => { diff --git a/dash/src/crypto/key.rs b/dash/src/crypto/key.rs index d3be59ac5..15bb45087 100644 --- a/dash/src/crypto/key.rs +++ b/dash/src/crypto/key.rs @@ -155,12 +155,12 @@ mod tests { #[test] #[cfg(feature = "rand-std")] fn public_key_constructors() { - use secp256k1::rand; + use crate::crypto::ecdsa::EcdsaSecretKey; - let kp = Keypair::new(&mut rand::rng()); + let pk = EcdsaSecretKey::generate().public_key(); - let _ = PublicKey::new(kp); - let _ = PublicKey::new_uncompressed(kp); + let _ = PublicKey::new(pk); + let _ = PublicKey::new_uncompressed(pk); } #[test] diff --git a/dash/src/crypto/mod.rs b/dash/src/crypto/mod.rs index 0a3e7e288..e1624bf2b 100644 --- a/dash/src/crypto/mod.rs +++ b/dash/src/crypto/mod.rs @@ -7,7 +7,11 @@ //! pub mod ecdsa { - pub use dashcore_crypto::ecdsa::{Error, SerializedSignature, Signature}; + pub use dashcore_crypto::ecdsa::{ + DecodeError, ECDSA_PK_LEN, ECDSA_PK_UNCOMPRESSED_LEN, ECDSA_REC_SIG_LEN, ECDSA_SIG_LEN, + ECDSA_SK_LEN, EcdsaError, EcdsaPublicKey, EcdsaRecSignature, EcdsaSecretKey, + EcdsaSignature, ParseSignatureError, SerializedSignature, Signature, + }; } pub mod key; pub mod sighash; diff --git a/dash/src/crypto/sighash.rs b/dash/src/crypto/sighash.rs index 8d42b06f7..9a7cad811 100644 --- a/dash/src/crypto/sighash.rs +++ b/dash/src/crypto/sighash.rs @@ -1267,10 +1267,10 @@ mod tests { }) } - use secp256k1::{self, SecretKey, XOnlyPublicKey}; - use crate::consensus::serde as con_serde; - use crate::taproot::{TapNodeHash, TapTweakHash}; + use crate::crypto::ecdsa::EcdsaSecretKey; + use crate::crypto::key::{Keypair, XOnlyPublicKey}; + use crate::taproot::{TapNodeHash, TapTweak, TapTweakHash}; #[derive(serde::Deserialize)] @@ -1303,7 +1303,7 @@ mod tests { #[serde(rename_all = "camelCase")] struct KpsInputSpendingGiven { txin_index: usize, - internal_privkey: SecretKey, + internal_privkey: EcdsaSecretKey, merkle_root: Option, #[serde(deserialize_with = "sighash_deser_numeric")] hash_type: TapSighashType, @@ -1314,7 +1314,7 @@ mod tests { struct KpsInputSpendingIntermediary { internal_pubkey: XOnlyPublicKey, tweak: TapTweakHash, - tweaked_privkey: SecretKey, + tweaked_privkey: EcdsaSecretKey, sig_msg: String, //precomputed_used: Vec, // unused sig_hash: TapSighash, @@ -1388,22 +1388,18 @@ mod tests { let expected = inp.intermediary; let sig_str = inp.expected.witness.remove(0); - let (expected_key_spend_sig, expected_hash_ty) = if sig_str.len() == 128 { - ( - secp256k1::schnorr::Signature::from_str(&sig_str).unwrap(), - TapSighashType::Default, - ) + let expected_hash_ty = if sig_str.len() == 128 { + TapSighashType::Default } else { let hash_ty = u8::from_str_radix(&sig_str[128..130], 16).unwrap(); - let hash_ty = TapSighashType::from_consensus_u8(hash_ty).unwrap(); - (secp256k1::schnorr::Signature::from_str(&sig_str[..128]).unwrap(), hash_ty) + TapSighashType::from_consensus_u8(hash_ty).unwrap() }; // tests - let keypair = secp256k1::Keypair::from_secret_key(&internal_priv_key); - let (internal_key, _parity) = XOnlyPublicKey::from_keypair(&keypair); + let keypair = Keypair::from(internal_priv_key); + let internal_key = XOnlyPublicKey::from_keypair(&keypair); let tweak = TapTweakHash::from_key_and_tweak(internal_key, merkle_root); - let tweaked_keypair = keypair.add_xonly_tweak(&tweak.to_scalar()).unwrap(); + let tweaked_keypair = keypair.tap_tweak(merkle_root); let mut sig_msg = Vec::new(); cache .taproot_encode_signing_data_to( @@ -1419,19 +1415,19 @@ mod tests { .taproot_signature_hash(tx_ind, &Prevouts::All(&utxos), None, None, hash_ty) .unwrap(); - let msg = secp256k1::Message::from_digest(sighash.to_byte_array()); - let key_spend_sig = - secp256k1::schnorr::sign_with_aux_rand(msg.as_ref(), &tweaked_keypair, &[0u8; 32]); + let key_spend_sig = tweaked_keypair + .as_keypair() + .raw_bip340_sign_with_aux_randomness(&sighash.to_byte_array(), &[0u8; 32]); - assert_eq!(expected.internal_pubkey, internal_key); + assert_eq!(expected.internal_pubkey.serialize().0, internal_key.serialize().0); assert_eq!(expected.tweak, tweak); assert_eq!(expected.sig_msg, sig_msg.to_lower_hex_string()); assert_eq!(expected.sig_hash, sighash); assert_eq!(expected_hash_ty, hash_ty); - assert_eq!(expected_key_spend_sig, key_spend_sig); + assert_eq!(sig_str[..128], key_spend_sig.to_string()); - let tweaked_priv_key = SecretKey::from_keypair(&tweaked_keypair); - assert_eq!(expected.tweaked_privkey, tweaked_priv_key); + let tweaked_priv_key = tweaked_keypair.as_keypair().to_secret_bytes(); + assert_eq!(*expected.tweaked_privkey.to_bytes(), tweaked_priv_key); } } diff --git a/dash/src/internal_macros.rs b/dash/src/internal_macros.rs index 80189eb5e..6937343f2 100644 --- a/dash/src/internal_macros.rs +++ b/dash/src/internal_macros.rs @@ -49,166 +49,6 @@ pub(crate) use impl_consensus_encoding; #[cfg(test)] pub(crate) use test_macros::*; -/// Formats `$len` bytes as hex in integer style. -/// -/// Right aligned by default and `0x` under `{:#}`, with precision ignored. -/// Formatting through `hex_conservative` directly would pad like a string and -/// change how these types print with a width. -macro_rules! fmt_hex_exact { - ($f:expr, $len:expr, $bytes:expr, $case:expr) => {{ - let mut encoder = hex_conservative::buf_encoder::BufEncoder::<{ $len * 2 }>::new($case); - encoder.put_bytes($bytes); - $f.pad_integral(true, "0x", encoder.as_str()) - }}; -} -pub(crate) use fmt_hex_exact; - -/// Implements several traits for byte-based newtypes. -/// Implements: -/// - core::fmt::LowerHex -/// - core::fmt::UpperHex -/// - core::fmt::Display -/// - core::str::FromStr -macro_rules! impl_bytes_newtype { - ($t:ident, $len:literal) => { - impl $t { - /// Returns a reference the underlying bytes. - #[inline] - pub fn as_bytes(&self) -> &[u8; $len] { - &self.0 - } - - /// Returns the underlying bytes. - #[inline] - pub fn to_bytes(self) -> [u8; $len] { - // We rely on `Copy` being implemented for $t so conversion - // methods use the correct Rust naming conventions. - fn check_copy() {} - check_copy::<$t>(); - - self.0 - } - } - - impl core::fmt::LowerHex for $t { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - $crate::internal_macros::fmt_hex_exact!( - f, - $len, - &self.0, - hex_conservative::Case::Lower - ) - } - } - - impl core::fmt::UpperHex for $t { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - $crate::internal_macros::fmt_hex_exact!( - f, - $len, - &self.0, - hex_conservative::Case::Upper - ) - } - } - - impl core::fmt::Display for $t { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::LowerHex::fmt(self, f) - } - } - - impl core::fmt::Debug for $t { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - core::fmt::LowerHex::fmt(self, f) - } - } - - impl core::str::FromStr for $t { - type Err = hex_conservative::DecodeFixedLengthBytesError; - fn from_str(s: &str) -> Result { - Ok($t(hex_conservative::decode_to_array(s)?)) - } - } - - #[cfg(feature = "serde")] - impl $crate::serde::Serialize for $t { - fn serialize(&self, s: S) -> Result { - if s.is_human_readable() { - s.collect_str(self) - } else { - s.serialize_bytes(&self[..]) - } - } - } - - #[cfg(feature = "serde")] - impl<'de> $crate::serde::Deserialize<'de> for $t { - fn deserialize>(d: D) -> Result<$t, D::Error> { - if d.is_human_readable() { - struct HexVisitor; - - impl<'de> $crate::serde::de::Visitor<'de> for HexVisitor { - type Value = $t; - - fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str("an ASCII hex string") - } - - fn visit_bytes(self, v: &[u8]) -> Result - where - E: $crate::serde::de::Error, - { - use $crate::serde::de::Unexpected; - - if let Ok(hex) = core::str::from_utf8(v) { - hex.parse().map_err(E::custom) - } else { - return Err(E::invalid_value(Unexpected::Bytes(v), &self)); - } - } - - fn visit_str(self, v: &str) -> Result - where - E: $crate::serde::de::Error, - { - v.parse().map_err(E::custom) - } - } - - d.deserialize_str(HexVisitor) - } else { - struct BytesVisitor; - - impl<'de> $crate::serde::de::Visitor<'de> for BytesVisitor { - type Value = $t; - - fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.write_str("a bytestring") - } - - fn visit_bytes(self, v: &[u8]) -> Result - where - E: $crate::serde::de::Error, - { - if v.len() != $len { - Err(E::invalid_length(v.len(), &stringify!($len))) - } else { - let mut ret = [0; $len]; - ret.copy_from_slice(v); - Ok($t(ret)) - } - } - } - - d.deserialize_bytes(BytesVisitor) - } - } - } - }; -} -pub(crate) use impl_bytes_newtype; - #[cfg(test)] mod test_macros { diff --git a/dash/src/lib.rs b/dash/src/lib.rs index 8bb87a8d3..22d48962c 100644 --- a/dash/src/lib.rs +++ b/dash/src/lib.rs @@ -30,14 +30,12 @@ //! //! ## Available feature flags //! -//! * `secp-recovery` - enables calculating public key from a signature and message. //! * `signer` - enables singing and validation ECDSA helpers. //! * `base64` - (dependency), enables encoding of message signatures. //! * `unstable` - enables unstable features for testing. //! * `rand` - (dependency), makes it more convenient to generate random values. //! * `bincode` - (dependency), implements bincode serialization and deserialization. //! * `serde` - (dependency), implements `serde`-based serialization and deserialization. -//! * `secp-lowmemory` - optimizations for low-memory devices. //! // Experimental features we need. @@ -67,7 +65,6 @@ pub extern crate bech32; #[cfg(feature = "bitcoinconsensus")] pub extern crate bitcoinconsensus; pub extern crate dashcore_hashes as hashes; -pub extern crate secp256k1; #[cfg(feature = "eddsa")] pub use dashcore_crypto::eddsa; diff --git a/dash/src/merkle_tree/block.rs b/dash/src/merkle_tree/block.rs index 88bab20a6..5dfd9e6ac 100644 --- a/dash/src/merkle_tree/block.rs +++ b/dash/src/merkle_tree/block.rs @@ -533,9 +533,9 @@ impl std::error::Error for MerkleBlockError { #[cfg(test)] mod tests { #[cfg(feature = "rand-std")] - use hashes::Hash; + use crate::crypto::key::rand::prelude::*; #[cfg(feature = "rand-std")] - use secp256k1::rand::prelude::*; + use hashes::Hash; use super::*; use crate::consensus::encode::{deserialize, serialize}; @@ -584,7 +584,7 @@ mod tests { use crate::merkle_tree; - let mut rng = secp256k1::rand::rng(); + let mut rng = crate::crypto::key::rand::rng(); // Create some fake tx ids let tx_ids = (1..=tx_count) .map(|i| format!("{:064x}", i).parse::().unwrap()) diff --git a/dash/src/platform_node_id.rs b/dash/src/platform_node_id.rs index 8265e9179..5a2b3f571 100644 --- a/dash/src/platform_node_id.rs +++ b/dash/src/platform_node_id.rs @@ -24,8 +24,8 @@ use hashes::{Hash, sha256}; use internals::impl_array_newtype; use crate::consensus::{Decodable, Encodable, encode}; -use crate::internal_macros::impl_bytes_newtype; use crate::io; +use internals::impl_bytes_newtype; /// A Dash Platform (Tenderdash/CometBFT) node ID. /// diff --git a/dash/src/sign_message.rs b/dash/src/sign_message.rs index 0db589098..8643ffb82 100644 --- a/dash/src/sign_message.rs +++ b/dash/src/sign_message.rs @@ -17,202 +17,89 @@ //! Signature //! -//! This module provides signature related functions including secp256k1 signature recovery when -//! library is used with the `secp-recovery` feature. +//! This module provides the Dash signed message format over +//! [`EcdsaRecSignature`], including public key recovery. //! +#[cfg(feature = "base64")] +use base64::Engine as _; +#[cfg(feature = "base64")] +use base64::engine::general_purpose::STANDARD as BASE64; use hashes::{Hash, HashEngine, sha256d}; -#[cfg(feature = "secp-recovery")] -pub use self::message_signing::{MessageSignature, MessageSignatureError}; +use crate::address::{Address, AddressType, Payload}; use crate::consensus::{Encodable, encode}; +use crate::crypto::ecdsa::{EcdsaError, EcdsaPublicKey, EcdsaRecSignature}; +use crate::crypto::key::PublicKey; +#[cfg(feature = "base64")] +use crate::prelude::*; /// The prefix for signed messages using Dash's message signing protocol. pub const DASH_SIGNED_MSG_PREFIX: &[u8] = b"\x19DarkCoin Signed Message:\n"; -#[cfg(feature = "secp-recovery")] -mod message_signing { - use core::fmt; - - #[cfg(feature = "base64")] - use base64::Engine as _; - #[cfg(feature = "base64")] - use base64::engine::general_purpose::STANDARD as BASE64; - use hashes::{Hash, sha256d}; - use internals::write_err; - use secp256k1; - use secp256k1::ecdsa::{RecoverableSignature, RecoveryId}; - - use crate::address::{Address, AddressType, Payload}; - use crate::crypto::key::PublicKey; - #[cfg(feature = "base64")] - use crate::prelude::*; - - /// An error used for dealing with Bitcoin Signed Messages. - #[derive(Debug, PartialEq, Eq)] - #[non_exhaustive] - pub enum MessageSignatureError { - /// Signature is expected to be 65 bytes. - InvalidLength, - /// The signature is invalidly constructed. - InvalidEncoding(secp256k1::Error), - /// Invalid base64 encoding. - InvalidBase64, - /// Unsupported Address Type - UnsupportedAddressType(AddressType), - } - - impl fmt::Display for MessageSignatureError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - MessageSignatureError::InvalidLength => write!(f, "length not 65 bytes"), - MessageSignatureError::InvalidEncoding(ref e) => { - write_err!(f, "invalid encoding"; e) - } - MessageSignatureError::InvalidBase64 => write!(f, "invalid base64"), - MessageSignatureError::UnsupportedAddressType(ref address_type) => { - write!(f, "unsupported address type: {}", address_type) - } - } - } - } - - impl std::error::Error for MessageSignatureError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - use self::MessageSignatureError::*; - - match self { - InvalidEncoding(e) => Some(e), - InvalidLength | InvalidBase64 | UnsupportedAddressType(_) => None, - } - } - } - - #[doc(hidden)] - impl From for MessageSignatureError { - fn from(e: secp256k1::Error) -> MessageSignatureError { - MessageSignatureError::InvalidEncoding(e) - } - } - - /// A signature on a Bitcoin Signed Message. - /// - /// In order to use the `to_base64` and `from_base64` methods, as well as the - /// `fmt::Display` and `str::FromStr` implementations, the `base64` feature - /// must be enabled. - #[derive(Copy, Clone, PartialEq, Eq, Debug)] - pub struct MessageSignature { - /// The inner recoverable signature. - pub signature: RecoverableSignature, - /// Whether or not this signature was created with a compressed key. - pub compressed: bool, - } - - impl MessageSignature { - /// Create a new [MessageSignature]. - pub fn new(signature: RecoverableSignature, compressed: bool) -> MessageSignature { - MessageSignature { - signature, - compressed, - } - } - - /// Serialize to bytes. - pub fn serialize(&self) -> [u8; 65] { - let (recid, raw) = self.signature.serialize_compact(); - let mut serialized = [0u8; 65]; - serialized[0] = 27; - serialized[0] += recid.to_u8(); - if self.compressed { - serialized[0] += 4; - } - serialized[1..].copy_from_slice(&raw[..]); - serialized - } - - /// Create from a byte slice. - pub fn from_slice(bytes: &[u8]) -> Result { - if bytes.len() != 65 { - return Err(MessageSignatureError::InvalidLength); - } - // Only headers 27..=34 encode a recovery id and a compression - // flag. - if !(27..=34).contains(&bytes[0]) { - return Err(MessageSignatureError::InvalidEncoding( - secp256k1::Error::InvalidRecoveryId, - )); - }; - let recid = RecoveryId::try_from(((bytes[0] - 27) & 0x03) as i32)?; - Ok(MessageSignature { - signature: RecoverableSignature::from_compact(&bytes[1..], recid)?, - compressed: ((bytes[0] - 27) & 0x04) != 0, - }) - } - - /// Attempt to recover a public key from the signature and the signed message. - /// - /// To get the message hash from a message, use [super::signed_msg_hash]. - pub fn recover_pubkey( - &self, - msg_hash: sha256d::Hash, - ) -> Result { - let msg = secp256k1::Message::from_digest(msg_hash.to_byte_array()); - let pubkey = self.signature.recover(msg)?; - Ok(PublicKey { - inner: pubkey, - compressed: self.compressed, - }) - } +/// An error used for dealing with Bitcoin Signed Messages. +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +#[non_exhaustive] +pub enum MessageSignatureError { + /// Signature is expected to be 65 bytes. + #[error("length not 65 bytes")] + InvalidLength, + /// The signature is invalidly constructed. + #[error("invalid encoding")] + InvalidEncoding(#[from] EcdsaError), + /// Invalid base64 encoding. + #[error("invalid base64")] + InvalidBase64, + /// Unsupported Address Type + #[error("unsupported address type: {0}")] + UnsupportedAddressType(AddressType), +} - /// Verify that the signature signs the message and was signed by the given address. - /// - /// To get the message hash from a message, use [super::signed_msg_hash]. - pub fn is_signed_by_address( - &self, - address: &Address, - msg_hash: sha256d::Hash, - ) -> Result { - match address.address_type() { - Some(AddressType::P2pkh) => { - let pubkey = self.recover_pubkey(msg_hash)?; - Ok(*address.payload() == Payload::p2pkh(&pubkey)) - } - Some(address_type) => { - Err(MessageSignatureError::UnsupportedAddressType(address_type)) - } - None => Ok(false), - } - } +/// Parses a signature from its 65-byte compact encoding. +pub fn from_slice(bytes: &[u8]) -> Result { + EcdsaRecSignature::try_from(bytes).map_err(|_| MessageSignatureError::InvalidLength) +} - /// Convert a signature from base64 encoding. - #[cfg(feature = "base64")] - pub fn from_base64(s: &str) -> Result { - let bytes = BASE64.decode(s).map_err(|_| MessageSignatureError::InvalidBase64)?; - MessageSignature::from_slice(&bytes) - } +/// Parses a signature from the base64 of its compact encoding. +#[cfg(feature = "base64")] +pub fn from_base64(s: &str) -> Result { + from_slice(&BASE64.decode(s).map_err(|_| MessageSignatureError::InvalidBase64)?) +} - /// Convert to base64 encoding. - #[cfg(feature = "base64")] - pub fn to_base64(self) -> String { - BASE64.encode(&self.serialize()[..]) - } - } +/// Encodes a signature as the base64 of its compact encoding. +#[cfg(feature = "base64")] +pub fn to_base64(signature: EcdsaRecSignature) -> String { + BASE64.encode(signature.as_bytes()) +} - #[cfg(feature = "base64")] - impl fmt::Display for MessageSignature { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let bytes = self.serialize(); - // This avoids the allocation of a String. - write!(f, "{}", base64::display::Base64Display::new(&bytes[..], &BASE64)) - } - } +/// Recovers the signing key from a signature over `msg_hash`. +/// +/// To get the message hash from a message, use [signed_msg_hash]. +pub fn recover_pubkey( + signature: &EcdsaRecSignature, + msg_hash: sha256d::Hash, +) -> Result { + Ok(PublicKey { + inner: EcdsaPublicKey::recover(msg_hash.as_byte_array(), signature)?, + compressed: signature.is_compressed()?, + }) +} - #[cfg(feature = "base64")] - impl core::str::FromStr for MessageSignature { - type Err = MessageSignatureError; - fn from_str(s: &str) -> Result { - MessageSignature::from_base64(s) +/// Checks that a signature over `msg_hash` was made by `address`. +/// +/// To get the message hash from a message, use [signed_msg_hash]. +pub fn is_signed_by_address( + signature: &EcdsaRecSignature, + address: &Address, + msg_hash: sha256d::Hash, +) -> Result { + match address.address_type() { + Some(AddressType::P2pkh) => { + let pubkey = recover_pubkey(signature, msg_hash)?; + Ok(*address.payload() == Payload::p2pkh(&pubkey)) } + Some(address_type) => Err(MessageSignatureError::UnsupportedAddressType(address_type)), + None => Ok(false), } } @@ -229,10 +116,6 @@ pub fn signed_msg_hash(msg: &str) -> sha256d::Hash { #[cfg(test)] mod tests { use super::*; - #[cfg(feature = "base64")] - use base64::Engine as _; - #[cfg(feature = "base64")] - use base64::engine::general_purpose::STANDARD as BASE64; #[test] fn test_signed_msg_hash() { @@ -244,59 +127,53 @@ mod tests { } #[test] - #[cfg(all(feature = "secp-recovery", feature = "base64", feature = "rand-std"))] + #[cfg(all(feature = "base64", feature = "rand-std"))] fn test_message_signature() { - use core::str::FromStr; - - use secp256k1; - + use crate::crypto::ecdsa::EcdsaSecretKey; use crate::{Address, AddressType, Network}; let message = "rust-dash MessageSignature test"; let msg_hash = signed_msg_hash(message); - let msg = secp256k1::Message::from_digest(msg_hash.to_byte_array()); - let privkey = secp256k1::SecretKey::new(&mut secp256k1::rand::rng()); - let secp_sig = - secp256k1::ecdsa::RecoverableSignature::sign_ecdsa_recoverable(msg, &privkey); - let signature = MessageSignature { - signature: secp_sig, - compressed: true, - }; + let privkey = EcdsaSecretKey::generate(); + let signature = privkey.sign_recoverable(msg_hash.as_byte_array()); + assert_eq!(signature.is_compressed(), Ok(true)); - assert_eq!(signature.to_base64(), signature.to_string()); - let signature2 = MessageSignature::from_str(&signature.to_string()).unwrap(); - let pubkey = signature2.recover_pubkey(msg_hash).unwrap(); + let signature2 = from_base64(&to_base64(signature)).unwrap(); + assert_eq!(signature2, signature); + let pubkey = recover_pubkey(&signature2, msg_hash).unwrap(); assert!(pubkey.compressed); - assert_eq!(pubkey.inner, secp256k1::PublicKey::from_secret_key(&privkey)); + assert_eq!(pubkey.inner, privkey.public_key()); let p2pkh = Address::p2pkh(&pubkey, Network::Mainnet); - assert_eq!(signature2.is_signed_by_address(&p2pkh, msg_hash), Ok(true)); + assert_eq!(is_signed_by_address(&signature2, &p2pkh, msg_hash), Ok(true)); let p2wpkh = Address::p2wpkh(&pubkey, Network::Mainnet).unwrap(); assert_eq!( - signature2.is_signed_by_address(&p2wpkh, msg_hash), + is_signed_by_address(&signature2, &p2wpkh, msg_hash), Err(MessageSignatureError::UnsupportedAddressType(AddressType::P2wpkh)) ); let p2shwpkh = Address::p2shwpkh(&pubkey, Network::Mainnet).unwrap(); assert_eq!( - signature2.is_signed_by_address(&p2shwpkh, msg_hash), + is_signed_by_address(&signature2, &p2shwpkh, msg_hash), Err(MessageSignatureError::UnsupportedAddressType(AddressType::P2sh)) ); } #[test_case::test_matrix([26, 35])] - #[cfg(feature = "secp-recovery")] fn test_message_signature_header_out_of_range(header: u8) { + use crate::crypto::ecdsa::EcdsaError; + let mut bytes = [0u8; 65]; bytes[0] = header; + let signature = from_slice(&bytes).expect("65 bytes"); assert_eq!( - MessageSignature::from_slice(&bytes), - Err(MessageSignatureError::InvalidEncoding(secp256k1::Error::InvalidRecoveryId)) + recover_pubkey(&signature, signed_msg_hash("test")), + Err(MessageSignatureError::InvalidEncoding(EcdsaError::InvalidRecoveryId)) ); } #[test] - #[cfg(all(feature = "secp-recovery", feature = "base64"))] + #[cfg(feature = "base64")] fn test_incorrect_message_signature() { use crate::crypto::key::PublicKey; use crate::{Address, Network}; @@ -308,13 +185,12 @@ mod tests { // Signed with pk "UuOGDsfLPr4HIMKQX0ipjJeRaj1geCq3yPUF2COP5ME=" let signature_base64 = "IAM2qX24tYx/bdBTIgVLhD8QEAjrPlJpmjB4nZHdRYGIBa4DmVulAcwjPnWe6Q5iEwXH6F0pUCJP/ZeHPWS1h1o="; let pubkey_base64 = "A1FTfMEntPpAty3qkEo0q2Dc1FEycI10a3jmwEFy+Qr6"; - let signature = - super::MessageSignature::from_base64(signature_base64).expect("message signature"); + let signature = from_base64(signature_base64).expect("message signature"); let pubkey = PublicKey::from_slice(&BASE64.decode(pubkey_base64).expect("base64 string")) .expect("pubkey slice"); let p2pkh = Address::p2pkh(&pubkey, Network::Mainnet); - assert_eq!(signature.is_signed_by_address(&p2pkh, msg_hash), Ok(false)); + assert_eq!(is_signed_by_address(&signature, &p2pkh, msg_hash), Ok(false)); } } diff --git a/dash/src/signer.rs b/dash/src/signer.rs index e25683d5f..59323c014 100644 --- a/dash/src/signer.rs +++ b/dash/src/signer.rs @@ -2,14 +2,13 @@ use core::convert::TryInto; -use anyhow::{anyhow, bail}; +use anyhow::{anyhow, ensure}; use hashes::{Hash, hash160, sha256d}; use crate::PublicKey as ECDSAPublicKey; +use crate::crypto::ecdsa::{ECDSA_REC_SIG_LEN, EcdsaPublicKey, EcdsaSecretKey}; use crate::prelude::Vec; -use crate::secp256k1::ecdsa::RecoverableSignature; -use crate::secp256k1::{Message, SecretKey}; -use crate::sign_message::MessageSignature; +use crate::sign_message; /// verifies the ECDSA signature /// The provided signature must be recoverable. Which means: it must contain the recovery byte as a prefix @@ -18,15 +17,13 @@ pub fn verify_data_signature( signature: &[u8], public_key: &[u8], ) -> Result<(), anyhow::Error> { - let data_hash = double_sha(data); - - let msg = - Message::from_digest(data_hash.try_into().map_err(|_| anyhow!("Invalid hash length"))?); - let sig = MessageSignature::from_slice(signature)?.signature; + let data_hash: [u8; 32] = + double_sha(data).try_into().map_err(|_| anyhow!("Invalid hash length"))?; + let sig = sign_message::from_slice(signature)?; let pub_key = ECDSAPublicKey::from_slice(public_key).map_err(anyhow::Error::msg)?; - sig.to_standard().verify(msg, &pub_key.inner).map_err(anyhow::Error::msg) + pub_key.inner.verify(&data_hash, sig.signature()?).map_err(anyhow::Error::msg) } /// verifies the the hash signature. From provided signature and hash recovers the public key @@ -37,42 +34,37 @@ pub fn verify_hash_signature( data_signature: &[u8], public_key_id: &[u8], ) -> Result<(), anyhow::Error> { - let signature = MessageSignature::from_slice(data_signature)?.signature; + let signature = sign_message::from_slice(data_signature)?; - let msg = - Message::from_digest(data_hash.try_into().map_err(|_| anyhow!("Invalid hash length"))?); - let recovered_public_key = signature.recover(msg).map_err(anyhow::Error::msg)?; + let data_hash: &[u8; 32] = data_hash.try_into().map_err(|_| anyhow!("Invalid hash length"))?; + let recovered_public_key = + EcdsaPublicKey::recover(data_hash, &signature).map_err(anyhow::Error::msg)?; let hash_recovered_key = ECDSAPublicKey::new(recovered_public_key).pubkey_hash(); - let are_equal = public_key_id == hash_recovered_key.as_byte_array(); - - if are_equal { - Ok(()) - } else { - bail!("the signature isn't valid") - } + ensure!(public_key_id == hash_recovered_key.as_byte_array(), "the signature isn't valid"); + Ok(()) } /// sign and get the ECDSA signature -pub fn sign(data: &[u8], private_key: &[u8]) -> Result<[u8; 65], anyhow::Error> { +pub fn sign(data: &[u8], private_key: &[u8]) -> Result<[u8; ECDSA_REC_SIG_LEN], anyhow::Error> { let data_hash = double_sha(data); sign_hash(&data_hash, private_key) } /// signs the hash of data and get the ECDSA signature -pub fn sign_hash(data_hash: &[u8], private_key: &[u8]) -> Result<[u8; 65], anyhow::Error> { +pub fn sign_hash( + data_hash: &[u8], + private_key: &[u8], +) -> Result<[u8; ECDSA_REC_SIG_LEN], anyhow::Error> { let private_key: [u8; 32] = private_key .try_into() .map_err(|_| anyhow!("Invalid ECDSA private key: must be 32 bytes"))?; - let pk = SecretKey::from_secret_bytes(private_key) + let pk = EcdsaSecretKey::from_bytes(&private_key) .map_err(|e| anyhow!("Invalid ECDSA private key: {}", e))?; - let msg = - Message::from_digest(data_hash.try_into().map_err(|_| anyhow!("Invalid hash length"))?); + let data_hash: &[u8; 32] = data_hash.try_into().map_err(|_| anyhow!("Invalid hash length"))?; - let signature = RecoverableSignature::sign_ecdsa_recoverable(msg, &pk); - // TODO the compression flag should be obtained from the private key type - Ok(MessageSignature::new(signature, true).serialize()) + Ok(pk.sign_recoverable(data_hash).to_bytes()) } /// calculates double sha256 on data @@ -89,6 +81,7 @@ pub fn ripemd160_sha256(data: &[u8]) -> Vec { mod test { use super::*; use crate::PublicKey; + use crate::crypto::ecdsa::EcdsaSecretKey; use crate::internal_macros::hex; struct Keys { @@ -135,21 +128,20 @@ mod test { let signature = sign(&data, &k.private_key).expect("singing shouldn't fail"); let verification_result = verify_data_signature(&incorrect_data, &signature, &k.public_key_compressed); - assert_error_contains!(verification_result, "signature failed verification"); + assert_error_contains!(verification_result, "signature verification failed"); } #[test] fn signature_not_verified_with_different_public_key() { let k = get_keys(); - let mut rng = crate::secp256k1::rand::rng(); - let (_, different_public_key) = secp256k1::generate_keypair(&mut rng); + let different_public_key = EcdsaSecretKey::generate().public_key(); let data = hex!("fafafa"); let signature = sign(&data, &k.private_key).expect("signing shouldn't fail"); let verification_result = - verify_data_signature(&data, &signature, &different_public_key.serialize()); + verify_data_signature(&data, &signature, &different_public_key.to_compressed()); - assert_error_contains!(verification_result, "signature failed verification") + assert_error_contains!(verification_result, "signature verification failed") } #[test] @@ -195,8 +187,7 @@ mod test { #[test] fn should_fail_validation_with_incorrect_public_key() { let k = get_keys(); - let mut rng = crate::secp256k1::rand::rng(); - let (_, different_public_key) = secp256k1::generate_keypair(&mut rng); + let different_public_key = EcdsaSecretKey::generate().public_key(); let data = hex!("fafafa"); let signature = sign(&data, &k.private_key).expect("signing shouldn't fail"); @@ -205,7 +196,7 @@ mod test { let validation_result = verify_hash_signature( &data_hash, &signature, - &ripemd160_sha256(&different_public_key.serialize()), + &ripemd160_sha256(&different_public_key.to_compressed()), ); assert_error_contains!(validation_result, "the signature isn't valid") @@ -217,11 +208,10 @@ mod test { let data = hex!("fafafa"); let data_hash = double_sha(&data); let secret_key = - SecretKey::from_secret_bytes(k.private_key.as_slice().try_into().unwrap()).unwrap(); + EcdsaSecretKey::from_bytes(k.private_key.as_slice().try_into().unwrap()).unwrap(); - let unrecoverable_signature = - secret_key.sign_ecdsa(Message::from_digest(data_hash.try_into().unwrap())); - let unrecoverable_signature_bytes = unrecoverable_signature.serialize_compact(); + let unrecoverable_signature = secret_key.sign(data_hash.as_slice().try_into().unwrap()); + let unrecoverable_signature_bytes = unrecoverable_signature.to_bytes(); let validation_result = verify_data_signature(&data, &unrecoverable_signature_bytes, &k.public_key_compressed); diff --git a/dash/src/taproot.rs b/dash/src/taproot.rs index 28e35bdfc..b9a5730f1 100644 --- a/dash/src/taproot.rs +++ b/dash/src/taproot.rs @@ -11,12 +11,12 @@ use core::fmt; use core::iter::FusedIterator; use hashes::{Hash, HashEngine, sha256t_hash_newtype}; -use internals::write_err; -use secp256k1::{self, Parity, Scalar}; use crate::consensus::Encodable; +use crate::crypto::ecdsa::EcdsaSecretKey; use crate::crypto::key::{ - TweakedKeyPair, TweakedPublicKey, UntweakedKeyPair, UntweakedPublicKey, XOnlyPublicKey, + Keypair, Parity, Scalar, TweakedKeypair, TweakedPublicKey, UntweakedKeypair, + UntweakedPublicKey, XOnlyPublicKey, constants, }; // Re-export these so downstream only has to use one `taproot` module. pub use crate::crypto::taproot::{Error, Signature}; @@ -59,7 +59,7 @@ impl TapTweakHash { ) -> TapTweakHash { let mut eng = TapTweakHash::engine(); // always hash the key - eng.input(&internal_key.to_byte_array()); + eng.input(&internal_key.serialize().0); if let Some(h) = merkle_root { eng.input(h.as_ref()); } else { @@ -189,7 +189,7 @@ pub struct TaprootSpendInfo { /// The merkle root of the script tree (None if there are no scripts). merkle_root: Option, /// The sign final output pubkey as per BIP 341. - output_key_parity: secp256k1::Parity, + output_key_parity: Parity, /// The tweaked output key. output_key: TweakedPublicKey, /// Map from (script, leaf_version) to (sets of) [`TaprootMerkleBranch`]. More than one control @@ -262,7 +262,7 @@ impl TaprootSpendInfo { } /// Returns the parity of the output key. See also [`TaprootSpendInfo::output_key`]. - pub fn output_key_parity(&self) -> secp256k1::Parity { + pub fn output_key_parity(&self) -> Parity { self.output_key_parity } @@ -1294,7 +1294,7 @@ pub struct ControlBlock { /// The tapleaf version. pub leaf_version: LeafVersion, /// The parity of the output key (NOT THE INTERNAL KEY WHICH IS ALWAYS XONLY). - pub output_key_parity: secp256k1::Parity, + pub output_key_parity: Parity, /// The internal key. pub internal_key: UntweakedPublicKey, /// The merkle proof of a script associated with this leaf. @@ -1317,7 +1317,6 @@ impl ControlBlock { /// # Errors /// /// - [`TaprootError::InvalidControlBlockSize`] if `sl` is not of size 1 + 32 + 32N for any N >= 0. - /// - [`TaprootError::InvalidParity`] if first byte of `sl` is not a valid output key parity. /// - [`TaprootError::InvalidTaprootLeafVersion`] if first byte of `sl` is not a valid leaf version. /// - [`TaprootError::InvalidInternalKey`] if internal key is invalid (first 32 bytes after the parity byte). /// - [`TaprootError::InvalidMerkleTreeDepth`] if merkle tree is too deep (more than 128 levels). @@ -1327,17 +1326,20 @@ impl ControlBlock { { return Err(TaprootError::InvalidControlBlockSize(sl.len())); } - let output_key_parity = - secp256k1::Parity::from_i32((sl[0] & 1) as i32).map_err(TaprootError::InvalidParity)?; + let output_key_parity = if sl[0] & 1 == 1 { + Parity::Odd + } else { + Parity::Even + }; let leaf_version = LeafVersion::from_consensus(sl[0] & TAPROOT_LEAF_MASK)?; // Chunking the key off the parity byte yields the array let Some((internal_key, merkle_branch)) = - sl[1..].split_first_chunk::<{ secp256k1::constants::SCHNORR_PUBLIC_KEY_SIZE }>() + sl[1..].split_first_chunk::<{ constants::SCHNORR_PUBLIC_KEY_SIZE }>() else { return Err(TaprootError::InvalidControlBlockSize(sl.len())); }; - let internal_key = UntweakedPublicKey::from_byte_array(*internal_key) - .map_err(TaprootError::InvalidInternalKey)?; + let internal_key = UntweakedPublicKey::from_byte_array(internal_key) + .map_err(|_| TaprootError::InvalidInternalKey)?; let merkle_branch = TaprootMerkleBranch::decode(merkle_branch)?; Ok(ControlBlock { leaf_version, @@ -1362,7 +1364,7 @@ impl ControlBlock { let first_byte: u8 = i32::from(self.output_key_parity) as u8 | self.leaf_version.to_consensus(); writer.write_all(&[first_byte])?; - writer.write_all(&self.internal_key.to_byte_array())?; + writer.write_all(&self.internal_key.serialize().0)?; self.merkle_branch.encode(&mut writer)?; Ok(self.size()) } @@ -1394,7 +1396,7 @@ impl ControlBlock { // compute the taptweak let tweak = TapTweakHash::from_key_and_tweak(self.internal_key, Some(curr_hash)).to_scalar(); - self.internal_key.tweak_add_check(&output_key, self.output_key_parity, tweak) + self.internal_key.tweak_add_check(&output_key.with_parity(self.output_key_parity), tweak) } } @@ -1566,7 +1568,7 @@ pub enum TaprootBuilderError { /// Two nodes at depth 0 are not allowed. OverCompleteTree, /// Invalid taproot internal key. - InvalidInternalKey(secp256k1::Error), + InvalidInternalKey, /// Called finalize on a empty tree. EmptyTree, } @@ -1589,9 +1591,7 @@ impl fmt::Display for TaprootBuilderError { "Attempted to create a tree with two nodes at depth 0. There must\ only be a exactly one node at depth 0", ), - TaprootBuilderError::InvalidInternalKey(ref e) => { - write_err!(f, "invalid internal x-only key"; e) - } + TaprootBuilderError::InvalidInternalKey => write!(f, "invalid internal x-only key"), TaprootBuilderError::EmptyTree => { write!(f, "Called finalize on an empty tree") } @@ -1604,8 +1604,11 @@ impl std::error::Error for TaprootBuilderError { use self::TaprootBuilderError::*; match self { - InvalidInternalKey(e) => Some(e), - InvalidMerkleTreeDepth(_) | NodeNotInDfsOrder | OverCompleteTree | EmptyTree => None, + InvalidInternalKey + | InvalidMerkleTreeDepth(_) + | NodeNotInDfsOrder + | OverCompleteTree + | EmptyTree => None, } } } @@ -1623,9 +1626,7 @@ pub enum TaprootError { /// Invalid control block size. InvalidControlBlockSize(usize), /// Invalid taproot internal key. - InvalidInternalKey(secp256k1::Error), - /// Invalid parity for internal key. - InvalidParity(secp256k1::InvalidParityValue), + InvalidInternalKey, /// Empty tap tree. EmptyTree, } @@ -1651,10 +1652,7 @@ impl fmt::Display for TaprootError { "Control Block size({}) must be of the form 33 + 32*m where 0 <= m <= {} ", sz, TAPROOT_CONTROL_MAX_NODE_COUNT ), - TaprootError::InvalidInternalKey(ref e) => { - write_err!(f, "invalid internal x-only key"; e) - } - TaprootError::InvalidParity(_) => write!(f, "invalid parity value for internal key"), + TaprootError::InvalidInternalKey => write!(f, "invalid internal x-only key"), TaprootError::EmptyTree => write!(f, "Taproot Tree must contain at least one script"), } } @@ -1665,12 +1663,11 @@ impl std::error::Error for TaprootError { use self::TaprootError::*; match self { - InvalidInternalKey(e) => Some(e), - InvalidMerkleBranchSize(_) + InvalidInternalKey + | InvalidMerkleBranchSize(_) | InvalidMerkleTreeDepth(_) | InvalidTaprootLeafVersion(_) | InvalidControlBlockSize(_) - | InvalidParity(_) | EmptyTree => None, } } @@ -1722,10 +1719,10 @@ impl TapTweak for UntweakedPublicKey { /// The tweaked key and its parity. fn tap_tweak(self, merkle_root: Option) -> (TweakedPublicKey, Parity) { let tweak = TapTweakHash::from_key_and_tweak(self, merkle_root).to_scalar(); - let (output_key, parity) = self.add_tweak(&tweak).expect("Tap tweak failed"); + let output_key = self.add_tweak(&tweak).expect("Tap tweak failed"); - debug_assert!(self.tweak_add_check(&output_key, parity, tweak)); - (TweakedPublicKey::dangerous_assume_tweaked(output_key), parity) + debug_assert!(self.tweak_add_check(&output_key, tweak)); + (TweakedPublicKey::dangerous_assume_tweaked(output_key), output_key.parity()) } fn dangerous_assume_tweaked(self) -> TweakedPublicKey { @@ -1733,9 +1730,9 @@ impl TapTweak for UntweakedPublicKey { } } -impl TapTweak for UntweakedKeyPair { - type TweakedAux = TweakedKeyPair; - type TweakedKey = TweakedKeyPair; +impl TapTweak for UntweakedKeypair { + type TweakedAux = TweakedKeypair; + type TweakedKey = TweakedKeypair; /// Tweaks private and public keys within an untweaked `KeyPair` with corresponding public key /// value and optional script tree merkle root. @@ -1749,15 +1746,21 @@ impl TapTweak for UntweakedKeyPair { /// /// # Returns /// The tweaked key and its parity. - fn tap_tweak(self, merkle_root: Option) -> TweakedKeyPair { - let (pubkey, _parity) = XOnlyPublicKey::from_keypair(&self); - let tweak = TapTweakHash::from_key_and_tweak(pubkey, merkle_root).to_scalar(); - let tweaked = self.add_xonly_tweak(&tweak).expect("Tap tweak failed"); - TweakedKeyPair::dangerous_assume_tweaked(tweaked) + fn tap_tweak(self, merkle_root: Option) -> TweakedKeypair { + let pubkey = XOnlyPublicKey::from_keypair(&self); + let tweak = TapTweakHash::from_key_and_tweak(pubkey, merkle_root); + let mut sk = self.to_private_key(); + if pubkey.parity() == Parity::Odd { + sk = sk.negate(); + } + let tweaked = EcdsaSecretKey::from_bytes(&sk.to_secret_bytes()) + .and_then(|sk| sk.add_tweak(&tweak.to_byte_array())) + .expect("Tap tweak failed"); + TweakedKeypair::dangerous_assume_tweaked(Keypair::from(tweaked)) } - fn dangerous_assume_tweaked(self) -> TweakedKeyPair { - TweakedKeyPair::dangerous_assume_tweaked(self) + fn dangerous_assume_tweaked(self) -> TweakedKeypair { + TweakedKeypair::dangerous_assume_tweaked(self) } } @@ -1767,7 +1770,6 @@ mod test { use hashes::sha256t::Tag; use hashes::{Hash, HashEngine, sha256}; - use secp256k1::XOnlyPublicKey; use super::*; use crate::sighash::{TapSighash, TapSighashTag}; @@ -1865,7 +1867,7 @@ mod test { ControlBlock::decode(&hex_conservative::decode_to_vec(control_block_hex).unwrap()) .unwrap(); assert_eq!(control_block_hex, control_block.serialize().to_lower_hex_string()); - assert!(control_block.verify_taproot_commitment(out_pk.to_inner(), &script)); + assert!(control_block.verify_taproot_commitment(out_pk.to_x_only_public_key(), &script)); } #[test] @@ -2052,7 +2054,10 @@ mod test { for (_weights, script) in script_weights { let ver_script = (script, LeafVersion::TapScript); let ctrl_block = tree_info.control_block(&ver_script).unwrap(); - assert!(ctrl_block.verify_taproot_commitment(output_key.to_inner(), &ver_script.0,)) + assert!( + ctrl_block + .verify_taproot_commitment(output_key.to_x_only_public_key(), &ver_script.0,) + ) } } @@ -2122,7 +2127,10 @@ mod test { for script in [a, b, c, d, e] { let ver_script = (script, LeafVersion::TapScript); let ctrl_block = tree_info.control_block(&ver_script).unwrap(); - assert!(ctrl_block.verify_taproot_commitment(output_key.to_inner(), &ver_script.0,)) + assert!( + ctrl_block + .verify_taproot_commitment(output_key.to_x_only_public_key(), &ver_script.0,) + ) } } @@ -2276,7 +2284,7 @@ mod test { let addr = Address::p2tr(internal_key, merkle_root, Network::Mainnet); let spk = addr.script_pubkey(); - assert_eq!(expected_output_key, output_key.to_inner()); + assert_eq!(expected_output_key.serialize().0, output_key.serialize()); assert_eq!(expected_tweak, tweak); assert_eq!(expected_addr, addr); assert_eq!(expected_spk, spk); diff --git a/dash/src/test_utils/address.rs b/dash/src/test_utils/address.rs index 6ba340fb0..c02a56a45 100644 --- a/dash/src/test_utils/address.rs +++ b/dash/src/test_utils/address.rs @@ -1,6 +1,7 @@ use crate::Network; use hashes::{Hash, sha256}; +use crate::ecdsa::EcdsaSecretKey; use crate::{Address, PrivateKey, PublicKey}; impl crate::Address { @@ -9,7 +10,7 @@ impl crate::Address { data.extend_from_slice(&id.to_le_bytes()); let secret_bytes = sha256::Hash::hash(&data).to_byte_array(); - let secret_key = secp256k1::SecretKey::from_secret_bytes(secret_bytes) + let secret_key = EcdsaSecretKey::from_bytes(&secret_bytes) .unwrap_or_else(|e| panic!("Dummy address generation failed for id {id}: {e}")); let private_key = PrivateKey::new(secret_key, network); diff --git a/internals/src/macros.rs b/internals/src/macros.rs index 826c8340d..4420841b0 100644 --- a/internals/src/macros.rs +++ b/internals/src/macros.rs @@ -4,7 +4,7 @@ /// Implements standard array methods for a given wrapper type. #[macro_export] macro_rules! impl_array_newtype { - ($thing:ident, $ty:ty, $len:literal) => { + ($thing:ident, $ty:ty, $len:expr) => { impl<'a> core::convert::From<[$ty; $len]> for $thing { fn from(data: [$ty; $len]) -> Self { $thing(data) @@ -70,3 +70,94 @@ macro_rules! debug_from_display { } }; } + +/// Formats `$len` bytes as hex in integer style. +/// +/// Right aligned by default and `0x` under `{:#}`, with precision ignored. +/// Formatting through `hex_conservative` directly would pad like a string and +/// change how these types print with a width. +#[macro_export] +macro_rules! fmt_hex_exact { + ($f:expr, $len:expr, $bytes:expr, $case:expr) => {{ + let mut encoder = ::hex_conservative::buf_encoder::BufEncoder::<{ $len * 2 }>::new($case); + encoder.put_bytes($bytes); + $f.pad_integral(true, "0x", encoder.as_str()) + }}; +} + +/// Implements several traits for byte-based newtypes. +/// Implements: +/// - core::fmt::LowerHex +/// - core::fmt::UpperHex +/// - core::fmt::Display +/// - core::str::FromStr +/// +/// The serde impls need `dash_types` and `TryFrom<&[u8]>` on the type. +#[macro_export] +macro_rules! impl_bytes_newtype { + ($t:ident, $len:expr) => { + impl $t { + /// Returns a reference the underlying bytes. + #[inline] + pub fn as_bytes(&self) -> &[u8; $len] { + &self.0 + } + + /// Returns the underlying bytes. + #[inline] + pub fn to_bytes(self) -> [u8; $len] { + // We rely on `Copy` being implemented for $t so conversion + // methods use the correct Rust naming conventions. + fn check_copy() {} + check_copy::<$t>(); + + self.0 + } + } + + impl core::fmt::LowerHex for $t { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + $crate::fmt_hex_exact!(f, $len, &self.0, ::hex_conservative::Case::Lower) + } + } + + impl core::fmt::UpperHex for $t { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + $crate::fmt_hex_exact!(f, $len, &self.0, ::hex_conservative::Case::Upper) + } + } + + impl core::fmt::Display for $t { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + core::fmt::LowerHex::fmt(self, f) + } + } + + impl core::fmt::Debug for $t { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + core::fmt::LowerHex::fmt(self, f) + } + } + + impl core::str::FromStr for $t { + type Err = ::hex_conservative::DecodeFixedLengthBytesError; + fn from_str(s: &str) -> Result { + Ok($t(::hex_conservative::decode_to_array(s)?)) + } + } + + #[cfg(feature = "serde")] + impl ::serde::Serialize for $t { + fn serialize(&self, s: S) -> Result { + ::dash_types::serialize::hex::serialize_as(&self.0, self, s) + } + } + + #[cfg(feature = "serde")] + impl<'de> ::serde::Deserialize<'de> for $t { + fn deserialize>(d: D) -> Result<$t, D::Error> { + ::dash_types::serialize::hex::deserialize_as(d, str::parse::<$t>) + } + } + }; +} diff --git a/key-wallet-ffi/Cargo.toml b/key-wallet-ffi/Cargo.toml index de25f0e86..316822c84 100644 --- a/key-wallet-ffi/Cargo.toml +++ b/key-wallet-ffi/Cargo.toml @@ -35,7 +35,6 @@ key-wallet = { path = "../key-wallet" } key-wallet-manager = { path = "../key-wallet-manager" } dashcore = { path = "../dash" } dash-network = { path = "../dash-network", features = ["ffi"] } -secp256k1 = { workspace = true } tokio = { version = "1.32", features = ["rt-multi-thread", "sync"] } libc = "0.2" hex-conservative = { workspace = true } diff --git a/key-wallet-ffi/src/account_derivation.rs b/key-wallet-ffi/src/account_derivation.rs index 05816b163..e1ccf6e9b 100644 --- a/key-wallet-ffi/src/account_derivation.rs +++ b/key-wallet-ffi/src/account_derivation.rs @@ -231,7 +231,7 @@ pub unsafe extern "C" fn account_derive_private_key_at( account.inner().derive_from_master_xpriv_extended_xpriv_at(master_xpriv.inner(), index), error ); - Box::into_raw(Box::new(FFIPrivateKey::from_secret(derived.private_key))) + Box::into_raw(Box::new(FFIPrivateKey::from_secret(derived.private_key.clone()))) } /// Derive a private key from an account at a given chain/index and return as WIF string. @@ -262,7 +262,8 @@ pub unsafe extern "C" fn account_derive_private_key_as_wif_at( account.inner().derive_from_master_xpriv_extended_xpriv_at(master_xpriv.inner(), index), error ); - let dash_priv = dashcore::PrivateKey::new(derived.private_key, account.inner().network()); + let dash_priv = + dashcore::PrivateKey::new(derived.private_key.clone(), account.inner().network()); unwrap_or_return!(CString::new(dash_priv.to_wif()), error).into_raw() } @@ -313,7 +314,7 @@ pub unsafe extern "C" fn account_derive_private_key_from_seed( account.inner().derive_from_seed_extended_xpriv_at(seed_slice, index), error ); - Box::into_raw(Box::new(FFIPrivateKey::from_secret(derived.private_key))) + Box::into_raw(Box::new(FFIPrivateKey::from_secret(derived.private_key.clone()))) } /// Derive an extended private key from a mnemonic + optional passphrase at the given index. @@ -383,7 +384,7 @@ pub unsafe extern "C" fn account_derive_private_key_from_mnemonic( .derive_from_mnemonic_extended_xpriv_at(mnemonic_str, passphrase_str, index,), error ); - Box::into_raw(Box::new(FFIPrivateKey::from_secret(derived.private_key))) + Box::into_raw(Box::new(FFIPrivateKey::from_secret(derived.private_key.clone()))) } #[cfg(test)] diff --git a/key-wallet-ffi/src/derivation.rs b/key-wallet-ffi/src/derivation.rs index 40a99bba4..42dcd5d39 100644 --- a/key-wallet-ffi/src/derivation.rs +++ b/key-wallet-ffi/src/derivation.rs @@ -5,6 +5,7 @@ use crate::keys::FFIExtendedPrivKey; use crate::keys::FFIExtendedPubKey; use crate::{check_ptr, deref_ptr, unwrap_or_return}; use dash_network::ffi::FFINetwork; +use dashcore::ecdsa::EcdsaSecretKey; use dashcore::Network; use key_wallet::{ExtendedPrivKey, ExtendedPubKey}; use std::ffi::{CStr, CString}; @@ -480,13 +481,13 @@ pub unsafe extern "C" fn key_wallet_derive_address_from_key( }; // Create a secp256k1 private key - let secret_key = match secp256k1::SecretKey::from_secret_bytes(key_bytes) { + let secret_key = match EcdsaSecretKey::from_bytes(&key_bytes) { Ok(sk) => sk, Err(_) => return ptr::null_mut(), }; // Get public key - let public_key = secp256k1::PublicKey::from_secret_key(&secret_key); + let public_key = secret_key.public_key(); // Convert to dashcore PublicKey let dash_pubkey = dashcore::PublicKey::new(public_key); @@ -551,7 +552,7 @@ pub unsafe extern "C" fn key_wallet_derive_address_from_seed( // Get public key let extended_pubkey = ExtendedPubKey::from_priv(&derived_key); - // Convert secp256k1::PublicKey to dashcore::PublicKey + // Wrap as a dashcore::PublicKey let dash_pubkey = dashcore::PublicKey::new(extended_pubkey.public_key); // Convert to address @@ -610,7 +611,7 @@ pub unsafe extern "C" fn key_wallet_derive_private_key_from_seed( }; // Copy private key bytes - let key_bytes = derived_key.private_key.to_secret_bytes(); + let key_bytes = derived_key.private_key.to_bytes(); ptr::copy_nonoverlapping(key_bytes.as_ptr(), key_out, 32); 0 diff --git a/key-wallet-ffi/src/error.rs b/key-wallet-ffi/src/error.rs index 24721f98d..7a94e947d 100644 --- a/key-wallet-ffi/src/error.rs +++ b/key-wallet-ffi/src/error.rs @@ -263,7 +263,6 @@ impl From for FFIError { Error::Bip32(_) | Error::Slip10(_) | Error::BLS(_) - | Error::Secp256k1(_) | Error::Base58 | Error::KeyError(_) => FFIErrorCode::WalletError, }; diff --git a/key-wallet-ffi/src/keys.rs b/key-wallet-ffi/src/keys.rs index 07ae3e65a..91ce9a7ba 100644 --- a/key-wallet-ffi/src/keys.rs +++ b/key-wallet-ffi/src/keys.rs @@ -4,6 +4,7 @@ use crate::error::{FFIError, FFIErrorCode}; use crate::types::FFIWallet; use crate::{check_ptr, deref_ptr, unwrap_or_return}; use dash_network::ffi::FFINetwork; +use dashcore::ecdsa::{EcdsaPublicKey, EcdsaSecretKey}; use hex_conservative::DisplayHex; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_uint}; @@ -11,7 +12,7 @@ use std::ptr; /// Opaque type for a private key (SecretKey) pub struct FFIPrivateKey { - inner: secp256k1::SecretKey, + inner: EcdsaSecretKey, } /// Opaque type for an extended private key @@ -21,7 +22,7 @@ pub struct FFIExtendedPrivKey { /// Opaque type for a public key pub struct FFIPublicKey { - inner: secp256k1::PublicKey, + inner: EcdsaPublicKey, } /// Opaque type for an extended public key @@ -59,7 +60,7 @@ impl FFIExtendedPubKey { impl FFIPrivateKey { #[inline] - pub(crate) fn from_secret(inner: secp256k1::SecretKey) -> Self { + pub(crate) fn from_secret(inner: EcdsaSecretKey) -> Self { FFIPrivateKey { inner, } @@ -255,7 +256,7 @@ pub unsafe extern "C" fn extended_private_key_get_private_key( ) -> *mut FFIPrivateKey { let extended = deref_ptr!(extended_key, error); Box::into_raw(Box::new(FFIPrivateKey { - inner: extended.inner.private_key, + inner: extended.inner.private_key.clone(), })) } @@ -277,7 +278,7 @@ pub unsafe extern "C" fn private_key_to_wif( let network_rust: key_wallet::Network = network.into(); // Convert to WIF format - let dash_key = dashcore::PrivateKey::new(key.inner, network_rust); + let dash_key = dashcore::PrivateKey::new(key.inner.clone(), network_rust); let wif = dash_key.to_wif(); unwrap_or_return!(CString::new(wif), error).into_raw() @@ -449,7 +450,8 @@ pub unsafe extern "C" fn public_key_to_hex( error: *mut FFIError, ) -> *mut c_char { let key = deref_ptr!(key, error); - unwrap_or_return!(CString::new(key.inner.serialize().to_lower_hex_string()), error).into_raw() + unwrap_or_return!(CString::new(key.inner.to_compressed().to_lower_hex_string()), error) + .into_raw() } /// Convert derivation path string to indices diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs index 894a860a3..aa3abec2b 100644 --- a/key-wallet-ffi/src/transaction.rs +++ b/key-wallet-ffi/src/transaction.rs @@ -8,6 +8,7 @@ use crate::{check_ptr, FFIWalletManager}; use crate::{deref_ptr, deref_ptr_mut, unwrap_or_return}; use dash_network::ffi::FFINetwork; use dashcore::blockdata::script::Builder; +use dashcore::ecdsa::EcdsaSecretKey; use dashcore::{ consensus, hashes::Hash, sighash::SighashCache, EcdsaSighashType, Network, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut, Txid, @@ -18,7 +19,6 @@ use key_wallet::wallet::managed_wallet_info::asset_lock_builder::{ use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy::BranchAndBound; use key_wallet::wallet::managed_wallet_info::fee::FeeRate; use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; -use secp256k1::{Message, SecretKey}; use std::ffi::{CStr, CString}; use std::os::raw::c_char; use std::ptr; @@ -596,7 +596,7 @@ pub unsafe extern "C" fn transaction_sign_input( let Ok(privkey_bytes) = <[u8; 32]>::try_from(slice::from_raw_parts(private_key, 32)) else { return -1; }; - let privkey = match SecretKey::from_secret_bytes(privkey_bytes) { + let privkey = match EcdsaSecretKey::from_bytes(&privkey_bytes) { Ok(k) => k, Err(_) => { return -1; @@ -604,16 +604,15 @@ pub unsafe extern "C" fn transaction_sign_input( }; // Sign - let message = Message::from_digest(sighash); - let sig = privkey.sign_ecdsa(message); + let sig = privkey.sign(&sighash); // Build signature script (simplified P2PKH) - let pubkey = dashcore::PublicKey::new(secp256k1::PublicKey::from_secret_key(&privkey)); + let pubkey = dashcore::PublicKey::new(privkey.public_key()); tx.inner.input[input_index].script_sig = Builder::new() .push_slice( dashcore::ecdsa::Signature { - sig, - hash_ty: EcdsaSighashType::from_consensus(sighash_type), + signature: sig.into(), + sighash_type: EcdsaSighashType::from_consensus(sighash_type), } .serialize(), ) @@ -992,6 +991,7 @@ pub unsafe extern "C" fn wallet_build_and_sign_asset_lock_transaction( #[cfg(test)] mod tests { use super::*; + use dashcore::ecdsa::EcdsaSignature; #[test] fn script_p2pkh_is_dup_hash160_push_equalverify_checksig() { @@ -1015,8 +1015,7 @@ mod tests { #[test_case::test_case(0xffff_ff81, false ; "all anyone can pay with high bits")] fn sign_input_appends_the_sighash_type_it_signed(sighash_type: u32, ok: bool) { let secret = [0x42u8; 32]; - let pubkey = - secp256k1::PublicKey::from_secret_key(&SecretKey::from_secret_bytes(secret).unwrap()); + let pubkey = EcdsaSecretKey::from_bytes(&secret).unwrap().public_key(); let script_pubkey = ScriptBuf::new_p2pkh(&dashcore::PublicKey::new(pubkey).pubkey_hash()); unsafe { @@ -1048,7 +1047,7 @@ mod tests { let sig = pushes.next().unwrap(); let (&hash_ty, der) = sig.split_last().unwrap(); assert_eq!(hash_ty as u32, sighash_type); - assert_eq!(pushes.next().unwrap(), pubkey.serialize()); + assert_eq!(pushes.next().unwrap(), pubkey.to_compressed()); assert_eq!(pushes.next(), None); let mut sighash = [0u8; 32]; @@ -1063,10 +1062,8 @@ mod tests { ), 0 ); - secp256k1::ecdsa::Signature::from_der(der) - .unwrap() - .verify(Message::from_digest(sighash), &pubkey) - .unwrap(); + let sig = EcdsaSignature::from_der(der).unwrap(); + pubkey.verify(&sighash, sig).unwrap(); } transaction_destroy(tx); diff --git a/key-wallet-ffi/src/tx_decode.rs b/key-wallet-ffi/src/tx_decode.rs index fc8e2b0ce..c4ffbb5f6 100644 --- a/key-wallet-ffi/src/tx_decode.rs +++ b/key-wallet-ffi/src/tx_decode.rs @@ -249,7 +249,7 @@ mod tests { //! (signature + pubkey pushes), so that fixture stays hand-built. use super::*; use dashcore::consensus::serialize; - use dashcore::secp256k1::SecretKey; + use dashcore::ecdsa::EcdsaSecretKey; use dashcore::{OutPoint, TxIn, TxOut, Txid, Witness}; use std::ffi::CStr; @@ -318,7 +318,7 @@ mod tests { } fn test_pubkey() -> PublicKey { - let sk = SecretKey::from_secret_bytes([0x42u8; 32]).expect("valid secret key"); + let sk = EcdsaSecretKey::from_bytes(&[0x42u8; 32]).expect("valid secret key"); PublicKey::new(sk.public_key()) } diff --git a/key-wallet-manager/src/error.rs b/key-wallet-manager/src/error.rs index f644fcf57..0a5c51a27 100644 --- a/key-wallet-manager/src/error.rs +++ b/key-wallet-manager/src/error.rs @@ -101,7 +101,6 @@ impl From for WalletError { WalletError::InvalidParameter(format!("Serialization error: {}", msg)) } Error::Bip32(e) => WalletError::AccountCreation(format!("BIP32 error: {}", e)), - Error::Secp256k1(e) => WalletError::AccountCreation(format!("Secp256k1 error: {}", e)), Error::Base58 => WalletError::InvalidParameter("Base58 decoding error".to_string()), Error::NoKeySource => { WalletError::InvalidParameter("No key source available".to_string()) diff --git a/key-wallet/Cargo.toml b/key-wallet/Cargo.toml index aecf3e874..1a9e6c08b 100644 --- a/key-wallet/Cargo.toml +++ b/key-wallet/Cargo.toml @@ -12,7 +12,7 @@ version.workspace = true [features] default = ["getrandom"] -serde = ["dep:serde", "dep:serde_json", "dashcore_hashes/serde", "secp256k1/serde", "dashcore/serde"] +serde = ["dep:serde", "dep:serde_json", "dashcore_hashes/serde", "dashcore/serde"] bincode = ["serde", "dep:bincode", "dep:bincode_derive", "dashcore_hashes/bincode", "dashcore/bincode"] eddsa = ["dashcore/eddsa"] bls = ["dashcore/bls"] @@ -35,7 +35,6 @@ keep-finalized-transactions = [] internals = { path = "../internals", package = "dashcore-private" } dashcore_hashes = { path = "../hashes" } dashcore = { path="../dash" } -secp256k1 = { workspace = true, features = ["recovery"] } bip39 = { version = "2.2.0", default-features = false, features = ["std", "chinese-simplified", "chinese-traditional", "czech", "french", "italian", "japanese", "korean", "portuguese", "spanish", "zeroize"] } unicode-normalization = "0.1" serde = { workspace = true, features = ["derive"], optional = true } @@ -51,6 +50,7 @@ tracing = "0.1" async-trait = "0.1" [dev-dependencies] +bincode = { workspace = true, features = ["serde"] } criterion = "0.8.1" dashcore = { path="../dash", features = ["test-utils"] } key-wallet = { path = ".", features = ["test-utils", "serde", "bincode", "eddsa", "bls"] } diff --git a/key-wallet/src/account/mod.rs b/key-wallet/src/account/mod.rs index e1c595f0b..e10954330 100644 --- a/key-wallet/src/account/mod.rs +++ b/key-wallet/src/account/mod.rs @@ -158,7 +158,7 @@ impl AccountTrait for Account { } fn get_public_key_bytes(&self) -> Vec { - self.account_xpub.public_key.serialize().to_vec() + self.account_xpub.public_key.to_compressed().to_vec() } } @@ -316,7 +316,7 @@ impl AccountDerivation std::result::Result { let xpriv = self.derive_from_master_xpriv_extended_xpriv_at(master_xpriv, index)?; // Wrap into dashcore::PrivateKey with compressed=true - Ok(dashcore::PrivateKey::new(xpriv.private_key, self.network)) + Ok(dashcore::PrivateKey::new(xpriv.private_key.clone(), self.network)) } fn derive_from_seed_extended_xpriv_at( @@ -334,7 +334,7 @@ impl AccountDerivation std::result::Result { let xpriv = self.derive_from_seed_extended_xpriv_at(seed, index)?; - Ok(dashcore::PrivateKey::new(xpriv.private_key, self.network)) + Ok(dashcore::PrivateKey::new(xpriv.private_key.clone(), self.network)) } } diff --git a/key-wallet/src/bip32.rs b/key-wallet/src/bip32.rs index cf618fdc4..7c13626bc 100644 --- a/key-wallet/src/bip32.rs +++ b/key-wallet/src/bip32.rs @@ -27,13 +27,14 @@ use core::ops::Index; use core::str::FromStr; use std::error; +use dashcore::key::XOnlyPublicKey; use dashcore_hashes::{hash160, sha512, Hash, HashEngine, Hmac, HmacEngine}; -use secp256k1::{self, XOnlyPublicKey}; #[cfg(feature = "serde")] use serde; #[cfg(feature = "bincode")] use bincode_derive::{Decode, Encode}; +use dashcore::ecdsa::{EcdsaError, EcdsaPublicKey, EcdsaSecretKey, ECDSA_SK_LEN}; use dashcore::{base58, Network}; use hex_conservative::DisplayHex; use zeroize::Zeroize; @@ -42,10 +43,7 @@ use zeroize::Zeroize; type XpubIdentifier = hash160::Hash; pub use crate::dip9::{ApplicationKeyPurpose, KeyDerivationType}; -pub use secp256k1::Keypair; -pub use secp256k1::PublicKey; -/// Re-export key types from secp256k1 -pub use secp256k1::SecretKey as PrivateKey; +pub use dashcore::key::Keypair; /// A chain code #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -338,19 +336,17 @@ pub struct ExtendedPrivKey { /// Child number of the key used to derive from parent (0 for master) pub child_number: ChildNumber, /// Private key - pub private_key: secp256k1::SecretKey, + pub private_key: EcdsaSecretKey, /// Chain code pub chain_code: ChainCode, } -// Hand-written (not `#[derive(Zeroize)]`): `secp256k1::SecretKey` has no -// `Zeroize` impl, only `non_secure_erase()`, so a derive would not compile. // `Drop` (below) calls this, so the value is wiped automatically on scope exit // with no caller action required. Cf. `RootExtendedPrivKey`. impl zeroize::Zeroize for ExtendedPrivKey { fn zeroize(&mut self) { // Secret key material. - self.private_key.non_secure_erase(); + self.private_key.zeroize(); self.chain_code.zeroize(); // Derivation metadata — cleared too so the whole value is wiped. self.depth.zeroize(); @@ -379,7 +375,7 @@ impl bincode::Encode for ExtendedPrivKey { self.parent_fingerprint.encode(encoder)?; self.child_number.encode(encoder)?; // Encode the private key as bytes - self.private_key.to_secret_bytes().encode(encoder)?; + self.private_key.to_bytes().encode(encoder)?; self.chain_code.encode(encoder)?; Ok(()) } @@ -396,10 +392,9 @@ impl bincode::Decode for ExtendedPrivKey { let child_number = ChildNumber::decode(decoder)?; // Decode the private key from bytes let private_key_bytes: [u8; 32] = <[u8; 32]>::decode(decoder)?; - let private_key = - secp256k1::SecretKey::from_secret_bytes(private_key_bytes).map_err(|e| { - bincode::error::DecodeError::OtherString(format!("Invalid private key: {}", e)) - })?; + let private_key = EcdsaSecretKey::from_bytes(&private_key_bytes).map_err(|e| { + bincode::error::DecodeError::OtherString(format!("Invalid private key: {}", e)) + })?; let chain_code = ChainCode::decode(decoder)?; Ok(ExtendedPrivKey { @@ -418,26 +413,7 @@ impl<'de, C> bincode::BorrowDecode<'de, C> for ExtendedPrivKey { fn borrow_decode>( decoder: &mut D, ) -> Result { - let network = Network::borrow_decode(decoder)?; - let depth = u8::borrow_decode(decoder)?; - let parent_fingerprint = Fingerprint::borrow_decode(decoder)?; - let child_number = ChildNumber::borrow_decode(decoder)?; - // Decode the private key from bytes - let private_key_bytes: [u8; 32] = <[u8; 32]>::borrow_decode(decoder)?; - let private_key = - secp256k1::SecretKey::from_secret_bytes(private_key_bytes).map_err(|e| { - bincode::error::DecodeError::OtherString(format!("Invalid private key: {}", e)) - })?; - let chain_code = ChainCode::borrow_decode(decoder)?; - - Ok(ExtendedPrivKey { - network, - depth, - parent_fingerprint, - child_number, - private_key, - chain_code, - }) + >::decode(decoder) } } @@ -486,7 +462,7 @@ pub struct ExtendedPubKey { /// Child number of the key used to derive from parent (0 for master) pub child_number: ChildNumber, /// Public key - pub public_key: secp256k1::PublicKey, + pub public_key: EcdsaPublicKey, /// Chain code pub chain_code: ChainCode, } @@ -502,7 +478,7 @@ impl bincode::Encode for ExtendedPubKey { self.parent_fingerprint.encode(encoder)?; self.child_number.encode(encoder)?; // Encode the public key as bytes (33 bytes for compressed) - self.public_key.serialize().encode(encoder)?; + self.public_key.to_compressed().encode(encoder)?; self.chain_code.encode(encoder)?; Ok(()) } @@ -519,10 +495,9 @@ impl bincode::Decode for ExtendedPubKey { let child_number = ChildNumber::decode(decoder)?; // Decode the public key from bytes (33 bytes for compressed) let public_key_bytes: [u8; 33] = <[u8; 33]>::decode(decoder)?; - let public_key = secp256k1::PublicKey::from_byte_array_compressed(public_key_bytes) - .map_err(|e| { - bincode::error::DecodeError::OtherString(format!("Invalid public key: {}", e)) - })?; + let public_key = EcdsaPublicKey::from_bytes(&public_key_bytes).map_err(|e| { + bincode::error::DecodeError::OtherString(format!("Invalid public key: {}", e)) + })?; let chain_code = ChainCode::decode(decoder)?; Ok(ExtendedPubKey { @@ -541,26 +516,7 @@ impl<'de, C> bincode::BorrowDecode<'de, C> for ExtendedPubKey { fn borrow_decode>( decoder: &mut D, ) -> Result { - let network = Network::borrow_decode(decoder)?; - let depth = u8::borrow_decode(decoder)?; - let parent_fingerprint = Fingerprint::borrow_decode(decoder)?; - let child_number = ChildNumber::borrow_decode(decoder)?; - // Decode the public key from bytes (33 bytes for compressed) - let public_key_bytes: [u8; 33] = <[u8; 33]>::borrow_decode(decoder)?; - let public_key = secp256k1::PublicKey::from_byte_array_compressed(public_key_bytes) - .map_err(|e| { - bincode::error::DecodeError::OtherString(format!("Invalid public key: {}", e)) - })?; - let chain_code = ChainCode::borrow_decode(decoder)?; - - Ok(ExtendedPubKey { - network, - depth, - parent_fingerprint, - child_number, - public_key, - chain_code, - }) + >::decode(decoder) } } @@ -1235,8 +1191,8 @@ pub type KeySource = (Fingerprint, DerivationPath); pub enum Error { /// A pk->pk derivation was attempted on a hardened key CannotDeriveFromHardenedKey, - /// A secp256k1 error occurred - Secp256k1(secp256k1::Error), + /// An ECDSA key error occurred + Ecdsa(EcdsaError), /// A child number was provided that was out of range InvalidChildNumber(u32), /// Invalid childnumber format. @@ -1263,7 +1219,7 @@ impl fmt::Display for Error { Error::CannotDeriveFromHardenedKey => { f.write_str("cannot derive hardened key from public key") } - Error::Secp256k1(ref e) => fmt::Display::fmt(e, f), + Error::Ecdsa(ref e) => fmt::Display::fmt(e, f), Error::InvalidChildNumber(ref n) => { write!(f, "child number {} is invalid (not within [0, 2^31 - 1])", n) } @@ -1289,17 +1245,16 @@ impl fmt::Display for Error { impl error::Error for Error { fn cause(&self) -> Option<&dyn error::Error> { - if let Error::Secp256k1(ref e) = *self { - Some(e) - } else { - None + match *self { + Error::Ecdsa(ref e) => Some(e), + _ => None, } } } -impl From for Error { - fn from(e: secp256k1::Error) -> Error { - Error::Secp256k1(e) +impl From for Error { + fn from(e: EcdsaError) -> Error { + Error::Ecdsa(e) } } @@ -1310,11 +1265,8 @@ impl From for Error { } /// The secret-key half of a BIP32 HMAC-SHA512 output. -fn hmac_secret_half(hmac: &Hmac) -> [u8; secp256k1::constants::SECRET_KEY_SIZE] { - *hmac - .as_byte_array() - .first_chunk::<{ secp256k1::constants::SECRET_KEY_SIZE }>() - .expect("sha512 HMAC output is 64 bytes") +fn hmac_secret_half(hmac: &Hmac) -> [u8; ECDSA_SK_LEN] { + *hmac.as_byte_array().first_chunk::().expect("sha512 HMAC output is 64 bytes") } impl ExtendedPrivKey { @@ -1334,7 +1286,7 @@ impl ExtendedPrivKey { depth: 0, parent_fingerprint: Default::default(), child_number: ChildNumber::from_normal_idx(0)?, - private_key: secp256k1::SecretKey::from_secret_bytes(hmac_secret_half(&hmac_result))?, + private_key: EcdsaSecretKey::from_bytes(&hmac_secret_half(&hmac_result))?, chain_code: ChainCode::from_hmac(hmac_result), }) } @@ -1342,7 +1294,7 @@ impl ExtendedPrivKey { /// Constructs BIP340 keypair for Schnorr signatures and Taproot use matching the internal /// secret key representation. pub fn to_keypair(&self) -> Keypair { - Keypair::from_secret_key(&self.private_key) + Keypair::from(&self.private_key) } /// Attempts to derive an extended private key from a path. @@ -1364,7 +1316,7 @@ impl ExtendedPrivKey { index, } => { // Non-hardened key: compute public data and use that - hmac_engine.input(&self.private_key.public_key().serialize()[..]); + hmac_engine.input(&self.private_key.public_key().to_compressed()[..]); hmac_engine.input(&index.to_be_bytes()); } ChildNumber::Hardened { @@ -1372,14 +1324,14 @@ impl ExtendedPrivKey { } => { // Hardened key: use only secret data to prevent public derivation hmac_engine.input(&[0u8]); - hmac_engine.input(&self.private_key[..]); + hmac_engine.input(&self.private_key.to_bytes()[..]); hmac_engine.input(&(index | (1 << 31)).to_be_bytes()); } ChildNumber::Normal256 { index, } => { // Non-hardened key with 256-bit index - hmac_engine.input(&self.private_key.public_key().serialize()[..]); + hmac_engine.input(&self.private_key.public_key().to_compressed()[..]); hmac_engine.input(&index); } ChildNumber::Hardened256 { @@ -1387,16 +1339,16 @@ impl ExtendedPrivKey { } => { // Hardened key with 256-bit index hmac_engine.input(&[0u8]); - hmac_engine.input(&self.private_key[..]); + hmac_engine.input(&self.private_key.to_bytes()[..]); hmac_engine.input(&index); } } let hmac_result: Hmac = Hmac::from_engine(hmac_engine); - // IL added to the parent scalar; this rejects IL >= n and a zero sum, - // the two cases BIP32 declares invalid. - let tweaked = secp256k1::Scalar::from_be_bytes(hmac_secret_half(&hmac_result)) - .ok() - .and_then(|il| self.private_key.add_tweak(&il).ok()) + // IL added to the parent scalar; `add_tweak` rejects IL >= n and a zero + // sum, the two cases BIP32 declares invalid. + let tweaked = self + .private_key + .add_tweak(&hmac_secret_half(&hmac_result)) .expect("statistically impossible to hit"); Ok(ExtendedPrivKey { @@ -1450,8 +1402,8 @@ impl ExtendedPrivKey { chain_code: data[13..45] .try_into() .expect("45 - 13 == 32, which is the ChainCode length"), - private_key: secp256k1::SecretKey::from_secret_bytes( - <[u8; 32]>::try_from(&data[46..78]).expect("78 - 46 == 32, the key length"), + private_key: EcdsaSecretKey::from_bytes( + data[46..78].try_into().expect("78 - 46 == 32, the key length"), )?, }) } @@ -1470,7 +1422,7 @@ impl ExtendedPrivKey { ret[9..13].copy_from_slice(&u32::from(self.child_number).to_be_bytes()); ret[13..45].copy_from_slice(&self.chain_code[..]); ret[45] = 0; - ret[46..78].copy_from_slice(&self.private_key[..]); + ret[46..78].copy_from_slice(&self.private_key.to_bytes()[..]); ret } @@ -1506,8 +1458,8 @@ impl ExtendedPrivKey { }; let chain_code = data[42..74].try_into().expect("32 bytes for chain code"); - let private_key = secp256k1::SecretKey::from_secret_bytes( - <[u8; 32]>::try_from(&data[75..107]).expect("107 - 75 == 32, the key length"), + let private_key = EcdsaSecretKey::from_bytes( + &<[u8; 32]>::try_from(&data[75..107]).expect("107 - 75 == 32, the key length"), )?; Ok(ExtendedPrivKey { @@ -1566,7 +1518,7 @@ impl ExtendedPrivKey { // Key data (33 bytes) ret[74] = 0x00; // Padding for private key - ret[75..107].copy_from_slice(&self.private_key[..]); + ret[75..107].copy_from_slice(&self.private_key.to_bytes()[..]); ret } @@ -1583,7 +1535,7 @@ impl ExtendedPrivKey { /// Convert to a PrivateKey for signing operations pub fn to_priv(&self) -> dashcore::PrivateKey { - dashcore::PrivateKey::new(self.private_key, self.network) + dashcore::PrivateKey::new(self.private_key.clone(), self.network) } } @@ -1625,10 +1577,7 @@ impl ExtendedPubKey { /// Compute the scalar tweak added to this key to get a child key /// Compute the scalar tweak added to this key to get a child key - pub fn ckd_pub_tweak( - &self, - i: ChildNumber, - ) -> Result<(secp256k1::SecretKey, ChainCode), Error> { + pub fn ckd_pub_tweak(&self, i: ChildNumber) -> Result<(EcdsaSecretKey, ChainCode), Error> { match i { ChildNumber::Hardened { .. @@ -1641,13 +1590,12 @@ impl ExtendedPubKey { } => { let mut hmac_engine: HmacEngine = HmacEngine::new(&self.chain_code[..]); - hmac_engine.input(&self.public_key.serialize()[..]); + hmac_engine.input(&self.public_key.to_compressed()[..]); hmac_engine.input(&n.to_be_bytes()); let hmac_result: Hmac = Hmac::from_engine(hmac_engine); - let private_key = - secp256k1::SecretKey::from_secret_bytes(hmac_secret_half(&hmac_result))?; + let private_key = EcdsaSecretKey::from_bytes(&hmac_secret_half(&hmac_result))?; let chain_code = ChainCode::from_hmac(hmac_result); Ok((private_key, chain_code)) } @@ -1659,14 +1607,13 @@ impl ExtendedPubKey { HmacEngine::new(&self.chain_code[..]); // HMAC Input: serP(Kpar) || ser256(i) - hmac_engine.input(&self.public_key.serialize()[..]); + hmac_engine.input(&self.public_key.to_compressed()[..]); hmac_engine.input(&idx); let hmac_result: Hmac = Hmac::from_engine(hmac_engine); // IL must be less than n (order of the curve) - let private_key = - secp256k1::SecretKey::from_secret_bytes(hmac_secret_half(&hmac_result))?; + let private_key = EcdsaSecretKey::from_bytes(&hmac_secret_half(&hmac_result))?; let chain_code = ChainCode::from_hmac(hmac_result); Ok((private_key, chain_code)) @@ -1677,7 +1624,7 @@ impl ExtendedPubKey { /// Public->Public child key derivation pub fn ckd_pub(&self, i: ChildNumber) -> Result { let (sk, chain_code) = self.ckd_pub_tweak(i)?; - let tweaked = self.public_key.add_exp_tweak(&sk.into())?; + let tweaked = self.public_key.add_tweak(&sk.to_bytes())?; Ok(ExtendedPubKey { network: self.network, @@ -1730,9 +1677,7 @@ impl ExtendedPubKey { chain_code: data[13..45] .try_into() .expect("45 - 13 == 32, which is the ChainCode length"), - public_key: secp256k1::PublicKey::from_byte_array_compressed( - <[u8; 33]>::try_from(&data[45..78]).expect("78 - 45 == 33, the key length"), - )?, + public_key: EcdsaPublicKey::from_bytes(&data[45..78])?, }) } @@ -1749,7 +1694,7 @@ impl ExtendedPubKey { ret[5..9].copy_from_slice(&self.parent_fingerprint[..]); ret[9..13].copy_from_slice(&u32::from(self.child_number).to_be_bytes()); ret[13..45].copy_from_slice(&self.chain_code[..]); - ret[45..78].copy_from_slice(&self.public_key.serialize()[..]); + ret[45..78].copy_from_slice(&self.public_key.to_compressed()[..]); ret } @@ -1798,7 +1743,7 @@ impl ExtendedPubKey { ret[42..74].copy_from_slice(&self.chain_code[..]); // Key data (33 bytes) - ret[74..107].copy_from_slice(&self.public_key.serialize()[..]); + ret[74..107].copy_from_slice(&self.public_key.to_compressed()[..]); ret } @@ -1837,9 +1782,7 @@ impl ExtendedPubKey { let chain_code = data[42..74].try_into().expect("32 bytes for chain code"); // Key data (33 bytes) - let public_key = secp256k1::PublicKey::from_byte_array_compressed( - <[u8; 33]>::try_from(&data[74..107]).expect("107 - 74 == 33, the key length"), - )?; + let public_key = EcdsaPublicKey::from_bytes(&data[74..107])?; Ok(ExtendedPubKey { network, @@ -2302,10 +2245,10 @@ mod tests { } #[test] - #[should_panic(expected = "Secp256k1(InvalidSecretKey)")] + #[should_panic(expected = "Ecdsa(InvalidSecretKey)")] fn schnorr_broken_privkey_zeros() { /* this is how we generate key: - let mut sk = secp256k1::key::ONE_KEY; + let mut sk = ONE_KEY; let zeros = [0u8; 32]; unsafe { @@ -2330,7 +2273,7 @@ mod tests { } #[test] - #[should_panic(expected = "Secp256k1(InvalidSecretKey)")] + #[should_panic(expected = "Ecdsa(InvalidSecretKey)")] fn schnorr_broken_privkey_ffs() { // Xpriv having secret key set to all 0xFF's let xpriv_str = "xprv9s21ZrQH143K24Mfq5zL5MhWK9hUhhGbd45hLXo2Pq2oqzMMo63oStZzFAzHGBP2UuGCqWLTAPLcMtD9y5gkZ6Eq3Rjuahrv17fENZ3QzxW"; @@ -2549,7 +2492,7 @@ mod tests { path.derive_pub_ecdsa_for_master_seed(&seed, network) .unwrap() .public_key - .serialize() + .to_compressed() .to_lower_hex_string() }; diff --git a/key-wallet/src/derivation.rs b/key-wallet/src/derivation.rs index b3c68a025..1a99956e2 100644 --- a/key-wallet/src/derivation.rs +++ b/key-wallet/src/derivation.rs @@ -274,6 +274,7 @@ impl DerivationStrategy { mod tests { use super::*; use crate::mnemonic::Mnemonic; + use dashcore::ecdsa::EcdsaPublicKey; use dashcore_hashes::Hash; // ✓ Test BIP32 derivation with exact DashSync test vectors @@ -308,7 +309,7 @@ mod tests { ) .unwrap(); // Skip the first byte (network prefix) and compare the actual 32-byte key - assert_eq!(&derived_key.private_key.to_secret_bytes(), &expected_with_prefix[1..]); + assert_eq!(&derived_key.private_key.to_bytes()[..], &expected_with_prefix[1..]); // Test m/0'/0/97 path for zero padding test (from DashSync) let path_zero_padding = DerivationPath::from(vec![ @@ -330,7 +331,7 @@ mod tests { "00136c1ad038f9a00871895322a487ed14f1cdc4d22ad351cfa1a0d235975dd7", ) .unwrap(); - assert_eq!(&derived_key_zero.private_key.to_secret_bytes(), &expected_zero_padded[..]); + assert_eq!(&derived_key_zero.private_key.to_bytes()[..], &expected_zero_padded[..]); } // ✓ Test extended key serialization (from DashSync DSBIP32Tests.m) @@ -423,7 +424,10 @@ mod tests { ]); let identity_key = master_key.derive_priv(&identity_auth_path).unwrap(); - assert_ne!(&identity_key.private_key[..], &master_key.private_key[..]); + assert_ne!( + &identity_key.private_key.to_bytes()[..], + &master_key.private_key.to_bytes()[..] + ); // Test identity registration derivation // m/9'/5'/1'/1 (DIP-9: Identity Registration) @@ -443,7 +447,7 @@ mod tests { ]); let reg_key = master_key.derive_priv(&identity_reg_path).unwrap(); - assert_ne!(®_key.private_key[..], &identity_key.private_key[..]); + assert_ne!(®_key.private_key.to_bytes()[..], &identity_key.private_key.to_bytes()[..]); // Test identity top-up derivation // m/9'/5'/1'/2 (DIP-9: Identity Top-up) @@ -463,8 +467,8 @@ mod tests { ]); let topup_key = master_key.derive_priv(&identity_topup_path).unwrap(); - assert_ne!(&topup_key.private_key[..], ®_key.private_key[..]); - assert_ne!(&topup_key.private_key[..], &identity_key.private_key[..]); + assert_ne!(&topup_key.private_key.to_bytes()[..], ®_key.private_key.to_bytes()[..]); + assert_ne!(&topup_key.private_key.to_bytes()[..], &identity_key.private_key.to_bytes()[..]); // Test provider voting derivation (masternode voting) // m/3'/1'/0' (Provider voting) @@ -481,7 +485,7 @@ mod tests { ]); let voting_key = master_key.derive_priv(&provider_voting_path).unwrap(); - assert_ne!(&voting_key.private_key[..], &topup_key.private_key[..]); + assert_ne!(&voting_key.private_key.to_bytes()[..], &topup_key.private_key.to_bytes()[..]); // Test provider operator derivation // m/3'/0'/0' (Provider operator) @@ -498,7 +502,10 @@ mod tests { ]); let operator_key = master_key.derive_priv(&provider_op_path).unwrap(); - assert_ne!(&operator_key.private_key[..], &voting_key.private_key[..]); + assert_ne!( + &operator_key.private_key.to_bytes()[..], + &voting_key.private_key.to_bytes()[..] + ); } // ✓ Test derivation path builder pattern @@ -541,7 +548,7 @@ mod tests { // Test derivation with the built path let derived = master_key.derive_priv(&bip44_path).unwrap(); - assert_ne!(&derived.private_key[..], &master_key.private_key[..]); + assert_ne!(&derived.private_key.to_bytes()[..], &master_key.private_key.to_bytes()[..]); } // ✓ Test key signing and verification @@ -563,22 +570,15 @@ mod tests { let message_hash = dashcore_hashes::sha256::Hash::hash(message); // Sign the message (deterministic signing) - let signature1 = signing_key - .private_key - .sign_ecdsa(secp256k1::Message::from_digest(message_hash.to_byte_array())); - let signature2 = signing_key - .private_key - .sign_ecdsa(secp256k1::Message::from_digest(message_hash.to_byte_array())); + let signature1 = signing_key.private_key.sign(&message_hash.to_byte_array()); + let signature2 = signing_key.private_key.sign(&message_hash.to_byte_array()); // Signatures should be the same (deterministic) assert_eq!(signature1, signature2); // Verify the signature let pubkey = ExtendedPubKey::from_priv(&signing_key); - let verified = signature1.verify( - secp256k1::Message::from_digest(message_hash.to_byte_array()), - &pubkey.public_key, - ); + let verified = pubkey.public_key.verify(&message_hash.to_byte_array(), signature1); assert!(verified.is_ok()); } @@ -602,15 +602,11 @@ mod tests { let message_hash = dashcore_hashes::sha256::Hash::hash(message); // Create recoverable signature - let signature = secp256k1::ecdsa::RecoverableSignature::sign_ecdsa_recoverable( - secp256k1::Message::from_digest(message_hash.to_byte_array()), - &signing_key.private_key, - ); + let signature = signing_key.private_key.sign_recoverable(&message_hash.to_byte_array()); // Recover the public key from signature - let recovered_pubkey = signature - .recover(secp256k1::Message::from_digest(message_hash.to_byte_array())) - .unwrap(); + let recovered_pubkey = + EcdsaPublicKey::recover(&message_hash.to_byte_array(), &signature).unwrap(); // Should match original public key assert_eq!(recovered_pubkey, public_key.public_key); @@ -718,14 +714,9 @@ mod tests { // Verify the DashPay key can sign and verify messages let message = b"DashPay contact message"; let message_hash = dashcore_hashes::sha256::Hash::hash(message); - let signature = dashpay_key - .private_key - .sign_ecdsa(secp256k1::Message::from_digest(message_hash.to_byte_array())); + let signature = dashpay_key.private_key.sign(&message_hash.to_byte_array()); - let verified = signature.verify( - secp256k1::Message::from_digest(message_hash.to_byte_array()), - &dashpay_pubkey.public_key, - ); + let verified = dashpay_pubkey.public_key.verify(&message_hash.to_byte_array(), signature); assert!(verified.is_ok()); } } diff --git a/key-wallet/src/derivation_bls_bip32.rs b/key-wallet/src/derivation_bls_bip32.rs index 1ad8af6c9..a1677ea17 100644 --- a/key-wallet/src/derivation_bls_bip32.rs +++ b/key-wallet/src/derivation_bls_bip32.rs @@ -1709,6 +1709,7 @@ mod tests { #[test] fn secp_secret_above_the_order_is_reduced() { use crate::wallet::root_extended_keys::RootExtendedPrivKey; + use dashcore::ecdsa::EcdsaSecretKey; // secp256k1 draws from a larger field, so about half its secrets land // outside BLS's field, construct such a secret. @@ -1716,7 +1717,7 @@ mod tests { secret[0] = 0x02; secret[31] = 0xff; let root = RootExtendedPrivKey { - root_private_key: secp256k1::SecretKey::from_secret_bytes(secret).unwrap(), + root_private_key: EcdsaSecretKey::from_bytes(&secret).unwrap(), root_chain_code: ChainCode::from([3u8; 32]), }; diff --git a/key-wallet/src/error.rs b/key-wallet/src/error.rs index 3c5e0d0a2..816466ddc 100644 --- a/key-wallet/src/error.rs +++ b/key-wallet/src/error.rs @@ -23,8 +23,6 @@ pub enum Error { InvalidDerivationPath(String), /// Invalid address InvalidAddress(String), - /// Secp256k1 error - Secp256k1(secp256k1::Error), /// Base58 decoding error Base58, /// Invalid network @@ -73,7 +71,6 @@ impl fmt::Display for Error { Error::InvalidMnemonic(s) => write!(f, "Invalid mnemonic: {}", s), Error::InvalidDerivationPath(s) => write!(f, "Invalid derivation path: {}", s), Error::InvalidAddress(s) => write!(f, "Invalid address: {}", s), - Error::Secp256k1(e) => write!(f, "Secp256k1 error: {}", e), Error::Base58 => write!(f, "Base58 decoding error"), Error::InvalidNetwork => write!(f, "Invalid network"), Error::KeyError(s) => write!(f, "Key error: {}", s), @@ -111,7 +108,6 @@ impl error::Error for Error { Error::Bip32(e) => Some(e), #[cfg(feature = "eddsa")] Error::Slip10(e) => Some(e), - Error::Secp256k1(e) => Some(e), _ => None, } } @@ -123,12 +119,6 @@ impl From for Error { } } -impl From for Error { - fn from(e: secp256k1::Error) -> Self { - Error::Secp256k1(e) - } -} - #[cfg(feature = "eddsa")] impl From for Error { fn from(e: crate::derivation_slip10::Error) -> Self { diff --git a/key-wallet/src/managed_account/managed_account_trait.rs b/key-wallet/src/managed_account/managed_account_trait.rs index e162ab36a..291387f2d 100644 --- a/key-wallet/src/managed_account/managed_account_trait.rs +++ b/key-wallet/src/managed_account/managed_account_trait.rs @@ -497,7 +497,7 @@ pub trait ManagedAccountTrait { root_ext_priv.derive_priv(&info.path).map_err(|_| "Key derivation failed")?; let mut private_key = [0u8; 32]; - private_key.copy_from_slice(&derived_xpriv.private_key[..]); + private_key.copy_from_slice(&derived_xpriv.private_key.to_bytes()[..]); Ok(private_key) } diff --git a/key-wallet/src/signer.rs b/key-wallet/src/signer.rs index 05ca2b920..9ce091b55 100644 --- a/key-wallet/src/signer.rs +++ b/key-wallet/src/signer.rs @@ -12,7 +12,7 @@ //! sync derive-and-sign in `async {}` without meaningful overhead. use async_trait::async_trait; -use secp256k1::{ecdsa, PublicKey}; +use dashcore::ecdsa::{EcdsaPublicKey, EcdsaSignature}; use crate::bip32::{DerivationPath, ExtendedPubKey}; @@ -117,14 +117,14 @@ pub trait Signer: Send + Sync { &self, path: &DerivationPath, sighash: [u8; 32], - ) -> Result<(ecdsa::Signature, PublicKey), Self::Error>; + ) -> Result<(EcdsaSignature, EcdsaPublicKey), Self::Error>; /// Return the compressed public key at `path` without signing. /// /// Used to capture per-output public keys (e.g. asset-lock credit-output /// keys) that the caller later references when signing Platform state /// transitions. - async fn public_key(&self, path: &DerivationPath) -> Result; + async fn public_key(&self, path: &DerivationPath) -> Result; } /// A [`Signer`] that can additionally export BIP-32 extended public keys. diff --git a/key-wallet/src/tests/account_tests.rs b/key-wallet/src/tests/account_tests.rs index 8694b96d5..7b7bd46b4 100644 --- a/key-wallet/src/tests/account_tests.rs +++ b/key-wallet/src/tests/account_tests.rs @@ -398,7 +398,7 @@ fn test_account_network_consistency() { ]; let address_xpub = account.account_xpub.derive_pub(&receive_path).unwrap(); - let pubkey = dashcore::PublicKey::from_slice(&address_xpub.public_key.serialize()).unwrap(); + let pubkey = dashcore::PublicKey::from_slice(&address_xpub.public_key.to_compressed()).unwrap(); let address = dashcore::Address::p2pkh(&pubkey, network); // Verify the address is for the correct network diff --git a/key-wallet/src/tests/wallet_tests.rs b/key-wallet/src/tests/wallet_tests.rs index 834d1ff97..ad07ebd98 100644 --- a/key-wallet/src/tests/wallet_tests.rs +++ b/key-wallet/src/tests/wallet_tests.rs @@ -97,7 +97,7 @@ fn test_wallet_creation_from_extended_key() { let seed = mnemonic.to_seed(""); let root_key = RootExtendedPrivKey::new_master(&seed).unwrap(); let master_key = root_key.to_extended_priv_key(Network::Testnet); - let master_private_key = master_key.private_key; + let master_private_key = master_key.private_key.clone(); let wallet = Wallet::from_extended_key( master_key, diff --git a/key-wallet/src/wallet/helper.rs b/key-wallet/src/wallet/helper.rs index 1d49a39aa..07054ab40 100644 --- a/key-wallet/src/wallet/helper.rs +++ b/key-wallet/src/wallet/helper.rs @@ -7,6 +7,7 @@ use super::{Wallet, WalletType}; use crate::account::{Account, AccountType, StandardAccountType}; use crate::error::Result; use crate::Error; +use dashcore::ecdsa::{EcdsaPublicKey, EcdsaSecretKey}; use hex_conservative::DisplayHex; impl Wallet { @@ -407,10 +408,11 @@ impl Wallet { /// * `path` - The derivation path (e.g., "m/44'/5'/0'/0/0") /// /// # Returns - /// The private key (SecretKey), or an error if the wallet is watch-only or path is invalid - pub fn derive_private_key(&self, path: &crate::DerivationPath) -> Result { + /// The private key, or an error if the wallet is watch-only or the path + /// is invalid. + pub fn derive_private_key(&self, path: &crate::DerivationPath) -> Result { let extended = self.derive_extended_private_key(path)?; - Ok(extended.private_key) + Ok(extended.private_key.clone()) } /// Derive a private key at a specific derivation path and return as WIF @@ -495,9 +497,9 @@ impl Wallet { /// * `path` - The derivation path (e.g., "m/44'/5'/0'/0/0") /// /// # Returns - /// The public key (secp256k1::PublicKey), or an error if the path cannot be - /// derived for this wallet type. - pub fn derive_public_key(&self, path: &crate::DerivationPath) -> Result { + /// The public key, or an error if the path cannot be derived for this + /// wallet type. + pub fn derive_public_key(&self, path: &crate::DerivationPath) -> Result { // Check if the path contains hardened derivation let has_hardened = path.into_iter().any(|child| child.is_hardened()); @@ -511,7 +513,7 @@ impl Wallet { // For hardened paths, derive the private key first, then get public key let private_key = self.derive_private_key(path)?; - Ok(secp256k1::PublicKey::from_secret_key(&private_key)) + Ok(private_key.public_key()) } else { // For non-hardened paths, derive directly from public key let extended = self.derive_extended_public_key(path)?; @@ -536,7 +538,7 @@ impl Wallet { let public_key = self.derive_public_key(path)?; // Return as hex string - let serialized = public_key.serialize(); // compressed + let serialized = public_key.to_compressed(); Ok(serialized.to_lower_hex_string()) } diff --git a/key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs b/key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs index 322098b3c..355a444a5 100644 --- a/key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs +++ b/key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs @@ -5,8 +5,8 @@ use dashcore::blockdata::transaction::special_transaction::asset_lock::AssetLockPayload; use dashcore::blockdata::transaction::special_transaction::TransactionPayload; +use dashcore::ecdsa::EcdsaPublicKey; use dashcore::{OutPoint, Transaction, TxOut}; -use secp256k1::PublicKey; use std::fmt; use crate::account::AccountType; @@ -130,7 +130,7 @@ pub enum AssetLockCreditKeys { /// Public key + derivation path per credit output. Produced by /// [`ManagedWalletInfo::build_asset_lock_with_signer`] when the /// private keys never leave the signing device. - Public(Vec<(PublicKey, DerivationPath)>), + Public(Vec<(EcdsaPublicKey, DerivationPath)>), } /// Result of building an asset lock transaction. @@ -651,6 +651,7 @@ mod tests { use crate::signer::{ExtendedPubKeySigner, SignerMethod}; use crate::wallet::initialization::WalletAccountCreationOptions; use crate::{Network, Utxo}; + use dashcore::ecdsa::EcdsaSignature; use dashcore::{OutPoint, ScriptBuf, Txid}; use dashcore_hashes::Hash; use std::collections::HashSet; @@ -1005,25 +1006,24 @@ mod tests { &self, path: &DerivationPath, sighash: [u8; 32], - ) -> Result<(secp256k1::ecdsa::Signature, PublicKey), Self::Error> { + ) -> Result<(EcdsaSignature, EcdsaPublicKey), Self::Error> { let xpriv = self .root .to_extended_priv_key(self.network) .derive_priv(path) .map_err(|e| e.to_string())?; - let msg = secp256k1::Message::from_digest(sighash); - let sig = xpriv.private_key.sign_ecdsa(msg); - let pk = secp256k1::PublicKey::from_secret_key(&xpriv.private_key); + let sig = xpriv.private_key.sign(&sighash); + let pk = xpriv.private_key.public_key(); Ok((sig, pk)) } - async fn public_key(&self, path: &DerivationPath) -> Result { + async fn public_key(&self, path: &DerivationPath) -> Result { let xpriv = self .root .to_extended_priv_key(self.network) .derive_priv(path) .map_err(|e| e.to_string())?; - Ok(secp256k1::PublicKey::from_secret_key(&xpriv.private_key)) + Ok(xpriv.private_key.public_key()) } } @@ -1144,10 +1144,10 @@ mod tests { &self, _: &DerivationPath, _: [u8; 32], - ) -> Result<(secp256k1::ecdsa::Signature, PublicKey), Self::Error> { + ) -> Result<(EcdsaSignature, EcdsaPublicKey), Self::Error> { unreachable!("should be rejected before any signing is attempted") } - async fn public_key(&self, _: &DerivationPath) -> Result { + async fn public_key(&self, _: &DerivationPath) -> Result { unreachable!() } } @@ -1587,11 +1587,14 @@ mod tests { &self, _path: &DerivationPath, _sighash: [u8; 32], - ) -> Result<(secp256k1::ecdsa::Signature, PublicKey), Self::Error> { + ) -> Result<(EcdsaSignature, EcdsaPublicKey), Self::Error> { Err("signing device unavailable".to_string()) } - async fn public_key(&self, _path: &DerivationPath) -> Result { + async fn public_key( + &self, + _path: &DerivationPath, + ) -> Result { Err("signing device unavailable".to_string()) } } diff --git a/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs b/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs index 57afdefa6..832e7ab01 100644 --- a/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs +++ b/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs @@ -15,12 +15,11 @@ use dashcore::blockdata::opcodes; use dashcore::blockdata::script::{Builder, PushBytes, ScriptBuf}; use dashcore::blockdata::transaction::special_transaction::TransactionPayload; use dashcore::blockdata::transaction::{OutPoint, Transaction}; +use dashcore::ecdsa::{EcdsaPublicKey, EcdsaSignature}; use dashcore::sighash::{EcdsaSighashType, LegacySighash, SighashCache}; use dashcore::Address; use dashcore::{Network, TxIn, TxOut}; use dashcore_hashes::Hash; -use secp256k1::ecdsa::Signature; -use secp256k1::{Message, PublicKey}; use std::cmp::Ordering; use std::collections::HashSet; @@ -940,7 +939,7 @@ pub trait TransactionSigner { let (sig, pubkey) = self.sig_and_pubkey(sighash, path).await?; let script_sig = Builder::new() - .push_slice(dashcore::ecdsa::Signature::sighash_all(sig).serialize()) + .push_slice(dashcore::ecdsa::Signature::sighash_all(sig.into()).serialize()) .push_key(&dashcore::PublicKey::new(pubkey)) .into_script(); @@ -958,7 +957,7 @@ pub trait TransactionSigner { &self, sighash: LegacySighash, path: DerivationPath, - ) -> Result<(Signature, PublicKey), BuilderError>; + ) -> Result<(EcdsaSignature, EcdsaPublicKey), BuilderError>; } #[async_trait::async_trait] @@ -967,7 +966,7 @@ impl TransactionSigner for Wallet { &self, sighash: LegacySighash, path: DerivationPath, - ) -> Result<(Signature, PublicKey), BuilderError> { + ) -> Result<(EcdsaSignature, EcdsaPublicKey), BuilderError> { let root_xpriv = self.root_extended_priv_key().map_err(|_| BuilderError::WatchOnlyWallet)?; @@ -975,11 +974,10 @@ impl TransactionSigner for Wallet { let derived_xpriv = root_ext_priv.derive_priv(&path).map_err(|e| { BuilderError::SigningFailed(format!("couldn't derive extended priv key: {}", e)) })?; - let key = derived_xpriv.private_key; + let key = &derived_xpriv.private_key; - let message = Message::from_digest(*sighash.as_byte_array()); - let signature = key.sign_ecdsa(message); - let pubkey = PublicKey::from_secret_key(&key); + let signature = key.sign(sighash.as_byte_array()); + let pubkey = key.public_key(); Ok((signature, pubkey)) } @@ -991,7 +989,7 @@ impl TransactionSigner for S { &self, sighash: LegacySighash, path: DerivationPath, - ) -> Result<(Signature, PublicKey), BuilderError> { + ) -> Result<(EcdsaSignature, EcdsaPublicKey), BuilderError> { if !self.supports(crate::signer::SignerMethod::Digest) { return Err(BuilderError::SigningFailed(format!( "signer does not support required method {:?}", diff --git a/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs b/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs index e82a1212e..f90b5b2c5 100644 --- a/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs +++ b/key-wallet/src/wallet/managed_wallet_info/transaction_building.rs @@ -635,7 +635,7 @@ mod tests { use crate::DerivationPath; use crate::Wallet; use dashcore::address::NetworkUnchecked; - use secp256k1::PublicKey; + use dashcore::ecdsa::{EcdsaPublicKey, EcdsaSignature}; fn test_wallet_and_info() -> (Wallet, ManagedWalletInfo) { let wallet = @@ -667,25 +667,24 @@ mod tests { &self, path: &DerivationPath, sighash: [u8; 32], - ) -> Result<(secp256k1::ecdsa::Signature, PublicKey), Self::Error> { + ) -> Result<(EcdsaSignature, EcdsaPublicKey), Self::Error> { let xpriv = self .root .to_extended_priv_key(self.network) .derive_priv(path) .map_err(|e| e.to_string())?; - let msg = secp256k1::Message::from_digest(sighash); - let sig = xpriv.private_key.sign_ecdsa(msg); - let pk = secp256k1::PublicKey::from_secret_key(&xpriv.private_key); + let sig = xpriv.private_key.sign(&sighash); + let pk = xpriv.private_key.public_key(); Ok((sig, pk)) } - async fn public_key(&self, path: &DerivationPath) -> Result { + async fn public_key(&self, path: &DerivationPath) -> Result { let xpriv = self .root .to_extended_priv_key(self.network) .derive_priv(path) .map_err(|e| e.to_string())?; - Ok(secp256k1::PublicKey::from_secret_key(&xpriv.private_key)) + Ok(xpriv.private_key.public_key()) } } @@ -779,10 +778,10 @@ mod tests { &self, _: &DerivationPath, _: [u8; 32], - ) -> Result<(secp256k1::ecdsa::Signature, PublicKey), Self::Error> { + ) -> Result<(EcdsaSignature, EcdsaPublicKey), Self::Error> { unreachable!("should be rejected before any signing is attempted") } - async fn public_key(&self, _: &DerivationPath) -> Result { + async fn public_key(&self, _: &DerivationPath) -> Result { unreachable!() } } diff --git a/key-wallet/src/wallet/mod.rs b/key-wallet/src/wallet/mod.rs index 926056039..5f7b0662b 100644 --- a/key-wallet/src/wallet/mod.rs +++ b/key-wallet/src/wallet/mod.rs @@ -104,7 +104,7 @@ impl Wallet { /// distinct ids per network. The preimage is: /// /// ```text - /// root_public_key.serialize() || root_chain_code || DOMAIN_TAG || network_byte + /// root_public_key.to_compressed() || root_chain_code || DOMAIN_TAG || network_byte /// ``` /// /// where `DOMAIN_TAG` is the private `NETWORK_SCOPED_WALLET_ID_DOMAIN` @@ -113,7 +113,8 @@ impl Wallet { /// `Network as u8`). The tag guarantees a `Some(_)` digest can never collide /// with the `None` digest. /// * `None` → a **network-independent id**. The preimage is exactly - /// `root_public_key.serialize() || root_chain_code` (no tag, no discriminant) + /// `root_public_key.to_compressed() || root_chain_code` (no tag, no + /// discriminant) /// — useful when a caller deliberately wants one id shared across networks. /// /// Callers comparing a `Some(network)` id against a `None` id (or against a @@ -124,7 +125,7 @@ impl Wallet { network: Option, ) -> [u8; 32] { let mut data = Vec::new(); - data.extend_from_slice(&root_pub_key.root_public_key.serialize()); + data.extend_from_slice(&root_pub_key.root_public_key.to_compressed()); data.extend_from_slice(&root_pub_key.root_chain_code[..]); // A concrete network appends the domain tag + discriminant byte; with no // network the preimage stops here, giving a network-independent digest. diff --git a/key-wallet/src/wallet/root_extended_keys.rs b/key-wallet/src/wallet/root_extended_keys.rs index 620ad7dac..cb54c1914 100644 --- a/key-wallet/src/wallet/root_extended_keys.rs +++ b/key-wallet/src/wallet/root_extended_keys.rs @@ -9,6 +9,8 @@ use crate::{Error, Network, Wallet}; use bincode::{BorrowDecode, Decode, Encode}; #[cfg(feature = "bls")] use dashcore::bls_sig_utils::BlsSkBytes; +use dashcore::ecdsa::{EcdsaPublicKey, EcdsaSecretKey}; +use hex_conservative::hex; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use zeroize::Zeroize; @@ -16,13 +18,13 @@ use zeroize::Zeroize; #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct RootExtendedPrivKey { - pub root_private_key: secp256k1::SecretKey, + pub root_private_key: EcdsaSecretKey, pub root_chain_code: ChainCode, } impl Zeroize for RootExtendedPrivKey { fn zeroize(&mut self) { - self.root_private_key.non_secure_erase(); + self.root_private_key.zeroize(); self.root_chain_code.zeroize(); } } @@ -35,7 +37,7 @@ impl Drop for RootExtendedPrivKey { impl RootExtendedPrivKey { /// Create a new RootExtendedPrivKey - pub fn new(root_private_key: secp256k1::SecretKey, root_chain_code: ChainCode) -> Self { + pub fn new(root_private_key: EcdsaSecretKey, root_chain_code: ChainCode) -> Self { Self { root_private_key, root_chain_code, @@ -52,7 +54,7 @@ impl RootExtendedPrivKey { /// Create from an ExtendedPrivKey (must be depth 0) pub fn from_extended_priv_key(key: &ExtendedPrivKey) -> Self { Self { - root_private_key: key.private_key, + root_private_key: key.private_key.clone(), root_chain_code: key.chain_code, } } @@ -64,7 +66,7 @@ impl RootExtendedPrivKey { depth: 0, parent_fingerprint: Default::default(), child_number: ChildNumber::from(0), - private_key: self.root_private_key, + private_key: self.root_private_key.clone(), chain_code: self.root_chain_code, } } @@ -77,7 +79,7 @@ impl RootExtendedPrivKey { // Convert secp256k1 private key bytes to BLS private key // The scalar is read little-endian from the secp secret, the bag holds // big-endian, so the bytes are reversed going in. - let mut scalar_bytes = self.root_private_key.to_secret_bytes(); + let mut scalar_bytes = self.root_private_key.to_bytes(); scalar_bytes.reverse(); ExtendedBLSPrivKey::from_parts( @@ -85,7 +87,7 @@ impl RootExtendedPrivKey { 0, Default::default(), ChildNumber::from(0), - BlsSkBytes::from_bytes(scalar_bytes), + BlsSkBytes::from_bytes(*scalar_bytes), self.root_chain_code, ) .map_err(|_| { @@ -105,21 +107,21 @@ impl RootExtendedPrivKey { // Convert secp256k1 private key bytes to Ed25519 seed // Ed25519 uses 32-byte seeds to generate keys - let seed_bytes = self.root_private_key.to_secret_bytes(); + let seed_bytes = self.root_private_key.to_bytes(); // Create Ed25519 extended private key from seed using new_master - let eddsa_key = ExtendedEd25519PrivKey::new_master(network, &seed_bytes).map_err(|e| { - Error::InvalidParameter(format!("Failed to convert to EdDSA key: {:?}", e)) - })?; + let eddsa_key = + ExtendedEd25519PrivKey::new_master(network, &seed_bytes[..]).map_err(|e| { + Error::InvalidParameter(format!("Failed to convert to EdDSA key: {:?}", e)) + })?; Ok(eddsa_key) } /// Get the corresponding public key pub fn to_root_extended_pub_key(&self) -> RootExtendedPubKey { - let public_key = secp256k1::PublicKey::from_secret_key(&self.root_private_key); RootExtendedPubKey { - root_public_key: public_key, + root_public_key: self.root_private_key.public_key(), root_chain_code: self.root_chain_code, } } @@ -132,8 +134,8 @@ impl Encode for RootExtendedPrivKey { encoder: &mut E, ) -> Result<(), bincode::error::EncodeError> { // Encode the private key as 32 bytes - let private_key_bytes = self.root_private_key.to_secret_bytes(); - bincode::Encode::encode(&private_key_bytes, encoder)?; + let private_key_bytes = self.root_private_key.to_bytes(); + bincode::Encode::encode(&*private_key_bytes, encoder)?; // Encode the chain code bincode::Encode::encode(&self.root_chain_code, encoder)?; @@ -149,10 +151,9 @@ impl Decode for RootExtendedPrivKey { ) -> Result { // Decode the private key bytes let private_key_bytes: [u8; 32] = bincode::Decode::decode(decoder)?; - let root_private_key = - secp256k1::SecretKey::from_secret_bytes(private_key_bytes).map_err(|e| { - bincode::error::DecodeError::OtherString(format!("Invalid private key: {}", e)) - })?; + let root_private_key = EcdsaSecretKey::from_bytes(&private_key_bytes).map_err(|e| { + bincode::error::DecodeError::OtherString(format!("Invalid private key: {}", e)) + })?; // Decode the chain code let root_chain_code: ChainCode = bincode::Decode::decode(decoder)?; @@ -169,8 +170,7 @@ impl<'de, C> BorrowDecode<'de, C> for RootExtendedPrivKey { fn borrow_decode>( decoder: &mut D, ) -> Result { - // For borrowed decode, we still need to copy the data since secp256k1::SecretKey - // doesn't support borrowing from the decoder + // `EcdsaSecretKey` cannot borrow from the decoder, so decode a copy. >::decode(decoder) } } @@ -202,7 +202,7 @@ impl FromOnNetwork for ExtendedPrivKey { depth: 0, parent_fingerprint: Default::default(), child_number: ChildNumber::from(0), - private_key: value.root_private_key, + private_key: value.root_private_key.clone(), chain_code: value.root_chain_code, } } @@ -211,7 +211,7 @@ impl FromOnNetwork for ExtendedPrivKey { #[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct RootExtendedPubKey { - pub root_public_key: secp256k1::PublicKey, + pub root_public_key: EcdsaPublicKey, pub root_chain_code: ChainCode, } @@ -219,11 +219,9 @@ impl zeroize::Zeroize for RootExtendedPubKey { fn zeroize(&mut self) { // Replace the public key with a dummy value (generator point G) // This is a best-effort zeroization since PublicKey doesn't implement Zeroize - self.root_public_key = secp256k1::PublicKey::from_byte_array_compressed([ - 0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, - 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81, - 0x5b, 0x16, 0xf8, 0x17, 0x98, - ]) + self.root_public_key = EcdsaPublicKey::from_bytes(&hex!( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798" + )) .expect("hardcoded generator point should be valid"); // Zeroize the chain code @@ -233,7 +231,7 @@ impl zeroize::Zeroize for RootExtendedPubKey { impl RootExtendedPubKey { /// Create a new RootExtendedPubKey - pub fn new(root_public_key: secp256k1::PublicKey, root_chain_code: ChainCode) -> Self { + pub fn new(root_public_key: EcdsaPublicKey, root_chain_code: ChainCode) -> Self { Self { root_public_key, root_chain_code, @@ -268,7 +266,7 @@ impl Encode for RootExtendedPubKey { encoder: &mut E, ) -> Result<(), bincode::error::EncodeError> { // Encode the public key as serialized bytes (33 bytes compressed) - let public_key_bytes = self.root_public_key.serialize(); + let public_key_bytes = self.root_public_key.to_compressed(); bincode::Encode::encode(&public_key_bytes, encoder)?; // Encode the chain code @@ -285,10 +283,9 @@ impl Decode for RootExtendedPubKey { ) -> Result { // Decode the public key bytes let public_key_bytes: [u8; 33] = bincode::Decode::decode(decoder)?; - let root_public_key = secp256k1::PublicKey::from_byte_array_compressed(public_key_bytes) - .map_err(|e| { - bincode::error::DecodeError::OtherString(format!("Invalid public key: {}", e)) - })?; + let root_public_key = EcdsaPublicKey::from_bytes(&public_key_bytes).map_err(|e| { + bincode::error::DecodeError::OtherString(format!("Invalid public key: {}", e)) + })?; // Decode the chain code let root_chain_code: ChainCode = bincode::Decode::decode(decoder)?; @@ -305,8 +302,7 @@ impl<'de, C> BorrowDecode<'de, C> for RootExtendedPubKey { fn borrow_decode>( decoder: &mut D, ) -> Result { - // For borrowed decode, we still need to copy the data since secp256k1::PublicKey - // doesn't support borrowing from the decoder + // `EcdsaPublicKey` cannot borrow from the decoder, so decode a copy. >::decode(decoder) } } @@ -397,3 +393,122 @@ impl Wallet { } } } + +#[cfg(all(test, feature = "serde"))] +mod tests { + use super::*; + use hex_conservative::{hex, DisplayHex}; + + /// BIP32 test vector 1. + const SEED: [u8; 16] = hex!("000102030405060708090a0b0c0d0e0f"); + const SK_HEX: &str = "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"; + const PK_HEX: &str = "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"; + const PK_UNCOMPRESSED_HEX: &str = "0439a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2\ + 3cbe7ded0e7ce6a594896b8f62888fdbc5c8821305e2ea42bf01e37300116281"; + const CHAIN_CODE_HEX: &str = "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508"; + + /// Both root keys serialize as hex in human-readable formats and as bare + /// fixed-size tuples otherwise. The public key is in compressed form. + #[test] + fn serde_layout_of_root_keys() { + let sk = RootExtendedPrivKey::new_master(&SEED).expect("master key"); + let pk = sk.to_root_extended_pub_key(); + + let sk_json = serde_json::to_string(&sk).expect("serialize private key"); + assert_eq!( + sk_json, + format!( + "{{\"root_private_key\":\"{}\",\"root_chain_code\":\"{}\"}}", + SK_HEX, CHAIN_CODE_HEX + ) + ); + let pk_json = serde_json::to_string(&pk).expect("serialize public key"); + assert_eq!( + pk_json, + format!( + "{{\"root_public_key\":\"{}\",\"root_chain_code\":\"{}\"}}", + PK_HEX, CHAIN_CODE_HEX + ) + ); + + let config = bincode::config::standard(); + let chain_code = format!("40{}", CHAIN_CODE_HEX.as_bytes().to_lower_hex_string()); + let sk_bin = bincode::serde::encode_to_vec(&sk, config).expect("encode private key"); + assert_eq!(sk_bin.to_lower_hex_string(), format!("{}{}", SK_HEX, chain_code)); + let pk_bin = bincode::serde::encode_to_vec(&pk, config).expect("encode public key"); + assert_eq!(pk_bin.to_lower_hex_string(), format!("{}{}", PK_HEX, chain_code)); + + let sk_back: RootExtendedPrivKey = + serde_json::from_str(&sk_json).expect("parse private key"); + assert_eq!(sk_back.root_private_key, sk.root_private_key); + let (sk_back, _): (RootExtendedPrivKey, _) = + bincode::serde::decode_from_slice(&sk_bin, config).expect("decode private key"); + assert_eq!(sk_back.root_private_key, sk.root_private_key); + let pk_back: RootExtendedPubKey = serde_json::from_str(&pk_json).expect("parse public key"); + assert_eq!(pk_back.root_public_key, pk.root_public_key); + let (pk_back, _): (RootExtendedPubKey, _) = + bincode::serde::decode_from_slice(&pk_bin, config).expect("decode public key"); + assert_eq!(pk_back.root_public_key, pk.root_public_key); + } + + /// Both root keys take hex in either case, and the public key also takes + /// its uncompressed encoding. Malformed or invalid keys are rejected in + /// both formats. + #[test] + fn serde_input_of_root_keys() { + let sk = RootExtendedPrivKey::new_master(&SEED).expect("master key"); + let pk = sk.to_root_extended_pub_key(); + let pk_json = |key: &str| { + format!( + "{{\"root_public_key\":\"{}\",\"root_chain_code\":\"{}\"}}", + key, CHAIN_CODE_HEX + ) + }; + let sk_json = |key: &str| { + format!( + "{{\"root_private_key\":\"{}\",\"root_chain_code\":\"{}\"}}", + key, CHAIN_CODE_HEX + ) + }; + + for key in [PK_UNCOMPRESSED_HEX.to_string(), PK_HEX.to_uppercase()] { + let back: RootExtendedPubKey = serde_json::from_str(&pk_json(&key)).expect("parse"); + assert_eq!(back.root_public_key, pk.root_public_key); + assert_eq!(serde_json::to_string(&back).expect("serialize"), pk_json(PK_HEX)); + } + let bad_point = format!("05{}", &PK_HEX[2..]); + let prefixed = format!("0x{}", PK_HEX); + for key in [&PK_HEX[..64], &bad_point, &prefixed] { + assert!(serde_json::from_str::(&pk_json(key)).is_err(), "{}", key); + } + + let back: RootExtendedPrivKey = + serde_json::from_str(&sk_json(&SK_HEX.to_uppercase())).expect("parse"); + assert_eq!(back.root_private_key, sk.root_private_key); + let long = format!("{}00", SK_HEX); + let zero = "00".repeat(32); + let order = "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"; + for key in [&SK_HEX[..62], &long, &zero, order] { + assert!(serde_json::from_str::(&sk_json(key)).is_err(), "{}", key); + } + + let config = bincode::config::standard(); + let chain_code = format!("40{}", CHAIN_CODE_HEX.as_bytes().to_lower_hex_string()); + let decode = |hex: &str| hex_conservative::decode_to_vec(hex).expect("decode hex"); + let wrong_prefix = decode(&format!("04{}{}", &PK_HEX[2..], chain_code)); + assert!(bincode::serde::decode_from_slice::(&wrong_prefix, config) + .is_err()); + assert!(bincode::serde::decode_from_slice::( + &decode(&PK_HEX[..64]), + config + ) + .is_err()); + let zero = decode(&format!("{}{}", zero, chain_code)); + assert!(bincode::serde::decode_from_slice::(&zero, config).is_err()); + assert!(bincode::serde::decode_from_slice::( + &decode(&SK_HEX[..62]), + config + ) + .is_err()); + } +} diff --git a/key-wallet/tests/address_tests.rs b/key-wallet/tests/address_tests.rs index f90187b96..768f5f156 100644 --- a/key-wallet/tests/address_tests.rs +++ b/key-wallet/tests/address_tests.rs @@ -1,14 +1,14 @@ //! Address tests use core::str::FromStr; +use dashcore::ecdsa::EcdsaSecretKey; use dashcore::{Address, AddressType, Network as DashNetwork, ScriptBuf}; -use secp256k1::PublicKey; #[test] fn test_p2pkh_address_creation() { // Create a public key - let secret_key = secp256k1::SecretKey::from_secret_bytes([1u8; 32]).unwrap(); - let public_key = PublicKey::from_secret_key(&secret_key); + let secret_key = EcdsaSecretKey::from_bytes(&[1u8; 32]).unwrap(); + let public_key = secret_key.public_key(); let dash_pubkey = dashcore::PublicKey::new(public_key); // Create P2PKH address @@ -42,8 +42,8 @@ fn test_p2sh_address_creation() { #[test] fn test_testnet_address() { // Create a public key - let secret_key = secp256k1::SecretKey::from_secret_bytes([2u8; 32]).unwrap(); - let public_key = PublicKey::from_secret_key(&secret_key); + let secret_key = EcdsaSecretKey::from_bytes(&[2u8; 32]).unwrap(); + let public_key = secret_key.public_key(); let dash_pubkey = dashcore::PublicKey::new(public_key); // Create testnet P2PKH address @@ -63,10 +63,8 @@ fn test_address_parsing() { use dashcore::key::PrivateKey; // Create a mainnet address - let privkey_mainnet = PrivateKey::new( - dashcore::secp256k1::SecretKey::from_secret_bytes([0x01; 32]).unwrap(), - DashNetwork::Mainnet, - ); + let privkey_mainnet = + PrivateKey::new(EcdsaSecretKey::from_bytes(&[0x01; 32]).unwrap(), DashNetwork::Mainnet); let pubkey_mainnet = privkey_mainnet.public_key(); let mainnet_address = Address::p2pkh(&pubkey_mainnet, DashNetwork::Mainnet); @@ -81,10 +79,8 @@ fn test_address_parsing() { assert_eq!(checked_mainnet.address_type(), Some(AddressType::P2pkh)); // Create a testnet address - let privkey_testnet = PrivateKey::new( - dashcore::secp256k1::SecretKey::from_secret_bytes([0x02; 32]).unwrap(), - DashNetwork::Testnet, - ); + let privkey_testnet = + PrivateKey::new(EcdsaSecretKey::from_bytes(&[0x02; 32]).unwrap(), DashNetwork::Testnet); let pubkey_testnet = privkey_testnet.public_key(); let testnet_address = Address::p2pkh(&pubkey_testnet, DashNetwork::Testnet); @@ -102,8 +98,8 @@ fn test_address_parsing() { #[test] fn test_address_roundtrip() { // Create a public key - let secret_key = secp256k1::SecretKey::from_secret_bytes([3u8; 32]).unwrap(); - let public_key = PublicKey::from_secret_key(&secret_key); + let secret_key = EcdsaSecretKey::from_bytes(&[3u8; 32]).unwrap(); + let public_key = secret_key.public_key(); let dash_pubkey = dashcore::PublicKey::new(public_key); // Create address diff --git a/key-wallet/tests/derivation_tests.rs b/key-wallet/tests/derivation_tests.rs index 31b6045a5..abc08ad00 100644 --- a/key-wallet/tests/derivation_tests.rs +++ b/key-wallet/tests/derivation_tests.rs @@ -43,7 +43,7 @@ fn test_dip17_platform_payment_vector1_mainnet() { let xprv = master_key.derive_priv(&path).unwrap(); // Verify private key matches DIP-17 test vector - let privkey_hex = xprv.private_key.to_secret_bytes().to_lower_hex_string(); + let privkey_hex = xprv.private_key.to_bytes().to_lower_hex_string(); assert_eq!( privkey_hex, "6bca392f43453b7bc33a9532b69221ce74906a8815281637e0c9d0bee35361fe", "Private key mismatch for DIP-17 vector 1" @@ -115,7 +115,7 @@ fn test_dip17_platform_payment_vector2() { let xprv_mainnet = master_mainnet.derive_priv(&path_mainnet).unwrap(); // Verify private key - let privkey_hex = xprv_mainnet.private_key.to_secret_bytes().to_lower_hex_string(); + let privkey_hex = xprv_mainnet.private_key.to_bytes().to_lower_hex_string(); assert_eq!( privkey_hex, "eef58ce73383f63d5062f281ed0c1e192693c170fbc0049662a73e48a1981523", "Private key mismatch for DIP-17 vector 2" @@ -173,7 +173,7 @@ fn test_dip17_platform_payment_vector3_non_default_key_class() { let xprv_mainnet = master_mainnet.derive_priv(&path_mainnet).unwrap(); // Verify private key - let privkey_hex = xprv_mainnet.private_key.to_secret_bytes().to_lower_hex_string(); + let privkey_hex = xprv_mainnet.private_key.to_bytes().to_lower_hex_string(); assert_eq!( privkey_hex, "cc05b4389712a2e724566914c256217685d781503d7cc05af6642e60260830db", "Private key mismatch for DIP-17 vector 3" diff --git a/rpc-client/src/client.rs b/rpc-client/src/client.rs index 9fb9259a0..2b1b6e555 100644 --- a/rpc-client/src/client.rs +++ b/rpc-client/src/client.rs @@ -25,8 +25,8 @@ use crate::dashcore::{ScriptBuf, block, consensus}; use crate::error::*; use crate::json; use crate::queryable; +use dashcore::ecdsa::EcdsaSignature; use dashcore::hashes::Hash; -use dashcore::secp256k1::ecdsa::Signature; use dashcore::{ Address, Amount, Block, OutPoint, PrivateKey, ProTxHash, PublicKey, QuorumHash, Transaction, }; @@ -797,7 +797,7 @@ pub trait RpcApi: Sized { fn verify_message( &self, address: &Address, - signature: &Signature, + signature: &EcdsaSignature, message: &str, ) -> Result { let args = [address.to_string().into(), signature.to_string().into(), into_json(message)?]; diff --git a/rpc-integration-test/src/main.rs b/rpc-integration-test/src/main.rs index ed0f3e1aa..bc33bc252 100644 --- a/rpc-integration-test/src/main.rs +++ b/rpc-integration-test/src/main.rs @@ -25,8 +25,8 @@ use dashcore_rpc::{ Address, AddressType, Amount, EcdsaSighashType, Network, OutPoint, PrivateKey, SignedAmount, Transaction, TxIn, TxOut, Txid, Witness, consensus::encode::{deserialize, serialize}, + ecdsa::EcdsaSecretKey, hashes::Hash, - secp256k1, }, }; @@ -711,8 +711,7 @@ fn test_get_block_filter(cl: &Client) { } fn test_sign_raw_transaction_with_send_raw_transaction(cl: &Client) { - let sk = - PrivateKey::new(secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), Network::Regtest); + let sk = PrivateKey::new(EcdsaSecretKey::generate(), Network::Regtest); let addr = Address::p2pkh(&sk.public_key(), Network::Regtest); let options = json::ListUnspentQueryOptions { @@ -1026,24 +1025,21 @@ fn test_list_received_by_address(cl: &Client) { } fn test_import_public_key(cl: &Client) { - let sk = - PrivateKey::new(secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), Network::Regtest); + let sk = PrivateKey::new(EcdsaSecretKey::generate(), Network::Regtest); cl.import_public_key(&sk.public_key(), None, None).unwrap(); cl.import_public_key(&sk.public_key(), Some("l"), None).unwrap(); cl.import_public_key(&sk.public_key(), None, Some(false)).unwrap(); } fn test_import_priv_key(cl: &Client) { - let sk = - PrivateKey::new(secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), Network::Regtest); + let sk = PrivateKey::new(EcdsaSecretKey::generate(), Network::Regtest); cl.import_private_key(&sk, None, None).unwrap(); cl.import_private_key(&sk, Some("l"), None).unwrap(); cl.import_private_key(&sk, None, Some(false)).unwrap(); } fn test_import_address(cl: &Client) { - let sk = - PrivateKey::new(secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), Network::Regtest); + let sk = PrivateKey::new(EcdsaSecretKey::generate(), Network::Regtest); let addr = Address::p2pkh(&sk.public_key(), Network::Regtest); cl.import_address(&addr, None, None).unwrap(); cl.import_address(&addr, Some("l"), None).unwrap(); @@ -1051,8 +1047,7 @@ fn test_import_address(cl: &Client) { } fn test_import_address_script(cl: &Client) { - let sk = - PrivateKey::new(secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), Network::Regtest); + let sk = PrivateKey::new(EcdsaSecretKey::generate(), Network::Regtest); let addr = Address::p2pkh(&sk.public_key(), Network::Regtest); cl.import_address_script(&addr.script_pubkey(), None, None, None).unwrap(); cl.import_address_script(&addr.script_pubkey(), Some("l"), None, None).unwrap(); diff --git a/rpc-json/Cargo.toml b/rpc-json/Cargo.toml index 0b568a997..9ccacc6be 100644 --- a/rpc-json/Cargo.toml +++ b/rpc-json/Cargo.toml @@ -26,6 +26,6 @@ serde_repr = "0.1" hex-conservative = { workspace = true } key-wallet = { path = "../key-wallet", default-features = false, features=["serde", "getrandom"] } -dashcore = { path = "../dash", features=["secp-recovery", "rand-std", "signer", "serde"] } +dashcore = { path = "../dash", features=["rand-std", "signer", "serde"] } bincode = { workspace = true, features = ["serde"] }