From 385163cf769b56901ac616e976e58a0d0e0da20e Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 5 Oct 2026 03:11:28 +0530 Subject: [PATCH 01/12] fix(crypto): reject WIF keys where compression flag isn't `0x01` --- crypto/src/key.rs | 12 +++++++++++- dash/src/crypto/key.rs | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/crypto/src/key.rs b/crypto/src/key.rs index 0b601e3be..7805f7439 100644 --- a/crypto/src/key.rs +++ b/crypto/src/key.rs @@ -39,6 +39,9 @@ pub enum Error { InvalidAddressVersion(u8), /// The base58 decoded correctly but the payload was the wrong length. InvalidBase58PayloadLength(usize), + /// A 34-byte WIF payload ended in something other than the `0x01` + /// compression flag. + InvalidWifCompressionFlag(u8), /// Hex decoding error Hex(hex_conservative::DecodeFixedLengthBytesError), /// `PublicKey` hex should be 66 or 130 digits long. @@ -59,6 +62,9 @@ impl fmt::Display for Error { 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) @@ -81,6 +87,7 @@ impl std::error::Error for Error { InvalidAddressVersion(_) | InvalidBase58PayloadLength(_) | InvalidKeyPrefix(_) + | InvalidWifCompressionFlag(_) | InvalidHexLength(_) => None, NotSupported(_) => None, } @@ -413,9 +420,12 @@ impl PrivateKey { pub fn from_wif(wif: &str) -> Result { let data = base58::decode_check(wif)?; + // Core's `DecodeSecret` takes a 34th byte as the compression flag + // only when it is exactly 0x01. let compressed = match data.len() { 33 => false, - 34 => true, + 34 if data[33] == 1 => true, + 34 => return Err(Error::InvalidWifCompressionFlag(data[33])), _ => { return Err(Error::InvalidBase58PayloadLength(data.len())); } diff --git a/dash/src/crypto/key.rs b/dash/src/crypto/key.rs index b065b2ade..a20fbc2d5 100644 --- a/dash/src/crypto/key.rs +++ b/dash/src/crypto/key.rs @@ -19,6 +19,20 @@ mod tests { use crate::crypto::key::{PrivateKey, PublicKey}; use crate::io; + /// Core's `DecodeSecret` takes a 34-byte payload only when it ends in + /// the 0x01 compression flag. + #[test_case::test_matrix([0x00, 0x02, 0xff])] + fn wif_compression_flag_must_be_one(flag: u8) { + let mut data = + crate::base58::decode_check("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy") + .unwrap(); + data[33] = flag; + assert_eq!( + PrivateKey::from_wif(crate::base58::Base58CkString::encode_unbounded(&data).as_str()), + Err(crate::crypto::key::Error::InvalidWifCompressionFlag(flag)) + ); + } + #[test] fn test_key_derivation() { // testnet compressed From d61993309646a7df0f81fc1104a2622ace19a46f Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:57:41 +0530 Subject: [PATCH 02/12] refactor!: drop unused key definitions --- crypto/src/key.rs | 246 +--------------------------------- dash/src/blockdata/witness.rs | 33 ----- dash/src/crypto/key.rs | 50 ------- key-wallet/src/bip32.rs | 5 +- rpc-client/src/error.rs | 10 -- rpc-json/src/lib.rs | 2 +- 6 files changed, 3 insertions(+), 343 deletions(-) diff --git a/crypto/src/key.rs b/crypto/src/key.rs index 7805f7439..bb0b2a410 100644 --- a/crypto/src/key.rs +++ b/crypto/src/key.rs @@ -46,8 +46,6 @@ pub enum Error { Hex(hex_conservative::DecodeFixedLengthBytesError), /// `PublicKey` hex should be 66 or 130 digits long. InvalidHexLength(usize), - /// Something is not supported based on active features - NotSupported(String), } impl fmt::Display for Error { @@ -69,9 +67,6 @@ impl fmt::Display for Error { Error::InvalidHexLength(got) => { write!(f, "PublicKey hex should be 66 or 130 digits long, got: {}", got) } - Error::NotSupported(string) => { - write!(f, "{}", string.as_str()) - } } } } @@ -89,7 +84,6 @@ impl std::error::Error for Error { | InvalidKeyPrefix(_) | InvalidWifCompressionFlag(_) | InvalidHexLength(_) => None, - NotSupported(_) => None, } } } @@ -116,7 +110,7 @@ impl From for Error { } /// A Dash ECDSA public key -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct PublicKey { /// Whether this public key should be serialized as compressed pub compressed: bool, @@ -155,27 +149,6 @@ impl PublicKey { self.with_serialized(|bytes| writer.write_all(bytes)) } - /// Read the public key from a reader - /// - /// This internally reads the first byte before reading the rest, so - /// use of a `BufReader` is recommended. - pub fn read_from(mut reader: R) -> Result { - let mut bytes = [0; 65]; - - reader.read_exact(&mut bytes[0..1])?; - let bytes = if bytes[0] < 4 { - &mut bytes[..33] - } else { - &mut bytes[..65] - }; - - reader.read_exact(&mut bytes[1..])?; - Self::from_slice(bytes).map_err(|e| { - let reason = e; - io::Error::new(io::ErrorKind::InvalidData, reason) - }) - } - /// Serialize the public key to bytes pub fn to_bytes(self) -> Vec { let mut buf = Vec::new(); @@ -183,72 +156,6 @@ impl PublicKey { buf } - /// Serialize the public key into a `SortKey`. - /// - /// `SortKey` is not too useful by itself, but it can be used to sort a - /// `[PublicKey]` slice using `sort_unstable_by_key`, `sort_by_cached_key`, - /// `sort_by_key`, or any of the other `*_by_key` methods on slice. - /// Pass the method into the sort method directly. (i.e. `PublicKey::to_sort_key`) - /// - /// This method of sorting is in line with Dash Core's implementation of - /// sorting keys for output descriptors such as `sortedmulti()`. - /// - /// If every `PublicKey` in the slice is `compressed == true` then this will sort - /// the keys in a - /// [BIP67](https://github.com/bitcoin/bips/blob/master/bip-0067.mediawiki) - /// compliant way. - /// - /// # Example: Using with `sort_unstable_by_key` - /// - /// ```rust - /// use std::str::FromStr; - /// use dashcore_crypto::key::PublicKey; - /// - /// let pk = |s| PublicKey::from_str(s).unwrap(); - /// - /// let mut unsorted = [ - /// pk("04c4b0bbb339aa236bff38dbe6a451e111972a7909a126bc424013cba2ec33bc38e98ac269ffe028345c31ac8d0a365f29c8f7e7cfccac72f84e1acd02bc554f35"), - /// pk("038f47dcd43ba6d97fc9ed2e3bba09b175a45fac55f0683e8cf771e8ced4572354"), - /// pk("028bde91b10013e08949a318018fedbd896534a549a278e220169ee2a36517c7aa"), - /// pk("04c4b0bbb339aa236bff38dbe6a451e111972a7909a126bc424013cba2ec33bc3816753d96001fd7cba3ce5372f5c9a0d63708183033538d07b1e532fc43aaacfa"), - /// pk("032b8324c93575034047a52e9bca05a46d8347046b91a032eff07d5de8d3f2730b"), - /// pk("045d753414fa292ea5b8f56e39cfb6a0287b2546231a5cb05c4b14ab4b463d171f5128148985b23eccb1e2905374873b1f09b9487f47afa6b1f2b0083ac8b4f7e8"), - /// pk("0234dd69c56c36a41230d573d68adeae0030c9bc0bf26f24d3e1b64c604d293c68"), - /// ]; - /// let sorted = [ - /// // These first 4 keys are in a BIP67 compatible sorted order - /// // (since they are compressed) - /// pk("0234dd69c56c36a41230d573d68adeae0030c9bc0bf26f24d3e1b64c604d293c68"), - /// pk("028bde91b10013e08949a318018fedbd896534a549a278e220169ee2a36517c7aa"), - /// pk("032b8324c93575034047a52e9bca05a46d8347046b91a032eff07d5de8d3f2730b"), - /// pk("038f47dcd43ba6d97fc9ed2e3bba09b175a45fac55f0683e8cf771e8ced4572354"), - /// // Uncompressed keys are not BIP67 compliant, but are sorted - /// // after compressed keys in Dash Core using `sortedmulti()` - /// pk("045d753414fa292ea5b8f56e39cfb6a0287b2546231a5cb05c4b14ab4b463d171f5128148985b23eccb1e2905374873b1f09b9487f47afa6b1f2b0083ac8b4f7e8"), - /// pk("04c4b0bbb339aa236bff38dbe6a451e111972a7909a126bc424013cba2ec33bc3816753d96001fd7cba3ce5372f5c9a0d63708183033538d07b1e532fc43aaacfa"), - /// pk("04c4b0bbb339aa236bff38dbe6a451e111972a7909a126bc424013cba2ec33bc38e98ac269ffe028345c31ac8d0a365f29c8f7e7cfccac72f84e1acd02bc554f35"), - /// ]; - /// - /// unsorted.sort_unstable_by_key(|k| PublicKey::to_sort_key(*k)); - /// - /// assert_eq!(unsorted, sorted); - /// ``` - pub fn to_sort_key(self) -> SortKey { - if self.compressed { - let bytes = self.inner.serialize(); - let mut res = [0; 32]; - res[..].copy_from_slice(&bytes[1..33]); - SortKey(bytes[0], res, [0; 32]) - } else { - let bytes = self.inner.serialize_uncompressed(); - let mut res_left = [0; 32]; - let mut res_right = [0; 32]; - res_left[..].copy_from_slice(&bytes[1..33]); - res_right[..].copy_from_slice(&bytes[33..65]); - SortKey(bytes[0], res_left, res_right) - } - } - /// Deserialize a public key from a slice pub fn from_slice(data: &[u8]) -> Result { let (compressed, inner) = match data.len() { @@ -313,10 +220,6 @@ impl From for PubkeyHash { } } -/// An opaque return type for PublicKey::to_sort_key -#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] -pub struct SortKey(u8, [u8; 32], [u8; 32]); - impl fmt::Display for PublicKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.with_serialized(|bytes| write!(f, "{:x}", bytes.as_hex())) @@ -700,150 +603,3 @@ impl From for TweakedPublicKey { TweakedPublicKey::from_keypair(pair) } } - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn pubkey_to_sort_key() { - let key1 = PublicKey::from_str( - "02ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8", - ) - .unwrap(); - let key2 = PublicKey { - inner: key1.inner, - compressed: false, - }; - let expected1 = SortKey( - 2, - hex_conservative::decode_to_array::<32>( - "ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8", - ) - .unwrap(), - [0_u8; 32], - ); - let expected2 = SortKey( - 4, - hex_conservative::decode_to_array::<32>( - "ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8", - ) - .unwrap(), - hex_conservative::decode_to_array::<32>( - "1794e7f3d5e420641a3bc690067df5541470c966cbca8c694bf39aa16d836918", - ) - .unwrap(), - ); - assert_eq!(key1.to_sort_key(), expected1); - assert_eq!(key2.to_sort_key(), expected2); - } - #[test] - fn pubkey_sort() { - struct Vector { - input: Vec, - expect: Vec, - } - let fmt = - |v: Vec<_>| v.into_iter().map(|s| PublicKey::from_str(s).unwrap()).collect::>(); - let vectors = vec![ - // Start BIP67 vectors - // Vector 1 - Vector { - input: fmt(vec![ - "02ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8", - "02fe6f0a5a297eb38c391581c4413e084773ea23954d93f7753db7dc0adc188b2f", - ]), - expect: fmt(vec![ - "02fe6f0a5a297eb38c391581c4413e084773ea23954d93f7753db7dc0adc188b2f", - "02ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8", - ]), - }, - // Vector 2 (Already sorted, no action required) - Vector { - input: fmt(vec![ - "02632b12f4ac5b1d1b72b2a3b508c19172de44f6f46bcee50ba33f3f9291e47ed0", - "027735a29bae7780a9755fae7a1c4374c656ac6a69ea9f3697fda61bb99a4f3e77", - "02e2cc6bd5f45edd43bebe7cb9b675f0ce9ed3efe613b177588290ad188d11b404", - ]), - expect: fmt(vec![ - "02632b12f4ac5b1d1b72b2a3b508c19172de44f6f46bcee50ba33f3f9291e47ed0", - "027735a29bae7780a9755fae7a1c4374c656ac6a69ea9f3697fda61bb99a4f3e77", - "02e2cc6bd5f45edd43bebe7cb9b675f0ce9ed3efe613b177588290ad188d11b404", - ]), - }, - // Vector 3 - Vector { - input: fmt(vec![ - "030000000000000000000000000000000000004141414141414141414141414141", - "020000000000000000000000000000000000004141414141414141414141414141", - "020000000000000000000000000000000000004141414141414141414141414140", - "030000000000000000000000000000000000004141414141414141414141414140", - ]), - expect: fmt(vec![ - "020000000000000000000000000000000000004141414141414141414141414140", - "020000000000000000000000000000000000004141414141414141414141414141", - "030000000000000000000000000000000000004141414141414141414141414140", - "030000000000000000000000000000000000004141414141414141414141414141", - ]), - }, - // Vector 4: (from bitcore) - Vector { - input: fmt(vec![ - "022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da", - "03e3818b65bcc73a7d64064106a859cc1a5a728c4345ff0b641209fba0d90de6e9", - "021f2f6e1e50cb6a953935c3601284925decd3fd21bc445712576873fb8c6ebc18", - ]), - expect: fmt(vec![ - "021f2f6e1e50cb6a953935c3601284925decd3fd21bc445712576873fb8c6ebc18", - "022df8750480ad5b26950b25c7ba79d3e37d75f640f8e5d9bcd5b150a0f85014da", - "03e3818b65bcc73a7d64064106a859cc1a5a728c4345ff0b641209fba0d90de6e9", - ]), - }, - // Non-BIP67 vectors - Vector { - input: fmt(vec![ - "02c690d642c1310f3a1ababad94e3930e4023c930ea472e7f37f660fe485263b88", - "0234dd69c56c36a41230d573d68adeae0030c9bc0bf26f24d3e1b64c604d293c68", - "041a181bd0e79974bd7ca552e09fc42ba9c3d5dbb3753741d6f0ab3015dbfd9a22d6b001a32f5f51ac6f2c0f35e73a6a62f59e848fa854d3d21f3f231594eeaa46", - "032b8324c93575034047a52e9bca05a46d8347046b91a032eff07d5de8d3f2730b", - "04c4b0bbb339aa236bff38dbe6a451e111972a7909a126bc424013cba2ec33bc3816753d96001fd7cba3ce5372f5c9a0d63708183033538d07b1e532fc43aaacfa", - "028e1c947c8c0b8ed021088b8e981491ac7af2b8fabebea1abdb448424c8ed75b7", - "045d753414fa292ea5b8f56e39cfb6a0287b2546231a5cb05c4b14ab4b463d171f5128148985b23eccb1e2905374873b1f09b9487f47afa6b1f2b0083ac8b4f7e8", - "03004a8a3d242d7957c0b60fb7208d386fa6a0193aabd1f3f095ffd0ac097e447b", - "04eb0db2d71ccbb0edd8fb35092cbcae2f7fa1f06d4c170804bf52007924b569a8d2d6f6bc8fd2b3caa3253fa1bb674443743bf7fb9f94f9c0b0831a252894cfa8", - "04516cde23e14f2319423b7a4a7ae48b1dadceb5e9c123198d417d10895684c42eb05e210f90ccbc72448803a22312e3f122ff2939956ccef4f7316f836295ddd5", - "038f47dcd43ba6d97fc9ed2e3bba09b175a45fac55f0683e8cf771e8ced4572354", - "04c6bec3b07586a4b085a78cbb97e9bab6f1d3c9ebf299b65dec85213c5eacd44487de86017183120bb7ea3b6c6660c5037615fe1add2a73f800cbeeae22c60438", - "03e1a1cfa9eaff604ae237b7af31ffe4c01be22eb96f3da0e62c5850dd4b4386c1", - "028d3a2d9f1b1c5c75845944f93bc183ba23aecde53f1978b8aa1b77661be6114f", - "028bde91b10013e08949a318018fedbd896534a549a278e220169ee2a36517c7aa", - "04c4b0bbb339aa236bff38dbe6a451e111972a7909a126bc424013cba2ec33bc38e98ac269ffe028345c31ac8d0a365f29c8f7e7cfccac72f84e1acd02bc554f35", - ]), - expect: fmt(vec![ - "0234dd69c56c36a41230d573d68adeae0030c9bc0bf26f24d3e1b64c604d293c68", - "028bde91b10013e08949a318018fedbd896534a549a278e220169ee2a36517c7aa", - "028d3a2d9f1b1c5c75845944f93bc183ba23aecde53f1978b8aa1b77661be6114f", - "028e1c947c8c0b8ed021088b8e981491ac7af2b8fabebea1abdb448424c8ed75b7", - "02c690d642c1310f3a1ababad94e3930e4023c930ea472e7f37f660fe485263b88", - "03004a8a3d242d7957c0b60fb7208d386fa6a0193aabd1f3f095ffd0ac097e447b", - "032b8324c93575034047a52e9bca05a46d8347046b91a032eff07d5de8d3f2730b", - "038f47dcd43ba6d97fc9ed2e3bba09b175a45fac55f0683e8cf771e8ced4572354", - "03e1a1cfa9eaff604ae237b7af31ffe4c01be22eb96f3da0e62c5850dd4b4386c1", - "041a181bd0e79974bd7ca552e09fc42ba9c3d5dbb3753741d6f0ab3015dbfd9a22d6b001a32f5f51ac6f2c0f35e73a6a62f59e848fa854d3d21f3f231594eeaa46", - "04516cde23e14f2319423b7a4a7ae48b1dadceb5e9c123198d417d10895684c42eb05e210f90ccbc72448803a22312e3f122ff2939956ccef4f7316f836295ddd5", - "045d753414fa292ea5b8f56e39cfb6a0287b2546231a5cb05c4b14ab4b463d171f5128148985b23eccb1e2905374873b1f09b9487f47afa6b1f2b0083ac8b4f7e8", - // These two pubkeys are mirrored. This helps verify the sort past the x value. - "04c4b0bbb339aa236bff38dbe6a451e111972a7909a126bc424013cba2ec33bc3816753d96001fd7cba3ce5372f5c9a0d63708183033538d07b1e532fc43aaacfa", - "04c4b0bbb339aa236bff38dbe6a451e111972a7909a126bc424013cba2ec33bc38e98ac269ffe028345c31ac8d0a365f29c8f7e7cfccac72f84e1acd02bc554f35", - "04c6bec3b07586a4b085a78cbb97e9bab6f1d3c9ebf299b65dec85213c5eacd44487de86017183120bb7ea3b6c6660c5037615fe1add2a73f800cbeeae22c60438", - "04eb0db2d71ccbb0edd8fb35092cbcae2f7fa1f06d4c170804bf52007924b569a8d2d6f6bc8fd2b3caa3253fa1bb674443743bf7fb9f94f9c0b0831a252894cfa8", - ]), - }, - ]; - for mut vector in vectors { - vector.input.sort_by_cached_key(|k| PublicKey::to_sort_key(*k)); - assert_eq!(vector.input, vector.expect); - } - } -} diff --git a/dash/src/blockdata/witness.rs b/dash/src/blockdata/witness.rs index 5060d7d68..e17136b47 100644 --- a/dash/src/blockdata/witness.rs +++ b/dash/src/blockdata/witness.rs @@ -10,13 +10,11 @@ use core::ops::Index; #[cfg(feature = "bincode")] use bincode::{Decode, Encode}; -use secp256k1::ecdsa; use crate::consensus::encode::{Error, MAX_VEC_SIZE}; use crate::consensus::{Decodable, Encodable, WriteExt}; use crate::io::{self, Read, Write}; use crate::prelude::*; -use crate::sighash::EcdsaSighashType; use crate::taproot::TAPROOT_ANNEX_PREFIX; use crate::{Script, VarInt}; @@ -275,20 +273,6 @@ impl Witness { self.content[end_varint..end_varint + new_element.len()].copy_from_slice(new_element); } - /// Pushes a DER-encoded ECDSA signature with a signature hash type as a new element on the - /// witness, requires an allocation. - pub fn push_bitcoin_signature( - &mut self, - signature: &ecdsa::SerializedSignature, - hash_type: EcdsaSighashType, - ) { - // Note that a maximal length ECDSA signature is 72 bytes, plus the sighash type makes 73 - let mut sig = [0; 73]; - sig[..signature.len()].copy_from_slice(signature); - sig[signature.len()] = hash_type.to_consensus_u8(); - self.push(&sig[..signature.len() + 1]); - } - fn element_at(&self, index: usize) -> Option<&[u8]> { let varint = VarInt::consensus_decode(&mut &self.content[index..]).ok()?; let start = index + varint.len(); @@ -496,8 +480,6 @@ impl From> for Witness { #[cfg(test)] mod test { - use secp256k1::ecdsa; - use super::*; use crate::Transaction; use crate::consensus::{deserialize, serialize}; @@ -580,21 +562,6 @@ mod test { } } - #[test] - fn test_push_ecdsa_sig() { - // The very first signature in block 734,958 - let sig_bytes = hex!( - "304402207c800d698f4b0298c5aac830b822f011bb02df41eb114ade9a6702f364d5e39c0220366900d2a60cab903e77ef7dd415d46509b1f78ac78906e3296f495aa1b1b541" - ); - let sig = ecdsa::Signature::from_der(&sig_bytes).unwrap(); - let mut witness = Witness::default(); - witness.push_bitcoin_signature(&sig.serialize_der(), EcdsaSighashType::All); - let expected_witness = vec![hex!( - "304402207c800d698f4b0298c5aac830b822f011bb02df41eb114ade9a6702f364d5e39c0220366900d2a60cab903e77ef7dd415d46509b1f78ac78906e3296f495aa1b1b54101" - )]; - assert_eq!(witness.to_vec(), expected_witness); - } - #[test] fn test_witness() { let w0 = hex!("03d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f2105"); diff --git a/dash/src/crypto/key.rs b/dash/src/crypto/key.rs index a20fbc2d5..d3be59ac5 100644 --- a/dash/src/crypto/key.rs +++ b/dash/src/crypto/key.rs @@ -17,7 +17,6 @@ mod tests { use crate::Network::{Mainnet, Testnet}; use crate::address::Address; use crate::crypto::key::{PrivateKey, PublicKey}; - use crate::io; /// Core's `DecodeSecret` takes a 34-byte payload only when it ends in /// the 0x01 compression flag. @@ -153,55 +152,6 @@ mod tests { assert_tokens(&pk_u.readable(), &[Token::BorrowedStr(PK_STR_U)]); } - fn random_key(mut seed: u8) -> PublicKey { - loop { - let mut data = [0; 65]; - for byte in &mut data[..] { - *byte = seed; - // totally a rng - seed = seed.wrapping_mul(41).wrapping_add(43); - } - if data[0] % 2 == 0 { - data[0] = 4; - if let Ok(key) = PublicKey::from_slice(&data[..]) { - return key; - } - } else { - data[0] = 2 + (data[0] >> 7); - if let Ok(key) = PublicKey::from_slice(&data[..33]) { - return key; - } - } - } - } - - #[test] - fn pubkey_read_write() { - const N_KEYS: usize = 20; - let keys: Vec<_> = (0..N_KEYS).map(|i| random_key(i as u8)).collect(); - - let mut v = vec![]; - for k in &keys { - k.write_into(&mut v).expect("writing into vec"); - } - - let mut dec_keys = vec![]; - let mut cursor = io::Cursor::new(&v); - for _ in 0..N_KEYS { - dec_keys.push(PublicKey::read_from(&mut cursor).expect("reading from vec")); - } - - assert_eq!(keys, dec_keys); - - // sanity checks - assert!(PublicKey::read_from(&mut cursor).is_err()); - assert!(PublicKey::read_from(io::Cursor::new(&[])).is_err()); - assert!(PublicKey::read_from(io::Cursor::new(&[0; 33][..])).is_err()); - assert!(PublicKey::read_from(io::Cursor::new(&[2; 32][..])).is_err()); - assert!(PublicKey::read_from(io::Cursor::new(&[0; 65][..])).is_err()); - assert!(PublicKey::read_from(io::Cursor::new(&[4; 64][..])).is_err()); - } - #[test] #[cfg(feature = "rand-std")] fn public_key_constructors() { diff --git a/key-wallet/src/bip32.rs b/key-wallet/src/bip32.rs index b74365ec5..0145a92d8 100644 --- a/key-wallet/src/bip32.rs +++ b/key-wallet/src/bip32.rs @@ -475,7 +475,7 @@ impl fmt::Debug for ExtendedPrivKey { } /// Extended public key -#[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord, Hash)] +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] pub struct ExtendedPubKey { /// The network this key is to be used on pub network: Network, @@ -1253,8 +1253,6 @@ pub enum Error { Hex(hex_conservative::DecodeFixedLengthBytesError), /// `PublicKey` hex should be 66 or 130 digits long. InvalidPublicKeyHexLength(usize), - /// Something is not supported based on active features - NotSupported(String), } impl fmt::Display for Error { @@ -1280,7 +1278,6 @@ impl fmt::Display for Error { Error::InvalidPublicKeyHexLength(got) => { write!(f, "PublicKey hex should be 66 or 130 digits long, got: {}", got) } - Error::NotSupported(ref msg) => write!(f, "Not supported: {}", msg), } } } diff --git a/rpc-client/src/error.rs b/rpc-client/src/error.rs index 3afc5c9a0..29637bffb 100644 --- a/rpc-client/src/error.rs +++ b/rpc-client/src/error.rs @@ -11,7 +11,6 @@ use std::{error, fmt, io}; use crate::dashcore; -use dashcore::secp256k1; pub type OldVecLen = usize; pub type FixedSizeLen = usize; @@ -23,7 +22,6 @@ pub enum Error { Hex(hex_conservative::DecodeVariableLengthBytesError), Json(serde_json::error::Error), BitcoinSerialization(dashcore::consensus::encode::Error), - Secp256k1(secp256k1::Error), Io(io::Error), InvalidAmount(dashcore::amount::ParseAmountError), InvalidCookieFile, @@ -55,12 +53,6 @@ impl From for Error { } } -impl From for Error { - fn from(e: secp256k1::Error) -> Error { - Error::Secp256k1(e) - } -} - impl From for Error { fn from(e: io::Error) -> Error { Error::Io(e) @@ -80,7 +72,6 @@ impl fmt::Display for Error { Error::Hex(ref e) => write!(f, "hex decode error: {}", e), Error::Json(ref e) => write!(f, "JSON error: {}", e), Error::BitcoinSerialization(ref e) => write!(f, "Bitcoin serialization error: {}", e), - Error::Secp256k1(ref e) => write!(f, "secp256k1 error: {}", e), Error::Io(ref e) => write!(f, "I/O error: {}", e), Error::InvalidAmount(ref e) => write!(f, "invalid amount: {}", e), Error::InvalidCookieFile => write!(f, "invalid cookie file"), @@ -102,7 +93,6 @@ impl error::Error for Error { Error::Hex(ref e) => Some(e), Error::Json(ref e) => Some(e), Error::BitcoinSerialization(ref e) => Some(e), - Error::Secp256k1(ref e) => Some(e), Error::Io(ref e) => Some(e), _ => None, } diff --git a/rpc-json/src/lib.rs b/rpc-json/src/lib.rs index b27f39dbc..12e3fb939 100644 --- a/rpc-json/src/lib.rs +++ b/rpc-json/src/lib.rs @@ -1866,7 +1866,7 @@ pub enum AddressType { } /// Used to represent arguments that can either be an address or a public key. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum PubKeyOrAddress<'a> { Address(&'a Address), PubKey(&'a PublicKey), From 8b66611fece0cae2f25823ffcc921b4d27b3fc2d Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:47:31 +0530 Subject: [PATCH 03/12] refactor: serialize public keys for scripts through `with_serialized` --- crypto/src/key.rs | 4 +++- dash/src/blockdata/script/builder.rs | 8 +++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crypto/src/key.rs b/crypto/src/key.rs index bb0b2a410..bb02ad5f3 100644 --- a/crypto/src/key.rs +++ b/crypto/src/key.rs @@ -136,7 +136,9 @@ impl PublicKey { } } - fn with_serialized R>(&self, f: F) -> R { + /// Calls `f` with the key's SEC1 encoding, compressed or not as + /// `compressed` says, without allocating. + pub fn with_serialized R>(&self, f: F) -> R { if self.compressed { f(&self.inner.serialize()) } else { diff --git a/dash/src/blockdata/script/builder.rs b/dash/src/blockdata/script/builder.rs index 5437fd7ea..c6b55f9ed 100644 --- a/dash/src/blockdata/script/builder.rs +++ b/dash/src/blockdata/script/builder.rs @@ -73,11 +73,9 @@ impl Builder { /// Adds instructions to push a public key onto the stack. pub fn push_key(self, key: &PublicKey) -> Builder { - if key.compressed { - self.push_slice(key.inner.serialize()) - } else { - self.push_slice(key.inner.serialize_uncompressed()) - } + key.with_serialized(|bytes| { + self.push_slice(<&PushBytes>::try_from(bytes).expect("a key is 33 or 65 bytes")) + }) } /// Adds instructions to push an XOnly public key onto the stack. From ee6a8d28f0db9568bf022a3a232524a8621e9d2c Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:45:14 +0530 Subject: [PATCH 04/12] refactor(dash)!: replace `CompactSignature` with `MessageSignature` --- dash/src/sign_message.rs | 16 ++++++++-- dash/src/signer.rs | 65 +++++----------------------------------- 2 files changed, 22 insertions(+), 59 deletions(-) diff --git a/dash/src/sign_message.rs b/dash/src/sign_message.rs index eb676afd8..0db589098 100644 --- a/dash/src/sign_message.rs +++ b/dash/src/sign_message.rs @@ -135,8 +135,9 @@ mod message_signing { if bytes.len() != 65 { return Err(MessageSignatureError::InvalidLength); } - // We just check this here so we can safely subtract further. - if bytes[0] < 27 { + // 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, )); @@ -283,6 +284,17 @@ mod tests { ); } + #[test_case::test_matrix([26, 35])] + #[cfg(feature = "secp-recovery")] + fn test_message_signature_header_out_of_range(header: u8) { + let mut bytes = [0u8; 65]; + bytes[0] = header; + assert_eq!( + MessageSignature::from_slice(&bytes), + Err(MessageSignatureError::InvalidEncoding(secp256k1::Error::InvalidRecoveryId)) + ); + } + #[test] #[cfg(all(feature = "secp-recovery", feature = "base64"))] fn test_incorrect_message_signature() { diff --git a/dash/src/signer.rs b/dash/src/signer.rs index 809f9e70a..10819250e 100644 --- a/dash/src/signer.rs +++ b/dash/src/signer.rs @@ -7,8 +7,9 @@ use hashes::{Hash, ripemd160, sha256, sha256d}; use crate::PublicKey as ECDSAPublicKey; use crate::prelude::Vec; -use crate::secp256k1::ecdsa::{RecoverableSignature, RecoveryId}; +use crate::secp256k1::ecdsa::RecoverableSignature; use crate::secp256k1::{Message, SecretKey}; +use crate::sign_message::MessageSignature; /// verifies the ECDSA signature /// The provided signature must be recoverable. Which means: it must contain the recovery byte as a prefix @@ -21,7 +22,7 @@ pub fn verify_data_signature( let msg = Message::from_digest(data_hash.try_into().map_err(|_| anyhow!("Invalid hash length"))?); - let sig: RecoverableSignature = RecoverableSignature::from_compact_signature(signature)?; + let sig = MessageSignature::from_slice(signature)?.signature; let pub_key = ECDSAPublicKey::from_slice(public_key).map_err(anyhow::Error::msg)?; @@ -36,8 +37,7 @@ pub fn verify_hash_signature( data_signature: &[u8], public_key_id: &[u8], ) -> Result<(), anyhow::Error> { - let signature: RecoverableSignature = - RecoverableSignature::from_compact_signature(data_signature)?; + let signature = MessageSignature::from_slice(data_signature)?.signature; let msg = Message::from_digest(data_hash.try_into().map_err(|_| anyhow!("Invalid hash length"))?); @@ -71,58 +71,9 @@ pub fn sign_hash(data_hash: &[u8], private_key: &[u8]) -> Result<[u8; 65], anyho let msg = Message::from_digest(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 - .to_compact_signature(true); - Ok(signature) -} - -/// converts the signature from/to compact format. Compact format is when the signature -/// is prefixed by the recovery byte -pub trait CompactSignature -where - Self: Sized, -{ - /// Converts the Signature with Recovery byte to the compact format where - /// the first byte of signature is occupied by the recovery byte - fn to_compact_signature(&self, is_compressed: bool) -> [u8; 65]; - /// Creates the Self from compacted version of signature - fn from_compact_signature(signature: impl AsRef<[u8]>) -> Result; -} - -impl CompactSignature for RecoverableSignature { - fn from_compact_signature(signature: impl AsRef<[u8]>) -> Result { - if signature.as_ref().len() != 65 { - bail!("the signature must be 65 bytes long") - } - - let recovery_byte = signature.as_ref()[0]; - let number = u8::from_be(recovery_byte) as i32; - let mut i = number - 27 - 4; - if i < 0 { - i += 4; - } - if !((i == 0) || (i == 1) || (i == 2) || (i == 3)) { - bail!("the recovery number must be between 0..4, got: '{}'", i); - } - - RecoverableSignature::from_compact( - &signature.as_ref()[1..], - RecoveryId::try_from(i).unwrap(), - ) - .map_err(anyhow::Error::msg) - } - - fn to_compact_signature(&self, is_compressed: bool) -> [u8; 65] { - let (recovery_byte, signature) = self.serialize_compact(); - let mut val = i32::from(recovery_byte.to_u8()) + 27 + 4; - if !is_compressed { - val -= 4; - } - let prefix = val.to_le_bytes()[0]; - let compact_signature = [&[prefix], signature.as_slice()].concat(); - compact_signature.try_into().unwrap() - } + 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()) } /// calculates double sha256 on data @@ -276,6 +227,6 @@ mod test { let validation_result = verify_data_signature(&data, &unrecoverable_signature_bytes, &k.public_key_compressed); - assert_error_contains!(validation_result, "the signature must be 65 bytes long") + assert_error_contains!(validation_result, "length not 65 bytes") } } From e91ff6561d827c96f95ebb95624c2eb0d99d4a3b Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:54:27 +0530 Subject: [PATCH 05/12] refactor: encode and hash with existing modules --- dash/src/signer.rs | 10 ++++------ key-wallet/src/bip32.rs | 8 ++++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/dash/src/signer.rs b/dash/src/signer.rs index 10819250e..e25683d5f 100644 --- a/dash/src/signer.rs +++ b/dash/src/signer.rs @@ -3,7 +3,7 @@ use core::convert::TryInto; use anyhow::{anyhow, bail}; -use hashes::{Hash, ripemd160, sha256, sha256d}; +use hashes::{Hash, hash160, sha256d}; use crate::PublicKey as ECDSAPublicKey; use crate::prelude::Vec; @@ -43,9 +43,8 @@ pub fn verify_hash_signature( 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 recovered_compressed_public_key = recovered_public_key.serialize(); - let hash_recovered_key = ripemd160_sha256(&recovered_compressed_public_key); - let are_equal = public_key_id == hash_recovered_key; + 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(()) @@ -83,8 +82,7 @@ pub fn double_sha(payload: impl AsRef<[u8]>) -> Vec { /// calculates the RIPEMD169(SHA256(data)) pub fn ripemd160_sha256(data: &[u8]) -> Vec { - let hash = sha256::Hash::hash(data).to_byte_array(); - ripemd160::Hash::hash(&hash).to_byte_array().to_vec() + hash160::Hash::hash(data).to_byte_array().to_vec() } #[cfg(test)] diff --git a/key-wallet/src/bip32.rs b/key-wallet/src/bip32.rs index 0145a92d8..da55b7018 100644 --- a/key-wallet/src/bip32.rs +++ b/key-wallet/src/bip32.rs @@ -1843,11 +1843,11 @@ impl ExtendedPubKey { }) } - /// Returns the HASH160 of the chaincode + /// Returns the HASH160 of the compressed public key pub fn identifier(&self) -> XpubIdentifier { - let mut engine = XpubIdentifier::engine(); - engine.input(&self.public_key.serialize()); - XpubIdentifier::from_engine(engine) + XpubIdentifier::from_byte_array( + dashcore::PublicKey::new(self.public_key).pubkey_hash().to_byte_array(), + ) } /// Returns the first four bytes of the identifier From 6b73e3ecbf345537307808f50d3d2438d7d0d7d5 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 6 Oct 2026 01:15:30 +0530 Subject: [PATCH 06/12] fix(key-wallet): accept a zero IL in private child derivation --- key-wallet/src/bip32.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/key-wallet/src/bip32.rs b/key-wallet/src/bip32.rs index da55b7018..6e2d044ec 100644 --- a/key-wallet/src/bip32.rs +++ b/key-wallet/src/bip32.rs @@ -1382,10 +1382,12 @@ impl ExtendedPrivKey { } } let hmac_result: Hmac = Hmac::from_engine(hmac_engine); - let sk = secp256k1::SecretKey::from_secret_bytes(hmac_secret_half(&hmac_result)) + // 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()) .expect("statistically impossible to hit"); - let tweaked = - sk.add_tweak(&self.private_key.into()).expect("statistically impossible to hit"); Ok(ExtendedPrivKey { network: self.network, From c53a818de39b12c8493fe04e2ee8e036091c0ac9 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:11:24 +0530 Subject: [PATCH 07/12] refactor: build compressed private keys with `PrivateKey::new` --- key-wallet-ffi/src/account_derivation.rs | 6 +--- key-wallet-ffi/src/keys.rs | 7 +---- key-wallet/src/account/mod.rs | 12 ++------ key-wallet/src/bip32.rs | 6 +--- key-wallet/src/wallet/helper.rs | 7 +---- key-wallet/tests/address_tests.rs | 18 ++++++------ rpc-integration-test/src/main.rs | 35 +++++++----------------- 7 files changed, 24 insertions(+), 67 deletions(-) diff --git a/key-wallet-ffi/src/account_derivation.rs b/key-wallet-ffi/src/account_derivation.rs index d1c18705d..05816b163 100644 --- a/key-wallet-ffi/src/account_derivation.rs +++ b/key-wallet-ffi/src/account_derivation.rs @@ -262,11 +262,7 @@ 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 { - compressed: true, - network: account.inner().network(), - inner: derived.private_key, - }; + let dash_priv = dashcore::PrivateKey::new(derived.private_key, account.inner().network()); unwrap_or_return!(CString::new(dash_priv.to_wif()), error).into_raw() } diff --git a/key-wallet-ffi/src/keys.rs b/key-wallet-ffi/src/keys.rs index e2b059114..07ae3e65a 100644 --- a/key-wallet-ffi/src/keys.rs +++ b/key-wallet-ffi/src/keys.rs @@ -277,12 +277,7 @@ pub unsafe extern "C" fn private_key_to_wif( let network_rust: key_wallet::Network = network.into(); // Convert to WIF format - use dashcore::PrivateKey as DashPrivateKey; - let dash_key = DashPrivateKey { - compressed: true, - network: network_rust, - inner: key.inner, - }; + let dash_key = dashcore::PrivateKey::new(key.inner, network_rust); let wif = dash_key.to_wif(); unwrap_or_return!(CString::new(wif), error).into_raw() diff --git a/key-wallet/src/account/mod.rs b/key-wallet/src/account/mod.rs index 7cf617a61..e1c595f0b 100644 --- a/key-wallet/src/account/mod.rs +++ b/key-wallet/src/account/mod.rs @@ -316,11 +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 { - compressed: true, - network: self.network, - inner: xpriv.private_key, - }) + Ok(dashcore::PrivateKey::new(xpriv.private_key, self.network)) } fn derive_from_seed_extended_xpriv_at( @@ -338,11 +334,7 @@ impl AccountDerivation std::result::Result { let xpriv = self.derive_from_seed_extended_xpriv_at(seed, index)?; - Ok(dashcore::PrivateKey { - compressed: true, - network: self.network, - inner: xpriv.private_key, - }) + Ok(dashcore::PrivateKey::new(xpriv.private_key, self.network)) } } diff --git a/key-wallet/src/bip32.rs b/key-wallet/src/bip32.rs index 6e2d044ec..412187ba7 100644 --- a/key-wallet/src/bip32.rs +++ b/key-wallet/src/bip32.rs @@ -1573,11 +1573,7 @@ impl ExtendedPrivKey { /// Convert to a PrivateKey for signing operations pub fn to_priv(&self) -> dashcore::PrivateKey { - dashcore::PrivateKey { - compressed: true, - network: self.network, - inner: self.private_key, - } + dashcore::PrivateKey::new(self.private_key, self.network) } } diff --git a/key-wallet/src/wallet/helper.rs b/key-wallet/src/wallet/helper.rs index a6c3c3e1d..1d49a39aa 100644 --- a/key-wallet/src/wallet/helper.rs +++ b/key-wallet/src/wallet/helper.rs @@ -427,12 +427,7 @@ impl Wallet { let private_key = self.derive_private_key(path)?; // Convert to WIF format - use dashcore::PrivateKey as DashPrivateKey; - let dash_key = DashPrivateKey { - compressed: true, - network: self.network, - inner: private_key, - }; + let dash_key = dashcore::PrivateKey::new(private_key, self.network); Ok(dash_key.to_wif()) } diff --git a/key-wallet/tests/address_tests.rs b/key-wallet/tests/address_tests.rs index 6d6685139..f90187b96 100644 --- a/key-wallet/tests/address_tests.rs +++ b/key-wallet/tests/address_tests.rs @@ -63,11 +63,10 @@ fn test_address_parsing() { use dashcore::key::PrivateKey; // Create a mainnet address - let privkey_mainnet = PrivateKey { - compressed: true, - network: DashNetwork::Mainnet, - inner: dashcore::secp256k1::SecretKey::from_secret_bytes([0x01; 32]).unwrap(), - }; + let privkey_mainnet = PrivateKey::new( + dashcore::secp256k1::SecretKey::from_secret_bytes([0x01; 32]).unwrap(), + DashNetwork::Mainnet, + ); let pubkey_mainnet = privkey_mainnet.public_key(); let mainnet_address = Address::p2pkh(&pubkey_mainnet, DashNetwork::Mainnet); @@ -82,11 +81,10 @@ fn test_address_parsing() { assert_eq!(checked_mainnet.address_type(), Some(AddressType::P2pkh)); // Create a testnet address - let privkey_testnet = PrivateKey { - compressed: true, - network: DashNetwork::Testnet, - inner: dashcore::secp256k1::SecretKey::from_secret_bytes([0x02; 32]).unwrap(), - }; + let privkey_testnet = PrivateKey::new( + dashcore::secp256k1::SecretKey::from_secret_bytes([0x02; 32]).unwrap(), + DashNetwork::Testnet, + ); let pubkey_testnet = privkey_testnet.public_key(); let testnet_address = Address::p2pkh(&pubkey_testnet, DashNetwork::Testnet); diff --git a/rpc-integration-test/src/main.rs b/rpc-integration-test/src/main.rs index b7ecda2ef..ed0f3e1aa 100644 --- a/rpc-integration-test/src/main.rs +++ b/rpc-integration-test/src/main.rs @@ -711,11 +711,8 @@ fn test_get_block_filter(cl: &Client) { } fn test_sign_raw_transaction_with_send_raw_transaction(cl: &Client) { - let sk = PrivateKey { - network: Network::Regtest, - inner: secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), - compressed: true, - }; + let sk = + PrivateKey::new(secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), Network::Regtest); let addr = Address::p2pkh(&sk.public_key(), Network::Regtest); let options = json::ListUnspentQueryOptions { @@ -1029,33 +1026,24 @@ fn test_list_received_by_address(cl: &Client) { } fn test_import_public_key(cl: &Client) { - let sk = PrivateKey { - network: Network::Regtest, - inner: secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), - compressed: true, - }; + let sk = + PrivateKey::new(secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), 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 { - network: Network::Regtest, - inner: secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), - compressed: true, - }; + let sk = + PrivateKey::new(secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), 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 { - network: Network::Regtest, - inner: secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), - compressed: true, - }; + let sk = + PrivateKey::new(secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), 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(); @@ -1063,11 +1051,8 @@ fn test_import_address(cl: &Client) { } fn test_import_address_script(cl: &Client) { - let sk = PrivateKey { - network: Network::Regtest, - inner: secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), - compressed: true, - }; + let sk = + PrivateKey::new(secp256k1::SecretKey::new(&mut secp256k1::rand::rng()), 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(); From c42e16ab14528ab46b525ac92bc0be02aadf324a Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:22:41 +0530 Subject: [PATCH 08/12] refactor(key-wallet)!: derive BIP32 master keys in one place --- key-wallet/src/bip32.rs | 18 ++++++++++++ key-wallet/src/wallet/root_extended_keys.rs | 32 ++------------------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/key-wallet/src/bip32.rs b/key-wallet/src/bip32.rs index 412187ba7..cf618fdc4 100644 --- a/key-wallet/src/bip32.rs +++ b/key-wallet/src/bip32.rs @@ -1247,6 +1247,8 @@ pub enum Error { UnknownVersion([u8; 4]), /// Encoded extended key data has wrong length WrongExtendedKeyLength(usize), + /// A master key seed must be 16 to 64 bytes + InvalidSeedLength(usize), /// Base58 encoding error Base58(base58::DecodeCheckError), /// Hexadecimal decoding error @@ -1273,6 +1275,9 @@ impl fmt::Display for Error { Error::WrongExtendedKeyLength(ref len) => { write!(f, "encoded extended key data has wrong length {}", len) } + Error::InvalidSeedLength(len) => { + write!(f, "seed must be 16 to 64 bytes, got {}", len) + } Error::Base58(ref err) => write!(f, "base58 encoding error: {}", err), Error::Hex(ref e) => write!(f, "Hexadecimal decoding error: {}", e), Error::InvalidPublicKeyHexLength(got) => { @@ -1315,6 +1320,11 @@ fn hmac_secret_half(hmac: &Hmac) -> [u8; secp256k1::constants::SEC impl ExtendedPrivKey { /// Construct a new master key from a seed value pub fn new_master(network: Network, seed: &[u8]) -> Result { + // BIP32 seeds are 128 to 512 bits. + if !(16..=64).contains(&seed.len()) { + return Err(Error::InvalidSeedLength(seed.len())); + } + let mut hmac_engine: HmacEngine = HmacEngine::new(b"Bitcoin seed"); hmac_engine.input(seed); let hmac_result: Hmac = Hmac::from_engine(hmac_engine); @@ -1919,6 +1929,14 @@ mod tests { assert!(format!("0x{digits}").parse::().is_err()); } + #[test_case::test_case(15 => Err(Error::InvalidSeedLength(15)) ; "too short")] + #[test_case::test_case(16 => Ok(()) ; "shortest")] + #[test_case::test_case(64 => Ok(()) ; "longest")] + #[test_case::test_case(65 => Err(Error::InvalidSeedLength(65)) ; "too long")] + fn master_seed_length(len: usize) -> Result<(), Error> { + ExtendedPrivKey::new_master(Mainnet, &vec![0x2a; len]).map(|_| ()) + } + #[test] fn test_parse_derivation_path() { assert_eq!(DerivationPath::from_str("42"), Err(Error::InvalidDerivationPathFormat)); diff --git a/key-wallet/src/wallet/root_extended_keys.rs b/key-wallet/src/wallet/root_extended_keys.rs index 7668a4ff0..620ad7dac 100644 --- a/key-wallet/src/wallet/root_extended_keys.rs +++ b/key-wallet/src/wallet/root_extended_keys.rs @@ -9,7 +9,6 @@ use crate::{Error, Network, Wallet}; use bincode::{BorrowDecode, Decode, Encode}; #[cfg(feature = "bls")] use dashcore::bls_sig_utils::BlsSkBytes; -use dashcore_hashes::{sha512, Hash, HashEngine, Hmac, HmacEngine}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use zeroize::Zeroize; @@ -45,34 +44,9 @@ impl RootExtendedPrivKey { /// Create a new master key from seed pub fn new_master(seed: &[u8]) -> Result { - // Seed should be between 128 and 512 bits (16 to 64 bytes) - if seed.len() < 16 || seed.len() > 64 { - return Err(crate::error::Error::InvalidParameter(format!( - "Invalid seed length: {} bytes", - seed.len() - ))); - } - - let mut hmac_engine: HmacEngine = HmacEngine::new(b"Bitcoin seed"); - hmac_engine.input(seed); - let hmac_result: Hmac = Hmac::from_engine(hmac_engine); - - // Split the result into private key (first 32 bytes) and chain code (last 32 bytes) - let mut private_key_bytes = [0u8; 32]; - private_key_bytes.copy_from_slice(&hmac_result[..32]); - let private_key = - secp256k1::SecretKey::from_secret_bytes(private_key_bytes).map_err(|e| { - crate::error::Error::InvalidParameter(format!("Invalid private key: {}", e)) - })?; - - let mut chain_code_bytes = [0u8; 32]; - chain_code_bytes.copy_from_slice(&hmac_result[32..64]); - let chain_code = ChainCode::from(chain_code_bytes); - - Ok(Self { - root_private_key: private_key, - root_chain_code: chain_code, - }) + // The network only picks the serialization version, which is dropped. + let master = ExtendedPrivKey::new_master(Network::Mainnet, seed)?; + Ok(Self::from_extended_priv_key(&master)) } /// Create from an ExtendedPrivKey (must be depth 0) From 589c007d787c7e48fe0f951efdaf43e2dae02d98 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:04:25 +0530 Subject: [PATCH 09/12] refactor!: build P2PKH scripts and scriptSigs with `dashcore` helpers --- key-wallet-ffi/src/transaction.rs | 51 +++++++++++-------- .../transaction_builder.rs | 14 ++--- 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs index d094cdffa..a1e604e69 100644 --- a/key-wallet-ffi/src/transaction.rs +++ b/key-wallet-ffi/src/transaction.rs @@ -7,9 +7,10 @@ use crate::types::{ 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::{ - consensus, hashes::Hash, sighash::SighashCache, Network, OutPoint, Script, ScriptBuf, - Transaction, TxIn, TxOut, Txid, + consensus, hashes::Hash, sighash::SighashCache, EcdsaSighashType, Network, OutPoint, Script, + ScriptBuf, Transaction, TxIn, TxOut, Txid, }; use key_wallet::wallet::managed_wallet_info::asset_lock_builder::{ AssetLockFundingType, CreditOutputFunding, @@ -601,19 +602,17 @@ pub unsafe extern "C" fn transaction_sign_input( let sig = privkey.sign_ecdsa(message); // Build signature script (simplified P2PKH) - let mut sig_bytes = sig.serialize_der().to_vec(); - sig_bytes.push(sighash_type as u8); - - let pubkey = secp256k1::PublicKey::from_secret_key(&privkey); - let pubkey_bytes = pubkey.serialize(); - - let mut script_sig = vec![]; - script_sig.push(sig_bytes.len() as u8); - script_sig.extend_from_slice(&sig_bytes); - script_sig.push(pubkey_bytes.len() as u8); - script_sig.extend_from_slice(&pubkey_bytes); - - tx.inner.input[input_index].script_sig = ScriptBuf::from(script_sig); + let pubkey = dashcore::PublicKey::new(secp256k1::PublicKey::from_secret_key(&privkey)); + tx.inner.input[input_index].script_sig = Builder::new() + .push_slice( + dashcore::ecdsa::Signature { + sig, + hash_ty: EcdsaSighashType::from_consensus(sighash_type), + } + .serialize(), + ) + .push_key(&pubkey) + .into_script(); 0 } @@ -639,12 +638,8 @@ pub unsafe extern "C" fn script_p2pkh( return -1; } - let hash_slice = slice::from_raw_parts(pubkey_hash, 20); - - // Build P2PKH script: OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG - let mut script = vec![0x76, 0xa9, 0x14]; // OP_DUP OP_HASH160 PUSH(20) - script.extend_from_slice(hash_slice); - script.extend_from_slice(&[0x88, 0xac]); // OP_EQUALVERIFY OP_CHECKSIG + let hash = <[u8; 20]>::try_from(slice::from_raw_parts(pubkey_hash, 20)).expect("20 bytes"); + let script = ScriptBuf::new_p2pkh(&dashcore::PubkeyHash::from_byte_array(hash)); let size = script.len() as u32; @@ -659,7 +654,7 @@ pub unsafe extern "C" fn script_p2pkh( return -1; } - ptr::copy_nonoverlapping(script.as_ptr(), out_buf, script.len()); + ptr::copy_nonoverlapping(script.as_bytes().as_ptr(), out_buf, script.len()); *out_len = size; 0 } @@ -992,6 +987,18 @@ pub unsafe extern "C" fn wallet_build_and_sign_asset_lock_transaction( mod tests { use super::*; + #[test] + fn script_p2pkh_is_dup_hash160_push_equalverify_checksig() { + let hash = [0x11u8; 20]; + let mut out = [0u8; 25]; + let mut len = out.len() as u32; + assert_eq!(unsafe { script_p2pkh(hash.as_ptr(), out.as_mut_ptr(), &mut len) }, 0); + assert_eq!(len, 25); + assert_eq!(out[..3], [0x76, 0xa9, 0x14]); + assert_eq!(out[3..23], hash); + assert_eq!(out[23..], [0x88, 0xac]); + } + fn preference(kind: FFIAccountTypePreferenceKind) -> FFIAccountTypePreference { FFIAccountTypePreference { kind, 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 3ae4ff233..982cb1b9b 100644 --- a/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs +++ b/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs @@ -1020,16 +1020,10 @@ pub trait TransactionSigner { for (sighash, path) in tasks { let (sig, pubkey) = self.sig_and_pubkey(sighash, path).await?; - let mut sig_bytes = sig.serialize_der().to_vec(); - sig_bytes.push(EcdsaSighashType::All.to_u32() as u8); - - let script_sig = - Builder::new() - .push_slice(<&PushBytes>::try_from(sig_bytes.as_slice()).map_err(|_| { - BuilderError::SigningFailed("invalid signature length".into()) - })?) - .push_slice(pubkey.serialize()) - .into_script(); + let script_sig = Builder::new() + .push_slice(dashcore::ecdsa::Signature::sighash_all(sig).serialize()) + .push_key(&dashcore::PublicKey::new(pubkey)) + .into_script(); signatures.push(script_sig); } From d9186dd8188acf5c87b0500f22f0be4ef76b001c Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 5 Oct 2026 03:15:49 +0530 Subject: [PATCH 10/12] fix(key-wallet): refuse sighash types wider than a byte when signing --- key-wallet-ffi/Cargo.toml | 1 + key-wallet-ffi/FFI_API.md | 2 +- key-wallet-ffi/src/transaction.rs | 76 ++++++++++++++++++++++++++++++- 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/key-wallet-ffi/Cargo.toml b/key-wallet-ffi/Cargo.toml index 7da9665c2..de25f0e86 100644 --- a/key-wallet-ffi/Cargo.toml +++ b/key-wallet-ffi/Cargo.toml @@ -47,3 +47,4 @@ cbindgen = "0.29" dashcore = { path = "../dash", features = ["test-utils"] } key-wallet = { path = "../key-wallet", features = ["test-utils"] } key-wallet-manager = { path = "../key-wallet-manager", features = ["test-utils"] } +test-case = "3.3" diff --git a/key-wallet-ffi/FFI_API.md b/key-wallet-ffi/FFI_API.md index c953ab253..3304a763c 100644 --- a/key-wallet-ffi/FFI_API.md +++ b/key-wallet-ffi/FFI_API.md @@ -3802,7 +3802,7 @@ transaction_sign_input(tx: *mut FFITransaction, input_index: u32, private_key: * ``` **Description:** -Sign a transaction input # Safety - `tx` must be a valid pointer to an FFITransaction - `private_key` must be a valid pointer to a 32-byte private key - `script_pubkey` must be a valid pointer to the script pubkey # Returns - 0 on success - -1 on error +Sign a transaction input # Safety - `tx` must be a valid pointer to an FFITransaction - `private_key` must be a valid pointer to a 32-byte private key - `script_pubkey` must be a valid pointer to the script pubkey # Returns - 0 on success - -1 on error, including a `sighash_type` above 0xff **Safety:** - `tx` must be a valid pointer to an FFITransaction - `private_key` must be a valid pointer to a 32-byte private key - `script_pubkey` must be a valid pointer to the script pubkey diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs index a1e604e69..894a860a3 100644 --- a/key-wallet-ffi/src/transaction.rs +++ b/key-wallet-ffi/src/transaction.rs @@ -551,7 +551,7 @@ pub unsafe extern "C" fn transaction_sighash( /// /// # Returns /// - 0 on success -/// - -1 on error +/// - -1 on error, including a `sighash_type` above 0xff #[no_mangle] pub unsafe extern "C" fn transaction_sign_input( tx: *mut FFITransaction, @@ -572,6 +572,12 @@ pub unsafe extern "C" fn transaction_sign_input( return -1; } + // Only the low byte of the type is appended to the signature, and that + // byte is what the input is verified under. + if u8::try_from(sighash_type).is_err() { + return -1; + } + // Calculate sighash let mut sighash = [0u8; 32]; if transaction_sighash( @@ -999,6 +1005,74 @@ mod tests { assert_eq!(out[23..], [0x88, 0xac]); } + /// A type that fits in a byte signs, and the scriptSig carries that byte + /// with a signature over the sighash computed under it; a wider type is + /// refused. + #[test_case::test_case(0x01, true ; "all")] + #[test_case::test_case(0x81, true ; "all anyone can pay")] + #[test_case::test_case(0x04, true ; "non-standard")] + #[test_case::test_case(0x101, false ; "all with a high bit")] + #[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 script_pubkey = ScriptBuf::new_p2pkh(&dashcore::PublicKey::new(pubkey).pubkey_hash()); + + unsafe { + let tx = transaction_create(); + let input = FFITxIn { + txid: [1; 32], + vout: 0, + script_sig_len: 0, + script_sig: ptr::null(), + sequence: u32::MAX, + }; + assert_eq!(transaction_add_input(tx, &input), 0); + + let signed = transaction_sign_input( + tx, + 0, + secret.as_ptr(), + script_pubkey.as_bytes().as_ptr(), + script_pubkey.len() as u32, + sighash_type, + ); + assert_eq!(signed == 0, ok); + + if ok { + let mut pushes = (&(*tx).inner.input)[0] + .script_sig + .instructions() + .map(|i| i.unwrap().push_bytes().unwrap().as_bytes().to_vec()); + 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(), None); + + let mut sighash = [0u8; 32]; + assert_eq!( + transaction_sighash( + tx, + 0, + script_pubkey.as_bytes().as_ptr(), + script_pubkey.len() as u32, + hash_ty as u32, + sighash.as_mut_ptr(), + ), + 0 + ); + secp256k1::ecdsa::Signature::from_der(der) + .unwrap() + .verify(Message::from_digest(sighash), &pubkey) + .unwrap(); + } + + transaction_destroy(tx); + } + } + fn preference(kind: FFIAccountTypePreferenceKind) -> FFIAccountTypePreference { FFIAccountTypePreference { kind, From d018a0b8281883fbb3da01049cc8a8562491110d Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 4 Oct 2026 20:58:53 +0530 Subject: [PATCH 11/12] refactor(key-wallet): drop the sender key length check in `tx_decode` --- key-wallet-ffi/src/tx_decode.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/key-wallet-ffi/src/tx_decode.rs b/key-wallet-ffi/src/tx_decode.rs index e3954b816..fc8e2b0ce 100644 --- a/key-wallet-ffi/src/tx_decode.rs +++ b/key-wallet-ffi/src/tx_decode.rs @@ -113,9 +113,6 @@ fn input_address_from_script_sig(script_sig: &ScriptBuf, network: Network) -> Op if sig.is_empty() || sig[0] != 0x30 || sig.len() > 73 { return None; } - if pubkey_bytes.len() != 33 && pubkey_bytes.len() != 65 { - return None; - } let pubkey = PublicKey::from_slice(pubkey_bytes).ok()?; Some(Address::p2pkh(&pubkey, network)) } From cb23ea208259558c2065f3408969074ae9d8ef69 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:06:58 +0530 Subject: [PATCH 12/12] docs: update changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ded8307bc..1d11213a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## Unreleased +### Added + +- Make `PublicKey::with_serialized` public + ### Changed - **Breaking:** the `bincode` feature and binary serialization dependencies now use @@ -47,6 +51,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **Breaking:** `dashcore::base58` is `base58ck` 0.5.0, so `Base58CkString` replaces the `encode_*` functions and the `Base58` error variants carry `base58::DecodeCheckError` +- **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 - 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 @@ -58,6 +66,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `transaction_sighash` in `key-wallet-ffi` hashes non-standard flags as given and rejects flags above `0xff` - Reject 256-bit child numbers in `key-wallet` with an odd number of hex digits +- Reject WIF keys whose compression flag is not `0x01`, with + `key::Error::InvalidWifCompressionFlag` +- Accept an IL of zero in `key-wallet` private child derivation +- `transaction_sign_input` in `key-wallet-ffi` rejects flags above `0xff` ### Removed @@ -74,6 +86,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **Breaking:** remove the `hex` module from `dashcore-private`, and the `btreemap_byte_values`, `btreemap_as_seq`, `btreemap_as_seq_byte_values` and `hex_bytes` serde helpers from `dashcore::serde_utils` +- **Breaking:** remove ordering from `PublicKey`, `ExtendedPubKey` and + `PubKeyOrAddress`, and `PublicKey::to_sort_key` and `SortKey` with it +- **Breaking:** remove `PublicKey::read_from` and + `Witness::push_bitcoin_signature` +- **Breaking:** remove `key::Error::NotSupported`, + `bip32::Error::NotSupported` and `dashcore_rpc::Error::Secp256k1` +- **Breaking:** remove `signer::CompactSignature`. Use `MessageSignature` + instead ## 0.44.0 - 2026-07-01