From 4743889feed1eda9190e336132a8653ca9c309b8 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:19:50 +0530 Subject: [PATCH 01/11] refactor(dash)!: drop the unconstructed Ed25519 key error variants --- dash/src/base58.rs | 8 -------- dash/src/crypto/key.rs | 7 ------- 2 files changed, 15 deletions(-) diff --git a/dash/src/base58.rs b/dash/src/base58.rs index 4ca76d68d..c7bd4ab6f 100644 --- a/dash/src/base58.rs +++ b/dash/src/base58.rs @@ -52,10 +52,6 @@ pub enum Error { // TODO: Remove this as part of crate-smashing, there should not be any key related errors in this module Hex(hex::Error), - /// edwards 25519 related error - #[cfg(feature = "ed25519-dalek")] - Ed25519Dalek(String), - /// A feature is not supported NotSupported(String), } @@ -77,8 +73,6 @@ impl fmt::Display for Error { Error::TooShort(_) => write!(f, "base58ck data not even long enough for a checksum"), Error::Secp256k1(ref e) => fmt::Display::fmt(&e, f), Error::Hex(ref e) => write!(f, "Hexadecimal decoding error: {}", e), - #[cfg(feature = "ed25519-dalek")] - Error::Ed25519Dalek(ref e) => write!(f, "Ed25519-Dalek error: {}", e), Error::NotSupported(ref e) => write!(f, "Not supported: {}", e), } } @@ -416,8 +410,6 @@ impl From for Error { key::Error::InvalidKeyPrefix(_) => Error::Secp256k1(secp256k1::Error::InvalidPublicKey), key::Error::Hex(e) => Error::Hex(e), key::Error::InvalidHexLength(size) => Error::InvalidLength(size), - #[cfg(feature = "ed25519-dalek")] - key::Error::Ed25519Dalek(e) => Error::Ed25519Dalek(e), key::Error::NotSupported(e) => Error::NotSupported(e), } } diff --git a/dash/src/crypto/key.rs b/dash/src/crypto/key.rs index 3b0cff47d..20e8b378c 100644 --- a/dash/src/crypto/key.rs +++ b/dash/src/crypto/key.rs @@ -43,9 +43,6 @@ pub enum Error { Base58(base58::Error), /// secp256k1-related error Secp256k1(secp256k1::Error), - /// edwards 25519 related error - #[cfg(feature = "ed25519-dalek")] - Ed25519Dalek(String), /// Invalid key prefix error InvalidKeyPrefix(u8), /// Hex decoding error @@ -69,8 +66,6 @@ impl fmt::Display for Error { Error::NotSupported(string) => { write!(f, "{}", string.as_str()) } - #[cfg(feature = "ed25519-dalek")] - Error::Ed25519Dalek(string) => write!(f, "{}", string.as_str()), } } } @@ -85,8 +80,6 @@ impl std::error::Error for Error { Hex(e) => Some(e), InvalidKeyPrefix(_) | InvalidHexLength(_) => None, NotSupported(_) => None, - #[cfg(feature = "ed25519-dalek")] - Ed25519Dalek(_) => None, } } } From 3c4135d068a572cf94fefe061178d562a09dcf6f Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:59:45 +0530 Subject: [PATCH 02/11] build(deps)!: take base64 and base58 from their own crates --- crypto/Cargo.toml | 1 + crypto/src/lib.rs | 1 + dash/Cargo.toml | 4 +- dash/src/address.rs | 20 +- dash/src/base58.rs | 485 ------------------ .../provider_registration.rs | 12 +- dash/src/crypto/key.rs | 25 +- dash/src/lib.rs | 2 +- dash/src/sign_message.rs | 20 +- key-wallet/Cargo.toml | 2 +- key-wallet/src/bip32.rs | 15 +- 11 files changed, 66 insertions(+), 521 deletions(-) delete mode 100644 dash/src/base58.rs diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index 87ce32809..08b9be553 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -17,6 +17,7 @@ bls = ["dep:dash-pkc", "dep:tracing"] serde = ["dep:serde", "dash-types/serde", "dashcore_hashes/serde"] [dependencies] +base58ck = { version = "0.1.0" } bincode = { workspace = true, optional = true } bincode_derive = { workspace = true, optional = true } dash-pkc = { version = "=0.1.0-beta", default-features = false, features = ["bls", "std"], optional = true } diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index ce3fc450f..b77b8ab26 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -11,6 +11,7 @@ extern crate alloc; +pub extern crate base58ck as base58; #[cfg(feature = "bls")] pub extern crate dash_pkc; pub extern crate dashcore_hashes as hashes; diff --git a/dash/Cargo.toml b/dash/Cargo.toml index cb958f9be..df0604efe 100644 --- a/dash/Cargo.toml +++ b/dash/Cargo.toml @@ -20,7 +20,7 @@ edition = "2024" # Please don't forget to add relevant features to docs.rs below [features] default = ["secp-recovery", "bincode" ] -base64 = [ "base64-compat" ] +base64 = [ "dep:base64" ] rand-std = ["secp256k1/rand"] rand = ["secp256k1/rand"] serde = ["dep:serde", "dashcore-crypto/serde", "dashcore_hashes/serde", "secp256k1/serde", "dash-network/serde"] @@ -50,7 +50,7 @@ dash-types = { version = "=0.1.0-beta", default-features = false, features = ["c secp256k1 = { version = "0.33.1" } serde = { version = "1.0.219", default-features = false, features = [ "derive", "alloc" ], optional = true } -base64-compat = { version = "1.0.0", optional = true } +base64 = { version = "0.22", optional = true } bitcoinconsensus = { version = "0.20.2-0.5.0", default-features = false, optional = true } hex_lit = "0.1.1" anyhow = { version= "1.0" } diff --git a/dash/src/address.rs b/dash/src/address.rs index 46f6a8c19..26fd47001 100644 --- a/dash/src/address.rs +++ b/dash/src/address.rs @@ -74,6 +74,10 @@ use serde::{Deserialize, Serialize}; pub enum Error { /// Base58 encoding error. Base58(base58::Error), + /// The address version byte was not one we recognise. + InvalidAddressVersion(u8), + /// The base58 decoded correctly but the payload was the wrong length. + InvalidBase58PayloadLength(usize), /// Bech32 encoding error. Bech32(bech32::Error), /// The bech32 payload was empty. @@ -116,6 +120,12 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Error::Base58(ref e) => write_err!(f, "base58 address encoding error"; e), + Error::InvalidAddressVersion(ref v) => { + write!(f, "address version {} is invalid for this base58 type", v) + } + Error::InvalidBase58PayloadLength(ref l) => { + write!(f, "length {} invalid for this base58 type", l) + } Error::Bech32(ref e) => write_err!(f, "bech32 address encoding error"; e), Error::EmptyBech32Payload => write!(f, "the bech32 payload was empty"), Error::InvalidBech32Variant { expected, found } => write!(f, "invalid bech32 checksum variant found {:?} when {:?} was expected", found, expected), @@ -145,7 +155,9 @@ impl std::error::Error for Error { Base58(e) => Some(e), Bech32(e) => Some(e), UnparsableWitnessVersion(e) => Some(e), - EmptyBech32Payload + InvalidAddressVersion(_) + | InvalidBase58PayloadLength(_) + | EmptyBech32Payload | InvalidBech32Variant { .. } @@ -1380,11 +1392,11 @@ impl FromStr for Address { // Base58 if s.len() > 50 { - return Err(Error::Base58(base58::Error::InvalidLength(s.len() * 11 / 15))); + return Err(Error::InvalidBase58PayloadLength(s.len() * 11 / 15)); } let data = base58::decode_check(s)?; if data.len() != 21 { - return Err(Error::Base58(base58::Error::InvalidLength(data.len()))); + return Err(Error::InvalidBase58PayloadLength(data.len())); } let (network, payload) = match data[0] { @@ -1400,7 +1412,7 @@ impl FromStr for Address { SCRIPT_ADDRESS_PREFIX_TEST => { (Network::Testnet, Payload::ScriptHash(ScriptHash::from_slice(&data[1..]).unwrap())) } - x => return Err(Error::Base58(base58::Error::InvalidAddressVersion(x))), + x => return Err(Error::InvalidAddressVersion(x)), }; Ok(Address::new(network, payload)) diff --git a/dash/src/base58.rs b/dash/src/base58.rs deleted file mode 100644 index c7bd4ab6f..000000000 --- a/dash/src/base58.rs +++ /dev/null @@ -1,485 +0,0 @@ -// Rust Dash Library -// Originally written in 2014 by -// Andrew Poelstra -// For Bitcoin -// Updated for Dash in 2022 by -// The Dash Core Developers -// -// To the extent possible under law, the author(s) have dedicated all -// copyright and related and neighboring rights to this software to -// the public domain worldwide. This software is distributed without -// any warranty. -// -// You should have received a copy of the CC0 Public Domain Dedication -// along with this software. -// If not, see . -// - -//! Base58 encoder and decoder. -//! -//! This module provides functions for encoding and decoding base58 slices and -//! strings respectively. -//! - -use core::{fmt, iter, slice, str}; - -use hashes::{Hash, hex, sha256d}; -use secp256k1; - -use crate::key; -use crate::prelude::*; - -/// An error that might occur during base58 decoding -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] -pub enum Error { - /// Invalid character encountered - BadByte(u8), - /// Checksum was not correct (expected, actual) - BadChecksum(u32, u32), - /// The length (in bytes) of the object was not correct - /// Note that if the length is excessively long the provided length may be - /// an estimate (and the checksum step may be skipped). - InvalidLength(usize), - /// Extended Key version byte(s) were not recognized - InvalidExtendedKeyVersion([u8; 4]), - /// Address version byte were not recognized - InvalidAddressVersion(u8), - /// Checked data was less than 4 bytes - TooShort(usize), - /// Secp256k1 error while parsing a secret key - Secp256k1(secp256k1::Error), - /// Hex decoding error - // TODO: Remove this as part of crate-smashing, there should not be any key related errors in this module - Hex(hex::Error), - - /// A feature is not supported - NotSupported(String), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - Error::BadByte(b) => write!(f, "invalid base58 character 0x{:x}", b), - Error::BadChecksum(exp, actual) => { - write!(f, "base58ck checksum 0x{:x} does not match expected 0x{:x}", actual, exp) - } - Error::InvalidLength(ell) => write!(f, "length {} invalid for this base58 type", ell), - Error::InvalidAddressVersion(ref v) => { - write!(f, "address version {} is invalid for this base58 type", v) - } - Error::InvalidExtendedKeyVersion(ref v) => { - write!(f, "extended key version {:#04x?} is invalid for this base58 type", v) - } - Error::TooShort(_) => write!(f, "base58ck data not even long enough for a checksum"), - Error::Secp256k1(ref e) => fmt::Display::fmt(&e, f), - Error::Hex(ref e) => write!(f, "Hexadecimal decoding error: {}", e), - Error::NotSupported(ref e) => write!(f, "Not supported: {}", e), - } - } -} - -impl std::error::Error for Error {} - -/// Vector-like object that holds the first 100 elements on the stack. If more space is needed it -/// will be allocated on the heap. -struct SmallVec { - len: usize, - stack: [T; 100], - heap: Vec, -} - -impl SmallVec { - pub fn new() -> SmallVec { - SmallVec { - len: 0, - stack: [T::default(); 100], - heap: Vec::new(), - } - } - - pub fn push(&mut self, val: T) { - if self.len < 100 { - self.stack[self.len] = val; - self.len += 1; - } else { - self.heap.push(val); - } - } - - pub fn iter(&self) -> iter::Chain, slice::Iter<'_, T>> { - // If len<100 then we just append an empty vec - self.stack[0..self.len].iter().chain(self.heap.iter()) - } - - pub fn iter_mut(&mut self) -> iter::Chain, slice::IterMut<'_, T>> { - // If len<100 then we just append an empty vec - self.stack[0..self.len].iter_mut().chain(self.heap.iter_mut()) - } -} - -static BASE58_CHARS: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; - -static BASE58_DIGITS: [Option; 128] = [ - None, - None, - None, - None, - None, - None, - None, - None, // 0-7 - None, - None, - None, - None, - None, - None, - None, - None, // 8-15 - None, - None, - None, - None, - None, - None, - None, - None, // 16-23 - None, - None, - None, - None, - None, - None, - None, - None, // 24-31 - None, - None, - None, - None, - None, - None, - None, - None, // 32-39 - None, - None, - None, - None, - None, - None, - None, - None, // 40-47 - None, - Some(0), - Some(1), - Some(2), - Some(3), - Some(4), - Some(5), - Some(6), // 48-55 - Some(7), - Some(8), - None, - None, - None, - None, - None, - None, // 56-63 - None, - Some(9), - Some(10), - Some(11), - Some(12), - Some(13), - Some(14), - Some(15), // 64-71 - Some(16), - None, - Some(17), - Some(18), - Some(19), - Some(20), - Some(21), - None, // 72-79 - Some(22), - Some(23), - Some(24), - Some(25), - Some(26), - Some(27), - Some(28), - Some(29), // 80-87 - Some(30), - Some(31), - Some(32), - None, - None, - None, - None, - None, // 88-95 - None, - Some(33), - Some(34), - Some(35), - Some(36), - Some(37), - Some(38), - Some(39), // 96-103 - Some(40), - Some(41), - Some(42), - Some(43), - None, - Some(44), - Some(45), - Some(46), // 104-111 - Some(47), - Some(48), - Some(49), - Some(50), - Some(51), - Some(52), - Some(53), - Some(54), // 112-119 - Some(55), - Some(56), - Some(57), - None, - None, - None, - None, - None, // 120-127 -]; - -/// Decode base58-encoded string into a byte vector -pub fn from(data: &str) -> Result, Error> { - decode(data) -} - -/// Decodes a base58-encoded string into a byte vector. -pub fn decode(data: &str) -> Result, Error> { - // 11/15 is just over log_256(58) - let mut scratch = vec![0u8; 1 + data.len() * 11 / 15]; - // Build in base 256 - for d58 in data.bytes() { - // Compute "X = X * 58 + next_digit" in base 256 - if d58 as usize >= BASE58_DIGITS.len() { - return Err(Error::BadByte(d58)); - } - let mut carry = match BASE58_DIGITS[d58 as usize] { - Some(d58) => d58 as u32, - None => { - return Err(Error::BadByte(d58)); - } - }; - for d256 in scratch.iter_mut().rev() { - carry += *d256 as u32 * 58; - *d256 = carry as u8; - carry /= 256; - } - assert_eq!(carry, 0); - } - - // Copy leading zeroes directly - let mut ret: Vec = data.bytes().take_while(|&x| x == BASE58_CHARS[0]).map(|_| 0).collect(); - // Copy rest of string - ret.extend(scratch.into_iter().skip_while(|&x| x == 0)); - Ok(ret) -} - -/// Decodes a base58check-encoded string into a byte vector verifying the checksum. -#[deprecated(since = "0.30.0", note = "Use base58::decode_check() instead")] -pub fn from_check(data: &str) -> Result, Error> { - decode_check(data) -} - -/// Decodes a base58check-encoded string into a byte vector verifying the checksum. -pub fn decode_check(data: &str) -> Result, Error> { - let mut ret: Vec = decode(data)?; - if ret.len() < 4 { - return Err(Error::TooShort(ret.len())); - } - let check_start = ret.len() - 4; - - let hash_check = - sha256d::Hash::hash(&ret[..check_start])[..4].try_into().expect("4 byte slice"); - let data_check = ret[check_start..].try_into().expect("4 byte slice"); - - let expected = u32::from_le_bytes(hash_check); - let actual = u32::from_le_bytes(data_check); - - if expected != actual { - return Err(Error::BadChecksum(expected, actual)); - } - - ret.truncate(check_start); - Ok(ret) -} - -fn format_iter(writer: &mut W, data: I) -> Result<(), fmt::Error> -where - I: Iterator + Clone, - W: fmt::Write, -{ - let mut ret = SmallVec::new(); - - let mut leading_zero_count = 0; - let mut leading_zeroes = true; - // Build string in little endian with 0-58 in place of characters... - for d256 in data { - let mut carry = d256 as usize; - if leading_zeroes && carry == 0 { - leading_zero_count += 1; - } else { - leading_zeroes = false; - } - - for ch in ret.iter_mut() { - let new_ch = *ch as usize * 256 + carry; - *ch = (new_ch % 58) as u8; - carry = new_ch / 58; - } - while carry > 0 { - ret.push((carry % 58) as u8); - carry /= 58; - } - } - - // ... then reverse it and convert to chars - for _ in 0..leading_zero_count { - ret.push(0); - } - - for ch in ret.iter().rev() { - writer.write_char(BASE58_CHARS[*ch as usize] as char)?; - } - - Ok(()) -} - -fn encode_iter(data: I) -> String -where - I: Iterator + Clone, -{ - let mut ret = String::new(); - format_iter(&mut ret, data).expect("writing into string shouldn't fail"); - ret -} - -/// Directly encode a slice as base58 -pub fn encode_slice(data: &[u8]) -> String { - encode_iter(data.iter().cloned()) -} - -/// Encodes `data` as a base58 string including the checksum. -/// -/// The checksum is the first four bytes of the sha256d of the data, concatenated onto the end. -pub fn encode_check(data: &[u8]) -> String { - let checksum = sha256d::Hash::hash(data); - encode_iter(data.iter().cloned().chain(checksum[0..4].iter().cloned())) -} - -/// Obtain a string with the base58check encoding of a slice -/// (Tack the first 4 256-digits of the object's Bitcoin hash onto the end.) -pub fn check_encode_slice(data: &[u8]) -> String { - let checksum = sha256d::Hash::hash(data); - encode_iter(data.iter().cloned().chain(checksum[0..4].iter().cloned())) -} - -/// Encodes `data` as base58, including the checksum, into a formatter. -/// -/// The checksum is the first four bytes of the sha256d of the data, concatenated onto the end. -#[deprecated(since = "0.30.0", note = "Use base58::encode_check_to_fmt() instead")] -pub fn check_encode_slice_to_fmt(fmt: &mut fmt::Formatter, data: &[u8]) -> fmt::Result { - encode_check_to_fmt(fmt, data) -} -/// Encodes a slice as base58, including the checksum, into a formatter. -/// -/// The checksum is the first four bytes of the sha256d of the data, concatenated onto the end. -pub fn encode_check_to_fmt(fmt: &mut fmt::Formatter, data: &[u8]) -> fmt::Result { - let checksum = sha256d::Hash::hash(data); - let iter = data.iter().cloned().chain(checksum[0..4].iter().cloned()); - format_iter(fmt, iter) -} - -#[doc(hidden)] -impl From for Error { - fn from(e: key::Error) -> Self { - match e { - key::Error::Secp256k1(e) => Error::Secp256k1(e), - key::Error::Base58(e) => e, - key::Error::InvalidKeyPrefix(_) => Error::Secp256k1(secp256k1::Error::InvalidPublicKey), - key::Error::Hex(e) => Error::Hex(e), - key::Error::InvalidHexLength(size) => Error::InvalidLength(size), - key::Error::NotSupported(e) => Error::NotSupported(e), - } - } -} - -#[cfg(test)] -mod tests { - use hashes::hex::FromHex; - - use super::*; - - #[test] - fn test_base58_encode() { - // Basics - assert_eq!(&encode_slice(&[0][..]), "1"); - assert_eq!(&encode_slice(&[1][..]), "2"); - assert_eq!(&encode_slice(&[58][..]), "21"); - assert_eq!(&encode_slice(&[13, 36][..]), "211"); - - // Leading zeroes - assert_eq!(&encode_slice(&[0, 13, 36][..]), "1211"); - assert_eq!(&encode_slice(&[0, 0, 0, 0, 13, 36][..]), "1111211"); - - // Long input (>100 bytes => has to use heap) - let res = encode_slice( - "BitcoinBitcoinBitcoinBitcoinBitcoinBitcoinBitcoinBitcoinBitcoinBit\ - coinBitcoinBitcoinBitcoinBitcoinBitcoinBitcoinBitcoinBitcoinBitcoinBitcoin" - .as_bytes(), - ); - let exp = "ZqC5ZdfpZRi7fjA8hbhX5pEE96MdH9hEaC1YouxscPtbJF16qVWksHWR4wwvx7MotFcs2ChbJqK8KJ9X\ - wZznwWn1JFDhhTmGo9v6GjAVikzCsBWZehu7bm22xL8b5zBR5AsBygYRwbFJsNwNkjpyFuDKwmsUTKvkULCvucPJrN5\ - QUdxpGakhqkZFL7RU4yT"; - assert_eq!(&res, exp); - - // Addresses - let addr = Vec::from_hex("00f8917303bfa8ef24f292e8fa1419b20460ba064d").unwrap(); - assert_eq!(&check_encode_slice(&addr[..]), "1PfJpZsjreyVrqeoAfabrRwwjQyoSQMmHH"); - } - - #[test] - fn test_base58_decode() { - // Basics - assert_eq!(from("1").ok(), Some(vec![0u8])); - assert_eq!(from("2").ok(), Some(vec![1u8])); - assert_eq!(from("21").ok(), Some(vec![58u8])); - assert_eq!(from("211").ok(), Some(vec![13u8, 36])); - - // Leading zeroes - assert_eq!(from("1211").ok(), Some(vec![0u8, 13, 36])); - assert_eq!(from("111211").ok(), Some(vec![0u8, 0, 0, 13, 36])); - - // Addresses - assert_eq!( - decode_check("1PfJpZsjreyVrqeoAfabrRwwjQyoSQMmHH").ok(), - Some(Vec::from_hex("00f8917303bfa8ef24f292e8fa1419b20460ba064d").unwrap()) - ); - // Non Base58 char. - assert_eq!(from("ยข").unwrap_err(), Error::BadByte(194)); - } - - #[test] - fn test_base58_roundtrip() { - let s = "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs"; - let v: Vec = decode_check(s).unwrap(); - assert_eq!(check_encode_slice(&v[..]), s); - assert_eq!(decode_check(&check_encode_slice(&v[..])).ok(), Some(v)); - - // Check that empty slice passes roundtrip. - assert_eq!(decode_check(&check_encode_slice(&[])), Ok(vec![])); - // Check that `len > 4` is enforced. - assert_eq!(decode_check(&encode_slice(&[1, 2, 3])), Err(Error::TooShort(3))); - } -} diff --git a/dash/src/blockdata/transaction/special_transaction/provider_registration.rs b/dash/src/blockdata/transaction/special_transaction/provider_registration.rs index 3c885245c..1da71e53b 100644 --- a/dash/src/blockdata/transaction/special_transaction/provider_registration.rs +++ b/dash/src/blockdata/transaction/special_transaction/provider_registration.rs @@ -299,8 +299,10 @@ impl Decodable for ProviderRegistrationPayload { #[cfg(test)] mod tests { use super::*; - use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; + use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; + use base64::Engine as _; + use base64::engine::general_purpose::STANDARD as BASE64; use hashes::Hash; use crate::bls_sig_utils::BLSPublicKey; @@ -313,8 +315,6 @@ mod tests { }; use crate::{OutPoint, PubkeyHash, ScriptBuf, Txid}; - use std::net::IpAddr; - use crate::Network; use crate::PrivateKey; use crate::Transaction; @@ -459,7 +459,7 @@ mod tests { collateral_private_key.to_bytes().as_slice(), ) .expect("expected to sign message digest"); - let base64_signature = base64::encode(signature.as_slice()); + let base64_signature = BASE64.encode(signature.as_slice()); assert_eq!( expected_base64_signature, base64_signature, @@ -635,11 +635,11 @@ mod tests { let expected_base64_signature = "H7N+ScH/K4BXcTk5pVE+bnEacc/y5RfmIk33JO11Cu8bf5rZ7GErSnJQIy4eQA2nGKlQHh2aVWVSbksf9owCh2M="; let signature = sign_hash( - base64::decode(&expected_base64_signature).expect("expected valid base64").as_slice(), + BASE64.decode(expected_base64_signature).expect("expected valid base64").as_slice(), collateral_private_key.to_bytes().as_slice(), ) .expect("expected to sign message digest"); - let base64_signature = base64::encode(signature.as_slice()); + let base64_signature = BASE64.encode(signature.as_slice()); assert_eq!( expected_base64_signature, base64_signature, diff --git a/dash/src/crypto/key.rs b/dash/src/crypto/key.rs index 20e8b378c..aeb308118 100644 --- a/dash/src/crypto/key.rs +++ b/dash/src/crypto/key.rs @@ -36,7 +36,7 @@ use crate::taproot::{TapNodeHash, TapTweakHash}; use crate::{base58, io}; /// A key-related error. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] #[non_exhaustive] pub enum Error { /// Base58 encoding error @@ -45,6 +45,10 @@ pub enum Error { Secp256k1(secp256k1::Error), /// Invalid key prefix error InvalidKeyPrefix(u8), + /// The WIF or extended key version byte was not one we recognise. + InvalidAddressVersion(u8), + /// The base58 decoded correctly but the payload was the wrong length. + InvalidBase58PayloadLength(usize), /// Hex decoding error Hex(hex::Error), /// `PublicKey` hex should be 66 or 130 digits long. @@ -58,6 +62,12 @@ impl fmt::Display for Error { 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::Hex(e) => write_err!(f, "key hex decoding error"; e), Error::InvalidHexLength(got) => { @@ -78,7 +88,10 @@ impl std::error::Error for Error { Base58(e) => Some(e), Secp256k1(e) => Some(e), Hex(e) => Some(e), - InvalidKeyPrefix(_) | InvalidHexLength(_) => None, + InvalidAddressVersion(_) + | InvalidBase58PayloadLength(_) + | InvalidKeyPrefix(_) + | InvalidHexLength(_) => None, NotSupported(_) => None, } } @@ -274,7 +287,7 @@ impl PublicKey { (false, secp256k1::PublicKey::from_byte_array_uncompressed(data)?) } len => { - return Err(base58::Error::InvalidLength(len).into()); + return Err(Error::InvalidBase58PayloadLength(len)); } }; @@ -413,7 +426,7 @@ impl PrivateKey { 33 => false, 34 => true, _ => { - return Err(Error::Base58(base58::Error::InvalidLength(data.len()))); + return Err(Error::InvalidBase58PayloadLength(data.len())); } }; @@ -421,13 +434,13 @@ impl PrivateKey { 204 => Network::Mainnet, 239 => Network::Testnet, x => { - return Err(Error::Base58(base58::Error::InvalidAddressVersion(x))); + return Err(Error::InvalidAddressVersion(x)); } }; let secret = data[1..] .first_chunk::<{ constants::SECRET_KEY_SIZE }>() - .ok_or(Error::Base58(base58::Error::InvalidLength(data.len())))?; + .ok_or(Error::InvalidBase58PayloadLength(data.len()))?; Ok(PrivateKey { compressed, diff --git a/dash/src/lib.rs b/dash/src/lib.rs index c9ef51379..9939050ca 100644 --- a/dash/src/lib.rs +++ b/dash/src/lib.rs @@ -92,7 +92,7 @@ pub mod serde_utils; pub mod network; pub mod address; pub mod amount; -pub mod base58; +pub use dashcore_crypto::base58; pub mod bip152; pub mod bip158; pub mod blockdata; diff --git a/dash/src/sign_message.rs b/dash/src/sign_message.rs index 91115f10e..eb676afd8 100644 --- a/dash/src/sign_message.rs +++ b/dash/src/sign_message.rs @@ -34,6 +34,10 @@ pub const DASH_SIGNED_MSG_PREFIX: &[u8] = b"\x19DarkCoin Signed Message:\n"; 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; @@ -182,14 +186,14 @@ mod message_signing { /// 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)?; + let bytes = BASE64.decode(s).map_err(|_| MessageSignatureError::InvalidBase64)?; MessageSignature::from_slice(&bytes) } /// Convert to base64 encoding. #[cfg(feature = "base64")] pub fn to_base64(self) -> String { - base64::encode(&self.serialize()[..]) + BASE64.encode(&self.serialize()[..]) } } @@ -198,11 +202,7 @@ mod message_signing { 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::with_config(&bytes[..], base64::STANDARD) - ) + write!(f, "{}", base64::display::Base64Display::new(&bytes[..], &BASE64)) } } @@ -228,6 +228,10 @@ 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() { @@ -295,7 +299,7 @@ mod tests { let signature = super::MessageSignature::from_base64(signature_base64).expect("message signature"); - let pubkey = PublicKey::from_slice(&base64::decode(pubkey_base64).expect("base64 string")) + let pubkey = PublicKey::from_slice(&BASE64.decode(pubkey_base64).expect("base64 string")) .expect("pubkey slice"); let p2pkh = Address::p2pkh(&pubkey, Network::Mainnet); diff --git a/key-wallet/Cargo.toml b/key-wallet/Cargo.toml index a2974752a..7aec26b90 100644 --- a/key-wallet/Cargo.toml +++ b/key-wallet/Cargo.toml @@ -38,7 +38,7 @@ secp256k1 = { version = "0.33.1", default-features = false, features = ["recover 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 = { version = "1.0", default-features = false, features = ["derive"], optional = true } -base58ck = { version = "0.1.0", default-features = false } +base58 = { package = "base58ck", version = "0.1.0", default-features = false } bitflags = { version = "2.6", default-features = false } getrandom = { version = "0.4", optional = true } # BIP38 dependencies (optional) diff --git a/key-wallet/src/bip32.rs b/key-wallet/src/bip32.rs index 96df27adf..4ed7172d0 100644 --- a/key-wallet/src/bip32.rs +++ b/key-wallet/src/bip32.rs @@ -43,7 +43,6 @@ use crate::dip9::{ IDENTITY_INVITATION_PATH_TESTNET, IDENTITY_REGISTRATION_PATH_MAINNET, IDENTITY_REGISTRATION_PATH_TESTNET, IDENTITY_TOPUP_PATH_MAINNET, IDENTITY_TOPUP_PATH_TESTNET, }; -use base58ck; #[cfg(feature = "bincode")] use bincode_derive::{Decode, Encode}; use dashcore::Network; @@ -1549,7 +1548,7 @@ pub enum Error { /// Encoded extended key data has wrong length WrongExtendedKeyLength(usize), /// Base58 encoding error - Base58(base58ck::Error), + Base58(base58::Error), /// Hexadecimal decoding error Hex(dashcore_hashes::hex::Error), /// `PublicKey` hex should be 66 or 130 digits long. @@ -1602,8 +1601,8 @@ impl From for Error { } } -impl From for Error { - fn from(err: base58ck::Error) -> Self { +impl From for Error { + fn from(err: base58::Error) -> Self { Error::Base58(err) } } @@ -2170,7 +2169,7 @@ impl ExtendedPubKey { impl fmt::Display for ExtendedPrivKey { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - fmt.write_str(&base58ck::encode_check(&self.encode()[..])) + fmt.write_str(&base58::encode_check(&self.encode()[..])) } } @@ -2178,14 +2177,14 @@ impl FromStr for ExtendedPrivKey { type Err = Error; fn from_str(inp: &str) -> Result { - let data = base58ck::decode_check(inp)?; + let data = base58::decode_check(inp)?; ExtendedPrivKey::decode(&data) } } impl fmt::Display for ExtendedPubKey { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - fmt.write_str(&base58ck::encode_check(&self.encode()[..])) + fmt.write_str(&base58::encode_check(&self.encode()[..])) } } @@ -2193,7 +2192,7 @@ impl FromStr for ExtendedPubKey { type Err = Error; fn from_str(inp: &str) -> Result { - let data = base58ck::decode_check(inp)?; + let data = base58::decode_check(inp)?; ExtendedPubKey::decode(&data) } } From 72e14d8591b0811b28916d31dba8063633f225b2 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:57:37 +0530 Subject: [PATCH 03/11] refactor(dash)!: move the taproot tweak off the key module `git diff --color-moved=dimmed-zebra --color-moved-ws=ignore-all-space` --- dash/src/address.rs | 17 +++--- dash/src/blockdata/script/owned.rs | 4 +- dash/src/crypto/key.rs | 86 ---------------------------- dash/src/taproot.rs | 91 +++++++++++++++++++++++++++++- 4 files changed, 100 insertions(+), 98 deletions(-) diff --git a/dash/src/address.rs b/dash/src/address.rs index 26fd47001..08f2b6b7f 100644 --- a/dash/src/address.rs +++ b/dash/src/address.rs @@ -45,6 +45,13 @@ use core::fmt; use core::marker::PhantomData; use core::str::FromStr; +use bech32; +use hashes::{Hash, HashEngine, sha256}; +use internals::write_err; +use secp256k1::XOnlyPublicKey; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + use crate::base58; use crate::blockdata::constants::{ MAX_SCRIPT_ELEMENT_SIZE, PUBKEY_ADDRESS_PREFIX_MAIN, PUBKEY_ADDRESS_PREFIX_TEST, @@ -55,18 +62,12 @@ use crate::blockdata::opcodes::all::*; use crate::blockdata::script::{ self, Instruction, PushBytes, PushBytesBuf, PushBytesErrorReport, Script, ScriptBuf, }; -use crate::crypto::key::{PublicKey, TapTweak, TweakedPublicKey, UntweakedPublicKey}; +use crate::crypto::key::{PublicKey, TweakedPublicKey, UntweakedPublicKey}; use crate::error::ParseIntError; use crate::hash_types::{PubkeyHash, ScriptHash}; use crate::network::constants::Network; use crate::prelude::*; -use crate::taproot::TapNodeHash; -use bech32; -use hashes::{Hash, HashEngine, sha256}; -use internals::write_err; -use secp256k1::XOnlyPublicKey; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; +use crate::taproot::{TapNodeHash, TapTweak}; /// Address error. #[derive(Debug, PartialEq, Eq, Clone)] diff --git a/dash/src/blockdata/script/owned.rs b/dash/src/blockdata/script/owned.rs index 9ba6c3753..4df712ecf 100644 --- a/dash/src/blockdata/script/owned.rs +++ b/dash/src/blockdata/script/owned.rs @@ -13,9 +13,9 @@ use crate::blockdata::opcodes::all::*; use crate::blockdata::opcodes::{self}; use crate::blockdata::script::{Builder, Instruction, PushBytes, Script, opcode_to_verify}; use crate::hash_types::{PubkeyHash, ScriptHash, WPubkeyHash, WScriptHash}; -use crate::key::{PublicKey, TapTweak, TweakedPublicKey, UntweakedPublicKey}; +use crate::key::{PublicKey, TweakedPublicKey, UntweakedPublicKey}; use crate::prelude::*; -use crate::taproot::TapNodeHash; +use crate::taproot::{TapNodeHash, TapTweak}; /// An owned, growable script. /// diff --git a/dash/src/crypto/key.rs b/dash/src/crypto/key.rs index aeb308118..265c60576 100644 --- a/dash/src/crypto/key.rs +++ b/dash/src/crypto/key.rs @@ -32,7 +32,6 @@ pub use secp256k1::{self, Keypair, Parity, Secp256k1, Verification, XOnlyPublicK use crate::hash_types::{PubkeyHash, WPubkeyHash}; use crate::network::constants::Network; use crate::prelude::*; -use crate::taproot::{TapNodeHash, TapTweakHash}; use crate::{base58, io}; /// A key-related error. @@ -622,91 +621,6 @@ pub type UntweakedKeyPair = Keypair; #[cfg_attr(feature = "serde", serde(transparent))] pub struct TweakedKeyPair(Keypair); -/// A trait for tweaking BIP340 key types (x-only public keys and key pairs). -pub trait TapTweak { - /// Tweaked key type with optional auxiliary information - type TweakedAux; - /// Tweaked key type - type TweakedKey; - - /// Tweaks an untweaked key with corresponding public key value and optional script tree merkle - /// root. For the `KeyPair` type this also tweaks the private key in the pair. - /// - /// This is done by using the equation Q = P + H(P|c)G, where - /// * Q is the tweaked public key - /// * P is the internal public key - /// * H is the hash function - /// * c is the commitment data - /// * G is the generator point - /// - /// # Returns - /// The tweaked key and its parity. - fn tap_tweak(self, merkle_root: Option) -> Self::TweakedAux; - - /// Directly converts an [`UntweakedPublicKey`] to a [`TweakedPublicKey`] - /// - /// 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. - fn dangerous_assume_tweaked(self) -> Self::TweakedKey; -} - -impl TapTweak for UntweakedPublicKey { - type TweakedAux = (TweakedPublicKey, Parity); - type TweakedKey = TweakedPublicKey; - - /// Tweaks an untweaked public key with corresponding public key value and optional script tree - /// merkle root. - /// - /// This is done by using the equation Q = P + H(P|c)G, where - /// * Q is the tweaked public key - /// * P is the internal public key - /// * H is the hash function - /// * c is the commitment data - /// * G is the generator point - /// - /// # Returns - /// 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"); - - debug_assert!(self.tweak_add_check(&output_key, parity, tweak)); - (TweakedPublicKey(output_key), parity) - } - - fn dangerous_assume_tweaked(self) -> TweakedPublicKey { - TweakedPublicKey(self) - } -} - -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. - /// - /// This is done by tweaking private key within the pair using the equation q = p + H(P|c), where - /// * q is the tweaked private key - /// * p is the internal private key - /// * H is the hash function - /// * c is the commitment data - /// The public key is generated from a private key by multiplying with generator point, Q = qG. - /// - /// # 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(tweaked) - } - - fn dangerous_assume_tweaked(self) -> TweakedKeyPair { - TweakedKeyPair(self) - } -} - impl TweakedPublicKey { /// Returns the [`TweakedPublicKey`] for `keypair`. #[inline] diff --git a/dash/src/taproot.rs b/dash/src/taproot.rs index 652ea1fd2..b9a2b9a71 100644 --- a/dash/src/taproot.rs +++ b/dash/src/taproot.rs @@ -12,10 +12,12 @@ use core::iter::FusedIterator; use hashes::{Hash, HashEngine, sha256t_hash_newtype}; use internals::write_err; -use secp256k1::{self, Scalar}; +use secp256k1::{self, Parity, Scalar}; use crate::consensus::Encodable; -use crate::crypto::key::{TapTweak, TweakedPublicKey, UntweakedPublicKey, XOnlyPublicKey}; +use crate::crypto::key::{ + TweakedKeyPair, TweakedPublicKey, UntweakedKeyPair, UntweakedPublicKey, XOnlyPublicKey, +}; // Re-export these so downstream only has to use one `taproot` module. pub use crate::crypto::taproot::{Error, Signature}; use crate::prelude::*; @@ -1674,6 +1676,91 @@ impl std::error::Error for TaprootError { } } +/// A trait for tweaking BIP340 key types (x-only public keys and key pairs). +pub trait TapTweak { + /// Tweaked key type with optional auxiliary information + type TweakedAux; + /// Tweaked key type + type TweakedKey; + + /// Tweaks an untweaked key with corresponding public key value and optional script tree merkle + /// root. For the `KeyPair` type this also tweaks the private key in the pair. + /// + /// This is done by using the equation Q = P + H(P|c)G, where + /// * Q is the tweaked public key + /// * P is the internal public key + /// * H is the hash function + /// * c is the commitment data + /// * G is the generator point + /// + /// # Returns + /// The tweaked key and its parity. + fn tap_tweak(self, merkle_root: Option) -> Self::TweakedAux; + + /// Directly converts an [`UntweakedPublicKey`] to a [`TweakedPublicKey`] + /// + /// 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. + fn dangerous_assume_tweaked(self) -> Self::TweakedKey; +} + +impl TapTweak for UntweakedPublicKey { + type TweakedAux = (TweakedPublicKey, Parity); + type TweakedKey = TweakedPublicKey; + + /// Tweaks an untweaked public key with corresponding public key value and optional script tree + /// merkle root. + /// + /// This is done by using the equation Q = P + H(P|c)G, where + /// * Q is the tweaked public key + /// * P is the internal public key + /// * H is the hash function + /// * c is the commitment data + /// * G is the generator point + /// + /// # Returns + /// 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"); + + debug_assert!(self.tweak_add_check(&output_key, parity, tweak)); + (TweakedPublicKey::dangerous_assume_tweaked(output_key), parity) + } + + fn dangerous_assume_tweaked(self) -> TweakedPublicKey { + TweakedPublicKey::dangerous_assume_tweaked(self) + } +} + +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. + /// + /// This is done by tweaking private key within the pair using the equation q = p + H(P|c), where + /// * q is the tweaked private key + /// * p is the internal private key + /// * H is the hash function + /// * c is the commitment data + /// The public key is generated from a private key by multiplying with generator point, Q = qG. + /// + /// # 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 dangerous_assume_tweaked(self) -> TweakedKeyPair { + TweakedKeyPair::dangerous_assume_tweaked(self) + } +} + #[cfg(test)] mod test { use core::str::FromStr; From 4c0c03d25fd4a6ddbceac662894efb4c4ee8b192 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 16 Sep 2026 15:58:31 +0530 Subject: [PATCH 04/11] refactor(dash): move the script conversion off the ECDSA signature `git diff --color-moved=dimmed-zebra --color-moved-ws=ignore-all-space` --- dash/src/blockdata/script/push_bytes.rs | 8 ++++++++ dash/src/crypto/ecdsa.rs | 8 -------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dash/src/blockdata/script/push_bytes.rs b/dash/src/blockdata/script/push_bytes.rs index d3fa7cf62..de919b740 100644 --- a/dash/src/blockdata/script/push_bytes.rs +++ b/dash/src/blockdata/script/push_bytes.rs @@ -474,3 +474,11 @@ mod error { } crate::error::impl_std_error!(PushBytesError); + +impl AsRef for crate::crypto::ecdsa::SerializedSignature { + #[inline] + fn as_ref(&self) -> &PushBytes { + let bytes: &[u8] = self; + bytes.try_into().expect("a DER signature is at most 73 bytes") + } +} diff --git a/dash/src/crypto/ecdsa.rs b/dash/src/crypto/ecdsa.rs index 2e8d776c3..8010a5019 100644 --- a/dash/src/crypto/ecdsa.rs +++ b/dash/src/crypto/ecdsa.rs @@ -14,7 +14,6 @@ use internals::write_err; use secp256k1; use crate::prelude::*; -use crate::script::PushBytes; use crate::sighash::{EcdsaSighashType, NonStandardSighashType}; const MAX_SIG_LEN: usize = 73; @@ -145,13 +144,6 @@ impl AsMut<[u8]> for SerializedSignature { } } -impl AsRef for SerializedSignature { - #[inline] - fn as_ref(&self) -> &PushBytes { - &<&PushBytes>::from(&self.data)[..self.len()] - } -} - impl core::borrow::Borrow<[u8]> for SerializedSignature { #[inline] fn borrow(&self) -> &[u8] { From 5423e4d4fe3b2a4df38ee6658d151888e16d1259 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:01:50 +0530 Subject: [PATCH 05/11] refactor(dash): give the taproot sighash type its own parse error --- dash/src/crypto/sighash.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/dash/src/crypto/sighash.rs b/dash/src/crypto/sighash.rs index 503bc098e..f05726757 100644 --- a/dash/src/crypto/sighash.rs +++ b/dash/src/crypto/sighash.rs @@ -534,7 +534,13 @@ impl TapSighashType { } /// Constructs a [`TapSighashType`] from a raw `u8`. - pub fn from_consensus_u8(hash_ty: u8) -> Result { + /// + /// # Errors + /// + /// Returns [`InvalidSighashTypeError`] when `hash_ty` names no sighash + /// type. The error is dedicated rather than the module's [`Error`] so + /// that naming a sighash type stays independent of computing one. + pub fn from_consensus_u8(hash_ty: u8) -> Result { use TapSighashType::*; Ok(match hash_ty { @@ -545,11 +551,29 @@ impl TapSighashType { 0x81 => AllPlusAnyoneCanPay, 0x82 => NonePlusAnyoneCanPay, 0x83 => SinglePlusAnyoneCanPay, - x => return Err(Error::InvalidSighashType(x as u32)), + x => return Err(InvalidSighashTypeError(x as u32)), }) } } +/// Integer is not a consensus valid sighash type. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidSighashTypeError(pub u32); + +impl fmt::Display for InvalidSighashTypeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "invalid sighash type {}", self.0) + } +} + +impl_std_error!(InvalidSighashTypeError); + +impl From for Error { + fn from(e: InvalidSighashTypeError) -> Self { + Error::InvalidSighashType(e.0) + } +} + /// This type is consensus valid but an input including it would prevent the transaction from /// being relayed on today's Bitcoin network. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] From 60ba824e41482a4c77cd5606b02272f73e9062f9 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:04:48 +0530 Subject: [PATCH 06/11] feat(crypto): migrate the sighash types to `dashcore-crypto` `git diff --color-moved=dimmed-zebra --color-moved-ws=ignore-all-space` --- crypto/Cargo.toml | 5 +- crypto/src/lib.rs | 21 +++ crypto/src/serde_utils.rs | 60 +++++++ crypto/src/sighash.rs | 340 +++++++++++++++++++++++++++++++++++++ dash/src/crypto/sighash.rs | 325 +---------------------------------- 5 files changed, 432 insertions(+), 319 deletions(-) create mode 100644 crypto/src/serde_utils.rs create mode 100644 crypto/src/sighash.rs diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index 08b9be553..55d06e1f8 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -6,7 +6,7 @@ license = "CC0-1.0" repository = "https://github.com/dashpay/rust-dashcore/" description = "Cryptographic primitives shared by rust-dashcore crates." categories = ["cryptography::cryptocurrencies"] -keywords = ["crypto", "dash", "bls"] +keywords = ["crypto", "dash", "bls", "secp256k1"] edition = "2021" [features] @@ -14,7 +14,7 @@ default = [] bincode = ["dep:bincode", "dep:bincode_derive", "dashcore_hashes/bincode"] bls = ["dep:dash-pkc", "dep:tracing"] -serde = ["dep:serde", "dash-types/serde", "dashcore_hashes/serde"] +serde = ["dep:serde", "dash-types/serde", "dashcore_hashes/serde", "secp256k1/serde"] [dependencies] base58ck = { version = "0.1.0" } @@ -25,6 +25,7 @@ dash-types = { version = "=0.1.0-beta", default-features = false, features = ["c dashcore_hashes = { path = "../hashes" } hex = { version = "0.4" } internals = { path = "../internals", package = "dashcore-private" } +secp256k1 = { version = "0.33.1" } serde = { version = "1.0.219", default-features = false, features = [ "derive", "alloc" ], optional = true } thiserror = "2" tracing = { version = "0.1", optional = true } diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index b77b8ab26..2e431a7c4 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -15,7 +15,28 @@ pub extern crate base58ck as base58; #[cfg(feature = "bls")] pub extern crate dash_pkc; pub extern crate dashcore_hashes as hashes; +pub extern crate secp256k1; #[cfg(feature = "serde")] pub extern crate serde; +#[cfg(feature = "serde")] +#[macro_use] +pub(crate) mod serde_utils; + pub mod bls; +pub mod sighash; + +/// Implements `std::error::Error` for a type whose `Display` carries the message. +macro_rules! impl_std_error { + ($type:ty) => { + impl std::error::Error for $type {} + }; + ($type:ty, $field:ident) => { + impl std::error::Error for $type { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.$field) + } + } + }; +} +pub(crate) use impl_std_error; diff --git a/crypto/src/serde_utils.rs b/crypto/src/serde_utils.rs new file mode 100644 index 000000000..41e5b8fe9 --- /dev/null +++ b/crypto/src/serde_utils.rs @@ -0,0 +1,60 @@ +// +// This file is a part of rust-dashcore. Contains portions from rust-bitcoin. +// SPDX-License-Identifier: CC0-1.0 +// See the accompanying file LICENSE or https://creativecommons.org/publicdomain/zero/1.0 +// + +//! Serde helpers. + +macro_rules! serde_string_serialize_impl { + ($name:ty, $expecting:literal) => { + impl $crate::serde::Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: $crate::serde::Serializer, + { + serializer.collect_str(&self) + } + } + }; +} + +macro_rules! serde_string_deserialize_impl { + ($name:ty, $expecting:literal) => { + impl<'de> $crate::serde::Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> Result<$name, D::Error> + where + D: $crate::serde::de::Deserializer<'de>, + { + use core::fmt::{self, Formatter}; + + struct Visitor; + impl<'de> $crate::serde::de::Visitor<'de> for Visitor { + type Value = $name; + + fn expecting(&self, f: &mut Formatter) -> fmt::Result { + f.write_str($expecting) + } + + fn visit_str(self, v: &str) -> Result + where + E: $crate::serde::de::Error, + { + v.parse::<$name>().map_err(E::custom) + } + } + + deserializer.deserialize_str(Visitor) + } + } + }; +} + +macro_rules! serde_string_impl { + ($name:ty, $expecting:literal) => { + $crate::serde_utils::serde_string_deserialize_impl!($name, $expecting); + $crate::serde_utils::serde_string_serialize_impl!($name, $expecting); + }; +} + +pub(crate) use {serde_string_deserialize_impl, serde_string_impl, serde_string_serialize_impl}; diff --git a/crypto/src/sighash.rs b/crypto/src/sighash.rs new file mode 100644 index 000000000..0bbb45ef1 --- /dev/null +++ b/crypto/src/sighash.rs @@ -0,0 +1,340 @@ +// +// This file is a part of rust-dashcore. Contains portions from rust-bitcoin. +// SPDX-License-Identifier: CC0-1.0 +// See the accompanying file LICENSE or https://creativecommons.org/publicdomain/zero/1.0 +// + +//! Signature hash types. + +use core::{fmt, str}; +use std::string::String; + +use hashes::{hash_newtype, sha256d, sha256t_hash_newtype, Hash as _}; + +use crate::impl_std_error; + +hash_newtype! { + /// Hash of a transaction according to the legacy signature algorithm. + #[hash_newtype(forward)] + pub struct LegacySighash(sha256d::Hash); + + /// Hash of a transaction according to the segwit version 0 signature algorithm. + #[hash_newtype(forward)] + pub struct SegwitV0Sighash(sha256d::Hash); +} + +sha256t_hash_newtype! { + pub struct TapSighashTag = hash_str("TapSighash"); + + /// Taproot-tagged hash with tag \"TapSighash\". + /// + /// This hash type is used for computing taproot signature hash." + #[hash_newtype(forward)] + pub struct TapSighash(_); +} + +/// Hashtype of an input's signature, encoded in the last byte of the signature. +/// Fixed values so they can be cast as integer types for encoding. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum TapSighashType { + /// 0x0: Used when not explicitly specified, defaults to [`TapSighashType::All`] + Default = 0x00, + /// 0x1: Sign all outputs. + All = 0x01, + /// 0x2: Sign no outputs --- anyone can choose the destination. + None = 0x02, + /// 0x3: Sign the output whose index matches this input's index. If none exists, + /// sign the hash `0000000000000000000000000000000000000000000000000000000000000001`. + /// (This rule is probably an unintentional C++ism, but it's consensus so we have + /// to follow it.) + Single = 0x03, + /// 0x81: Sign all outputs but only this input. + AllPlusAnyoneCanPay = 0x81, + /// 0x82: Sign no outputs and only this input. + NonePlusAnyoneCanPay = 0x82, + /// 0x83: Sign one output and only this input (see `Single` for what "one output" means). + SinglePlusAnyoneCanPay = 0x83, +} +#[cfg(feature = "serde")] +crate::serde_utils::serde_string_impl!(TapSighashType, "a TapSighashType data"); + +impl fmt::Display for TapSighashType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use TapSighashType::*; + + let s = match self { + Default => "SIGHASH_DEFAULT", + All => "SIGHASH_ALL", + None => "SIGHASH_NONE", + Single => "SIGHASH_SINGLE", + AllPlusAnyoneCanPay => "SIGHASH_ALL|SIGHASH_ANYONECANPAY", + NonePlusAnyoneCanPay => "SIGHASH_NONE|SIGHASH_ANYONECANPAY", + SinglePlusAnyoneCanPay => "SIGHASH_SINGLE|SIGHASH_ANYONECANPAY", + }; + f.write_str(s) + } +} + +impl str::FromStr for TapSighashType { + type Err = SighashTypeParseError; + + fn from_str(s: &str) -> Result { + use TapSighashType::*; + + match s { + "SIGHASH_DEFAULT" => Ok(Default), + "SIGHASH_ALL" => Ok(All), + "SIGHASH_NONE" => Ok(None), + "SIGHASH_SINGLE" => Ok(Single), + "SIGHASH_ALL|SIGHASH_ANYONECANPAY" => Ok(AllPlusAnyoneCanPay), + "SIGHASH_NONE|SIGHASH_ANYONECANPAY" => Ok(NonePlusAnyoneCanPay), + "SIGHASH_SINGLE|SIGHASH_ANYONECANPAY" => Ok(SinglePlusAnyoneCanPay), + _ => Err(SighashTypeParseError { + unrecognized: s.to_owned(), + }), + } + } +} + +/// Hashtype of an input's signature, encoded in the last byte of the signature. +/// +/// Fixed values so they can be cast as integer types for encoding (see also +/// [`TapSighashType`]). +#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)] +pub enum EcdsaSighashType { + /// 0x1: Sign all outputs. + All = 0x01, + /// 0x2: Sign no outputs --- anyone can choose the destination. + None = 0x02, + /// 0x3: Sign the output whose index matches this input's index. If none exists, + /// sign the hash `0000000000000000000000000000000000000000000000000000000000000001`. + /// (This rule is probably an unintentional C++ism, but it's consensus so we have + /// to follow it.) + Single = 0x03, + /// 0x81: Sign all outputs but only this input. + AllPlusAnyoneCanPay = 0x81, + /// 0x82: Sign no outputs and only this input. + NonePlusAnyoneCanPay = 0x82, + /// 0x83: Sign one output and only this input (see `Single` for what "one output" means). + SinglePlusAnyoneCanPay = 0x83, +} +#[cfg(feature = "serde")] +crate::serde_utils::serde_string_impl!(EcdsaSighashType, "a EcdsaSighashType data"); + +impl fmt::Display for EcdsaSighashType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use EcdsaSighashType::*; + + let s = match self { + All => "SIGHASH_ALL", + None => "SIGHASH_NONE", + Single => "SIGHASH_SINGLE", + AllPlusAnyoneCanPay => "SIGHASH_ALL|SIGHASH_ANYONECANPAY", + NonePlusAnyoneCanPay => "SIGHASH_NONE|SIGHASH_ANYONECANPAY", + SinglePlusAnyoneCanPay => "SIGHASH_SINGLE|SIGHASH_ANYONECANPAY", + }; + f.write_str(s) + } +} + +impl str::FromStr for EcdsaSighashType { + type Err = SighashTypeParseError; + + fn from_str(s: &str) -> Result { + use EcdsaSighashType::*; + + match s { + "SIGHASH_ALL" => Ok(All), + "SIGHASH_NONE" => Ok(None), + "SIGHASH_SINGLE" => Ok(Single), + "SIGHASH_ALL|SIGHASH_ANYONECANPAY" => Ok(AllPlusAnyoneCanPay), + "SIGHASH_NONE|SIGHASH_ANYONECANPAY" => Ok(NonePlusAnyoneCanPay), + "SIGHASH_SINGLE|SIGHASH_ANYONECANPAY" => Ok(SinglePlusAnyoneCanPay), + _ => Err(SighashTypeParseError { + unrecognized: s.to_owned(), + }), + } + } +} + +/// Splits a sighash flag into the "real" flag and the `SIGHASH_ANYONECANPAY` bit. +/// +/// A trait rather than an inherent method so that `dashcore`, which computes +/// the signature hashes, can reach it without the split becoming part of the +/// sighash types' own surface. +pub trait SplitAnyoneCanPay { + /// Splits the flag into the "real" sighash flag and the ANYONECANPAY boolean. + fn split_anyonecanpay_flag(self) -> (Self, bool) + where + Self: Sized; +} + +impl SplitAnyoneCanPay for EcdsaSighashType { + fn split_anyonecanpay_flag(self) -> (EcdsaSighashType, bool) { + use EcdsaSighashType::*; + + match self { + All => (All, false), + None => (None, false), + Single => (Single, false), + AllPlusAnyoneCanPay => (All, true), + NonePlusAnyoneCanPay => (None, true), + SinglePlusAnyoneCanPay => (Single, true), + } + } +} + +impl EcdsaSighashType { + /// Creates a [`EcdsaSighashType`] from a raw `u32`. + /// + /// **Note**: this replicates consensus behaviour, for current standardness rules correctness + /// you probably want [`Self::from_standard`]. + /// + /// This might cause unexpected behavior because it does not roundtrip. That is, + /// `EcdsaSighashType::from_consensus(n) as u32 != n` for non-standard values of `n`. While + /// verifying signatures, the user should retain the `n` and use it compute the signature hash + /// message. + pub fn from_consensus(n: u32) -> EcdsaSighashType { + use EcdsaSighashType::*; + + // In Bitcoin Core, the SignatureHash function will mask the (int32) value with + // 0x1f to (apparently) deactivate ACP when checking for SINGLE and NONE bits. + // We however want to be matching also against on ACP-masked ALL, SINGLE, and NONE. + // So here we re-activate ACP. + let mask = 0x1f | 0x80; + match n & mask { + // "real" sighashes + 0x01 => All, + 0x02 => None, + 0x03 => Single, + 0x81 => AllPlusAnyoneCanPay, + 0x82 => NonePlusAnyoneCanPay, + 0x83 => SinglePlusAnyoneCanPay, + // catchalls + x if x & 0x80 == 0x80 => AllPlusAnyoneCanPay, + _ => All, + } + } + + /// Creates a [`EcdsaSighashType`] from a raw `u32`. + /// + /// # Errors + /// + /// If `n` is a non-standard sighash value. + pub fn from_standard(n: u32) -> Result { + use EcdsaSighashType::*; + + match n { + // Standard sighashes, see https://github.com/bitcoin/bitcoin/blob/b805dbb0b9c90dadef0424e5b3bf86ac308e103e/src/script/interpreter.cpp#L189-L198 + 0x01 => Ok(All), + 0x02 => Ok(None), + 0x03 => Ok(Single), + 0x81 => Ok(AllPlusAnyoneCanPay), + 0x82 => Ok(NonePlusAnyoneCanPay), + 0x83 => Ok(SinglePlusAnyoneCanPay), + non_standard => Err(NonStandardSighashType(non_standard)), + } + } + + /// Converts [`EcdsaSighashType`] to a `u32` sighash flag. + /// + /// The returned value is guaranteed to be a valid according to standardness rules. + pub fn to_u32(self) -> u32 { + self as u32 + } +} + +impl From for TapSighashType { + fn from(s: EcdsaSighashType) -> Self { + use TapSighashType::*; + + match s { + EcdsaSighashType::All => All, + EcdsaSighashType::None => None, + EcdsaSighashType::Single => Single, + EcdsaSighashType::AllPlusAnyoneCanPay => AllPlusAnyoneCanPay, + EcdsaSighashType::NonePlusAnyoneCanPay => NonePlusAnyoneCanPay, + EcdsaSighashType::SinglePlusAnyoneCanPay => SinglePlusAnyoneCanPay, + } + } +} + +impl SplitAnyoneCanPay for TapSighashType { + fn split_anyonecanpay_flag(self) -> (TapSighashType, bool) { + use TapSighashType::*; + + match self { + Default => (Default, false), + All => (All, false), + None => (None, false), + Single => (Single, false), + AllPlusAnyoneCanPay => (All, true), + NonePlusAnyoneCanPay => (None, true), + SinglePlusAnyoneCanPay => (Single, true), + } + } +} + +impl TapSighashType { + /// Constructs a [`TapSighashType`] from a raw `u8`. + /// + /// # Errors + /// + /// Returns [`InvalidSighashTypeError`] when `hash_ty` names no sighash type. + pub fn from_consensus_u8(hash_ty: u8) -> Result { + use TapSighashType::*; + + Ok(match hash_ty { + 0x00 => Default, + 0x01 => All, + 0x02 => None, + 0x03 => Single, + 0x81 => AllPlusAnyoneCanPay, + 0x82 => NonePlusAnyoneCanPay, + 0x83 => SinglePlusAnyoneCanPay, + x => return Err(InvalidSighashTypeError(x as u32)), + }) + } +} + +/// Integer is not a consensus valid sighash type. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct InvalidSighashTypeError(pub u32); + +impl fmt::Display for InvalidSighashTypeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "invalid sighash type {}", self.0) + } +} + +impl_std_error!(InvalidSighashTypeError); + +/// This type is consensus valid but an input including it would prevent the transaction from +/// being relayed on today's Bitcoin network. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct NonStandardSighashType(pub u32); + +impl fmt::Display for NonStandardSighashType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Non standard sighash type {}", self.0) + } +} + +impl_std_error!(NonStandardSighashType); + +/// Error returned for failure during parsing one of the sighash types. +/// +/// This is currently returned for unrecognized sighash strings. +#[derive(Debug, Clone)] +pub struct SighashTypeParseError { + /// The unrecognized string we attempted to parse. + pub unrecognized: String, +} + +impl fmt::Display for SighashTypeParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Unrecognized SIGHASH string '{}'", self.unrecognized) + } +} + +impl_std_error!(SighashTypeParseError); diff --git a/dash/src/crypto/sighash.rs b/dash/src/crypto/sighash.rs index f05726757..9c7886ad3 100644 --- a/dash/src/crypto/sighash.rs +++ b/dash/src/crypto/sighash.rs @@ -12,16 +12,21 @@ //! [`SighashCache`] and calling its methods. use core::borrow::{Borrow, BorrowMut}; -use core::{fmt, str}; +use core::fmt; -use hashes::{Hash, hash_newtype, sha256, sha256d, sha256t_hash_newtype}; +use hashes::{Hash, sha256, sha256d}; + +pub use dashcore_crypto::sighash::SplitAnyoneCanPay; +pub use dashcore_crypto::sighash::{ + EcdsaSighashType, InvalidSighashTypeError, LegacySighash, NonStandardSighashType, + SegwitV0Sighash, SighashTypeParseError, TapSighash, TapSighashTag, TapSighashType, +}; use crate::blockdata::transaction::txin::TxIn; use crate::blockdata::transaction::txout::TxOut; use crate::blockdata::transaction::{EncodeSigningDataResult, Transaction}; use crate::blockdata::witness::Witness; use crate::consensus::{Encodable, encode}; -use crate::error::impl_std_error; use crate::io; use crate::prelude::*; use crate::script::{Script, ScriptBuf}; @@ -36,26 +41,6 @@ pub(crate) const UINT256_ONE: [u8; 32] = [ 0, 0, 0, 0, 0, 0, 0, 0 ]; -hash_newtype! { - /// Hash of a transaction according to the legacy signature algorithm. - #[hash_newtype(forward)] - pub struct LegacySighash(sha256d::Hash); - - /// Hash of a transaction according to the segwit version 0 signature algorithm. - #[hash_newtype(forward)] - pub struct SegwitV0Sighash(sha256d::Hash); -} - -sha256t_hash_newtype! { - pub struct TapSighashTag = hash_str("TapSighash"); - - /// Taproot-tagged hash with tag \"TapSighash\". - /// - /// This hash type is used for computing taproot signature hash." - #[hash_newtype(forward)] - pub struct TapSighash(_); -} - /// Efficiently calculates signature hash message for legacy, segwit and taproot inputs. #[derive(Debug)] pub struct SighashCache> { @@ -127,69 +112,6 @@ pub struct ScriptPath<'s> { leaf_version: LeafVersion, } -/// Hashtype of an input's signature, encoded in the last byte of the signature. -/// Fixed values so they can be cast as integer types for encoding. -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] -pub enum TapSighashType { - /// 0x0: Used when not explicitly specified, defaults to [`TapSighashType::All`] - Default = 0x00, - /// 0x1: Sign all outputs. - All = 0x01, - /// 0x2: Sign no outputs --- anyone can choose the destination. - None = 0x02, - /// 0x3: Sign the output whose index matches this input's index. If none exists, - /// sign the hash `0000000000000000000000000000000000000000000000000000000000000001`. - /// (This rule is probably an unintentional C++ism, but it's consensus so we have - /// to follow it.) - Single = 0x03, - /// 0x81: Sign all outputs but only this input. - AllPlusAnyoneCanPay = 0x81, - /// 0x82: Sign no outputs and only this input. - NonePlusAnyoneCanPay = 0x82, - /// 0x83: Sign one output and only this input (see `Single` for what "one output" means). - SinglePlusAnyoneCanPay = 0x83, -} -#[cfg(feature = "serde")] -crate::serde_utils::serde_string_impl!(TapSighashType, "a TapSighashType data"); - -impl fmt::Display for TapSighashType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use TapSighashType::*; - - let s = match self { - Default => "SIGHASH_DEFAULT", - All => "SIGHASH_ALL", - None => "SIGHASH_NONE", - Single => "SIGHASH_SINGLE", - AllPlusAnyoneCanPay => "SIGHASH_ALL|SIGHASH_ANYONECANPAY", - NonePlusAnyoneCanPay => "SIGHASH_NONE|SIGHASH_ANYONECANPAY", - SinglePlusAnyoneCanPay => "SIGHASH_SINGLE|SIGHASH_ANYONECANPAY", - }; - f.write_str(s) - } -} - -impl str::FromStr for TapSighashType { - type Err = SighashTypeParseError; - - fn from_str(s: &str) -> Result { - use TapSighashType::*; - - match s { - "SIGHASH_DEFAULT" => Ok(Default), - "SIGHASH_ALL" => Ok(All), - "SIGHASH_NONE" => Ok(None), - "SIGHASH_SINGLE" => Ok(Single), - "SIGHASH_ALL|SIGHASH_ANYONECANPAY" => Ok(AllPlusAnyoneCanPay), - "SIGHASH_NONE|SIGHASH_ANYONECANPAY" => Ok(NonePlusAnyoneCanPay), - "SIGHASH_SINGLE|SIGHASH_ANYONECANPAY" => Ok(SinglePlusAnyoneCanPay), - _ => Err(SighashTypeParseError { - unrecognized: s.to_owned(), - }), - } - } -} - /// Possible errors in computing the signature message. #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] #[non_exhaustive] @@ -367,243 +289,12 @@ impl<'s> From> for TapLeafHash { } } -/// Hashtype of an input's signature, encoded in the last byte of the signature. -/// -/// Fixed values so they can be cast as integer types for encoding (see also -/// [`TapSighashType`]). -#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)] -pub enum EcdsaSighashType { - /// 0x1: Sign all outputs. - All = 0x01, - /// 0x2: Sign no outputs --- anyone can choose the destination. - None = 0x02, - /// 0x3: Sign the output whose index matches this input's index. If none exists, - /// sign the hash `0000000000000000000000000000000000000000000000000000000000000001`. - /// (This rule is probably an unintentional C++ism, but it's consensus so we have - /// to follow it.) - Single = 0x03, - /// 0x81: Sign all outputs but only this input. - AllPlusAnyoneCanPay = 0x81, - /// 0x82: Sign no outputs and only this input. - NonePlusAnyoneCanPay = 0x82, - /// 0x83: Sign one output and only this input (see `Single` for what "one output" means). - SinglePlusAnyoneCanPay = 0x83, -} -#[cfg(feature = "serde")] -crate::serde_utils::serde_string_impl!(EcdsaSighashType, "a EcdsaSighashType data"); - -impl fmt::Display for EcdsaSighashType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use EcdsaSighashType::*; - - let s = match self { - All => "SIGHASH_ALL", - None => "SIGHASH_NONE", - Single => "SIGHASH_SINGLE", - AllPlusAnyoneCanPay => "SIGHASH_ALL|SIGHASH_ANYONECANPAY", - NonePlusAnyoneCanPay => "SIGHASH_NONE|SIGHASH_ANYONECANPAY", - SinglePlusAnyoneCanPay => "SIGHASH_SINGLE|SIGHASH_ANYONECANPAY", - }; - f.write_str(s) - } -} - -impl str::FromStr for EcdsaSighashType { - type Err = SighashTypeParseError; - - fn from_str(s: &str) -> Result { - use EcdsaSighashType::*; - - match s { - "SIGHASH_ALL" => Ok(All), - "SIGHASH_NONE" => Ok(None), - "SIGHASH_SINGLE" => Ok(Single), - "SIGHASH_ALL|SIGHASH_ANYONECANPAY" => Ok(AllPlusAnyoneCanPay), - "SIGHASH_NONE|SIGHASH_ANYONECANPAY" => Ok(NonePlusAnyoneCanPay), - "SIGHASH_SINGLE|SIGHASH_ANYONECANPAY" => Ok(SinglePlusAnyoneCanPay), - _ => Err(SighashTypeParseError { - unrecognized: s.to_owned(), - }), - } - } -} - -impl EcdsaSighashType { - /// Splits the sighash flag into the "real" sighash flag and the ANYONECANPAY boolean. - pub(crate) fn split_anyonecanpay_flag(self) -> (EcdsaSighashType, bool) { - use EcdsaSighashType::*; - - match self { - All => (All, false), - None => (None, false), - Single => (Single, false), - AllPlusAnyoneCanPay => (All, true), - NonePlusAnyoneCanPay => (None, true), - SinglePlusAnyoneCanPay => (Single, true), - } - } - - /// Creates a [`EcdsaSighashType`] from a raw `u32`. - /// - /// **Note**: this replicates consensus behaviour, for current standardness rules correctness - /// you probably want [`Self::from_standard`]. - /// - /// This might cause unexpected behavior because it does not roundtrip. That is, - /// `EcdsaSighashType::from_consensus(n) as u32 != n` for non-standard values of `n`. While - /// verifying signatures, the user should retain the `n` and use it compute the signature hash - /// message. - pub fn from_consensus(n: u32) -> EcdsaSighashType { - use EcdsaSighashType::*; - - // In Bitcoin Core, the SignatureHash function will mask the (int32) value with - // 0x1f to (apparently) deactivate ACP when checking for SINGLE and NONE bits. - // We however want to be matching also against on ACP-masked ALL, SINGLE, and NONE. - // So here we re-activate ACP. - let mask = 0x1f | 0x80; - match n & mask { - // "real" sighashes - 0x01 => All, - 0x02 => None, - 0x03 => Single, - 0x81 => AllPlusAnyoneCanPay, - 0x82 => NonePlusAnyoneCanPay, - 0x83 => SinglePlusAnyoneCanPay, - // catchalls - x if x & 0x80 == 0x80 => AllPlusAnyoneCanPay, - _ => All, - } - } - - /// Creates a [`EcdsaSighashType`] from a raw `u32`. - /// - /// # Errors - /// - /// If `n` is a non-standard sighash value. - pub fn from_standard(n: u32) -> Result { - use EcdsaSighashType::*; - - match n { - // Standard sighashes, see https://github.com/bitcoin/bitcoin/blob/b805dbb0b9c90dadef0424e5b3bf86ac308e103e/src/script/interpreter.cpp#L189-L198 - 0x01 => Ok(All), - 0x02 => Ok(None), - 0x03 => Ok(Single), - 0x81 => Ok(AllPlusAnyoneCanPay), - 0x82 => Ok(NonePlusAnyoneCanPay), - 0x83 => Ok(SinglePlusAnyoneCanPay), - non_standard => Err(NonStandardSighashType(non_standard)), - } - } - - /// Converts [`EcdsaSighashType`] to a `u32` sighash flag. - /// - /// The returned value is guaranteed to be a valid according to standardness rules. - pub fn to_u32(self) -> u32 { - self as u32 - } -} - -impl From for TapSighashType { - fn from(s: EcdsaSighashType) -> Self { - use TapSighashType::*; - - match s { - EcdsaSighashType::All => All, - EcdsaSighashType::None => None, - EcdsaSighashType::Single => Single, - EcdsaSighashType::AllPlusAnyoneCanPay => AllPlusAnyoneCanPay, - EcdsaSighashType::NonePlusAnyoneCanPay => NonePlusAnyoneCanPay, - EcdsaSighashType::SinglePlusAnyoneCanPay => SinglePlusAnyoneCanPay, - } - } -} - -impl TapSighashType { - /// Breaks the sighash flag into the "real" sighash flag and the `SIGHASH_ANYONECANPAY` boolean. - pub(crate) fn split_anyonecanpay_flag(self) -> (TapSighashType, bool) { - use TapSighashType::*; - - match self { - Default => (Default, false), - All => (All, false), - None => (None, false), - Single => (Single, false), - AllPlusAnyoneCanPay => (All, true), - NonePlusAnyoneCanPay => (None, true), - SinglePlusAnyoneCanPay => (Single, true), - } - } - - /// Constructs a [`TapSighashType`] from a raw `u8`. - /// - /// # Errors - /// - /// Returns [`InvalidSighashTypeError`] when `hash_ty` names no sighash - /// type. The error is dedicated rather than the module's [`Error`] so - /// that naming a sighash type stays independent of computing one. - pub fn from_consensus_u8(hash_ty: u8) -> Result { - use TapSighashType::*; - - Ok(match hash_ty { - 0x00 => Default, - 0x01 => All, - 0x02 => None, - 0x03 => Single, - 0x81 => AllPlusAnyoneCanPay, - 0x82 => NonePlusAnyoneCanPay, - 0x83 => SinglePlusAnyoneCanPay, - x => return Err(InvalidSighashTypeError(x as u32)), - }) - } -} - -/// Integer is not a consensus valid sighash type. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct InvalidSighashTypeError(pub u32); - -impl fmt::Display for InvalidSighashTypeError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "invalid sighash type {}", self.0) - } -} - -impl_std_error!(InvalidSighashTypeError); - impl From for Error { fn from(e: InvalidSighashTypeError) -> Self { Error::InvalidSighashType(e.0) } } -/// This type is consensus valid but an input including it would prevent the transaction from -/// being relayed on today's Bitcoin network. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct NonStandardSighashType(pub u32); - -impl fmt::Display for NonStandardSighashType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Non standard sighash type {}", self.0) - } -} - -impl_std_error!(NonStandardSighashType); - -/// Error returned for failure during parsing one of the sighash types. -/// -/// This is currently returned for unrecognized sighash strings. -#[derive(Debug, Clone)] -pub struct SighashTypeParseError { - /// The unrecognized string we attempted to parse. - pub unrecognized: String, -} - -impl fmt::Display for SighashTypeParseError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Unrecognized SIGHASH string '{}'", self.unrecognized) - } -} - -impl_std_error!(SighashTypeParseError); - impl> SighashCache { /// Constructs a new `SighashCache` from an unsigned transaction. /// From 21168454e286a66b733d09401549d1de99e41192 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:32:27 +0530 Subject: [PATCH 07/11] refactor(dash): drop the now-unused `serde_string_impl!` --- dash/src/serde_utils.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/dash/src/serde_utils.rs b/dash/src/serde_utils.rs index cf8048501..d30de7bda 100644 --- a/dash/src/serde_utils.rs +++ b/dash/src/serde_utils.rs @@ -348,13 +348,7 @@ macro_rules! serde_string_deserialize_impl { }; } -macro_rules! serde_string_impl { - ($name:ty, $expecting:literal) => { - $crate::serde_utils::serde_string_deserialize_impl!($name, $expecting); - $crate::serde_utils::serde_string_serialize_impl!($name, $expecting); - }; -} -pub(crate) use {serde_string_deserialize_impl, serde_string_impl, serde_string_serialize_impl}; +pub(crate) use {serde_string_deserialize_impl, serde_string_serialize_impl}; /// A combination macro where the human-readable serialization is done like /// serde_string_impl and the non-human-readable impl is done as a struct. From 46f208cd436ed800a5147a5cdda3c2a6c324233f Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 24 Sep 2026 14:07:18 +0530 Subject: [PATCH 08/11] refactor: move the ECDSA and taproot signatures into `dashcore-crypto` `git diff --color-moved=dimmed-zebra --color-moved-ws=ignore-all-space` --- {dash/src/crypto => crypto/src}/ecdsa.rs | 11 ++++++++--- crypto/src/lib.rs | 2 ++ {dash/src/crypto => crypto/src}/taproot.rs | 10 ++++++++-- dash/src/crypto/mod.rs | 9 ++++++--- 4 files changed, 24 insertions(+), 8 deletions(-) rename {dash/src/crypto => crypto/src}/ecdsa.rs (95%) rename {dash/src/crypto => crypto/src}/taproot.rs (91%) diff --git a/dash/src/crypto/ecdsa.rs b/crypto/src/ecdsa.rs similarity index 95% rename from dash/src/crypto/ecdsa.rs rename to crypto/src/ecdsa.rs index 8010a5019..f3e44e070 100644 --- a/dash/src/crypto/ecdsa.rs +++ b/crypto/src/ecdsa.rs @@ -1,5 +1,9 @@ -// Written in 2014 by Andrew Poelstra +// +// This file is a part of rust-dashcore. +// Portions written by Andrew Poelstra for rust-bitcoin. // SPDX-License-Identifier: CC0-1.0 +// See the accompanying file LICENSE or https://creativecommons.org/publicdomain/zero/1.0 +// //! ECDSA Bitcoin signatures. //! @@ -12,8 +16,9 @@ use hashes::hex::{self, FromHex}; use internals::hex::display::DisplayHex; use internals::write_err; use secp256k1; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; -use crate::prelude::*; use crate::sighash::{EcdsaSighashType, NonStandardSighashType}; const MAX_SIG_LEN: usize = 73; @@ -99,7 +104,7 @@ impl FromStr for Signature { /// 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 [`push_slice`](crate::script::ScriptBuf::push_slice). +/// into `ScriptBuf::push_slice`. #[derive(Copy, Clone)] pub struct SerializedSignature { data: [u8; MAX_SIG_LEN], diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs index 2e431a7c4..d956e60dc 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -24,7 +24,9 @@ pub extern crate serde; pub(crate) mod serde_utils; pub mod bls; +pub mod ecdsa; pub mod sighash; +pub mod taproot; /// Implements `std::error::Error` for a type whose `Display` carries the message. macro_rules! impl_std_error { diff --git a/dash/src/crypto/taproot.rs b/crypto/src/taproot.rs similarity index 91% rename from dash/src/crypto/taproot.rs rename to crypto/src/taproot.rs index 8ec5b50bb..74362831f 100644 --- a/dash/src/crypto/taproot.rs +++ b/crypto/src/taproot.rs @@ -1,5 +1,9 @@ -// Written in 2014 by Andrew Poelstra +// +// This file is a part of rust-dashcore. +// Portions written by Andrew Poelstra for rust-bitcoin. // SPDX-License-Identifier: CC0-1.0 +// See the accompanying file LICENSE or https://creativecommons.org/publicdomain/zero/1.0 +// //! Bitcoin taproot keys. //! @@ -9,9 +13,11 @@ use core::fmt; use internals::write_err; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + pub use secp256k1::{self}; -use crate::prelude::*; use crate::sighash::TapSighashType; /// A BIP340-341 serialized taproot signature with the corresponding hash type. diff --git a/dash/src/crypto/mod.rs b/dash/src/crypto/mod.rs index e28b09075..0a3e7e288 100644 --- a/dash/src/crypto/mod.rs +++ b/dash/src/crypto/mod.rs @@ -6,8 +6,11 @@ //! Cryptography related functionality: keys and signatures. //! -pub mod ecdsa; +pub mod ecdsa { + pub use dashcore_crypto::ecdsa::{Error, SerializedSignature, Signature}; +} pub mod key; pub mod sighash; -// Contents re-exported in `dash::taproot`. -pub mod taproot; +pub mod taproot { + pub use dashcore_crypto::taproot::{Error, Signature}; +} From a96a6708faa6e193ceeabc1727ce6e9e4494789a Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:40:16 +0530 Subject: [PATCH 09/11] refactor(key-wallet): move the DIP9 derivation paths off BIP32 module `git diff --color-moved=dimmed-zebra --color-moved-ws=ignore-all-space` --- key-wallet/src/bip32.rs | 256 +--------------------------------------- key-wallet/src/dip9.rs | 234 ++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 255 deletions(-) diff --git a/key-wallet/src/bip32.rs b/key-wallet/src/bip32.rs index 4ed7172d0..ea185f307 100644 --- a/key-wallet/src/bip32.rs +++ b/key-wallet/src/bip32.rs @@ -32,17 +32,6 @@ use secp256k1::{self, XOnlyPublicKey}; #[cfg(feature = "serde")] use serde; -use crate::dip9::{ - APPLICATION_ENCRYPTION_PATH_MAINNET, APPLICATION_ENCRYPTION_PATH_TESTNET, - APPLICATION_SESSION_AUTHENTICATION_PATH_MAINNET, - APPLICATION_SESSION_AUTHENTICATION_PATH_TESTNET, ASSET_LOCK_ADDRESS_TOPUP_PATH_MAINNET, - ASSET_LOCK_ADDRESS_TOPUP_PATH_TESTNET, ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_MAINNET, - ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_TESTNET, COINJOIN_PATH_MAINNET, COINJOIN_PATH_TESTNET, - DASH_BIP44_PATH_MAINNET, DASH_BIP44_PATH_TESTNET, IDENTITY_AUTHENTICATION_PATH_MAINNET, - IDENTITY_AUTHENTICATION_PATH_TESTNET, IDENTITY_INVITATION_PATH_MAINNET, - IDENTITY_INVITATION_PATH_TESTNET, IDENTITY_REGISTRATION_PATH_MAINNET, - IDENTITY_REGISTRATION_PATH_TESTNET, IDENTITY_TOPUP_PATH_MAINNET, IDENTITY_TOPUP_PATH_TESTNET, -}; #[cfg(feature = "bincode")] use bincode_derive::{Decode, Encode}; use dashcore::Network; @@ -51,6 +40,7 @@ use zeroize::Zeroize; /// XpubIdentifier as a hash160 result type XpubIdentifier = hash160::Hash; +pub use crate::dip9::{ApplicationKeyPurpose, KeyDerivationType}; pub use secp256k1::Keypair; pub use secp256k1::PublicKey; /// Re-export key types from secp256k1 @@ -1010,251 +1000,7 @@ impl<'de, C> bincode::BorrowDecode<'de, C> for DerivationPath { } } -#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] -#[repr(u32)] -pub enum KeyDerivationType { - ECDSA = 0, - BLS = 1, -} - -impl From for u32 { - fn from(val: KeyDerivationType) -> Self { - match val { - KeyDerivationType::ECDSA => 0, - KeyDerivationType::BLS => 1, - } - } -} - -/// The `key_purpose'` level of a DIP-13 application encryption path: the Platform identity key -/// purpose the derived key is registered with. -#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] -#[repr(u32)] -pub enum ApplicationKeyPurpose { - Encryption = 1, - Decryption = 2, -} - -impl From for u32 { - fn from(val: ApplicationKeyPurpose) -> Self { - match val { - ApplicationKeyPurpose::Encryption => 1, - ApplicationKeyPurpose::Decryption => 2, - } - } -} - impl DerivationPath { - pub fn bip_44_account(network: Network, account: u32) -> Self { - let mut root_derivation_path: DerivationPath = match network { - Network::Mainnet => DASH_BIP44_PATH_MAINNET, - _ => DASH_BIP44_PATH_TESTNET, - } - .into(); - root_derivation_path.0.extend(&[ChildNumber::Hardened { - index: account, - }]); - root_derivation_path - } - pub fn bip_44_payment_path( - network: Network, - account: u32, - change: bool, - address_index: u32, - ) -> Self { - let mut root_derivation_path: DerivationPath = match network { - Network::Mainnet => DASH_BIP44_PATH_MAINNET, - _ => DASH_BIP44_PATH_TESTNET, - } - .into(); - root_derivation_path.0.extend(&[ - ChildNumber::Hardened { - index: account, - }, - ChildNumber::Normal { - index: change.into(), - }, - ChildNumber::Normal { - index: address_index, - }, - ]); - root_derivation_path - } - pub fn coinjoin_path(network: Network, account: u32) -> Self { - let mut root_derivation_path: DerivationPath = match network { - Network::Mainnet => COINJOIN_PATH_MAINNET, - _ => COINJOIN_PATH_TESTNET, - } - .into(); - root_derivation_path.0.extend(&[ChildNumber::Hardened { - index: account, - }]); - root_derivation_path - } - - /// This might have been used in the past - pub fn identity_registration_path_child_non_hardened(network: Network, index: u32) -> Self { - let mut root_derivation_path: DerivationPath = match network { - Network::Mainnet => IDENTITY_REGISTRATION_PATH_MAINNET, - _ => IDENTITY_REGISTRATION_PATH_TESTNET, - } - .into(); - root_derivation_path.0.extend(&[ChildNumber::Normal { - index, - }]); - root_derivation_path - } - - pub fn identity_registration_path(network: Network, index: u32) -> Self { - let mut root_derivation_path: DerivationPath = match network { - Network::Mainnet => IDENTITY_REGISTRATION_PATH_MAINNET, - _ => IDENTITY_REGISTRATION_PATH_TESTNET, - } - .into(); - root_derivation_path.0.extend(&[ChildNumber::Hardened { - index, - }]); - root_derivation_path - } - - pub fn identity_top_up_path(network: Network, identity_index: u32, top_up_index: u32) -> Self { - let mut root_derivation_path: DerivationPath = match network { - Network::Mainnet => IDENTITY_TOPUP_PATH_MAINNET, - _ => IDENTITY_TOPUP_PATH_TESTNET, - } - .into(); - root_derivation_path.0.extend(&[ - ChildNumber::Hardened { - index: identity_index, - }, - ChildNumber::Normal { - index: top_up_index, - }, - ]); - root_derivation_path - } - - pub fn identity_invitation_path(network: Network, index: u32) -> Self { - let mut root_derivation_path: DerivationPath = match network { - Network::Mainnet => IDENTITY_INVITATION_PATH_MAINNET, - _ => IDENTITY_INVITATION_PATH_TESTNET, - } - .into(); - root_derivation_path.0.extend(&[ChildNumber::Hardened { - index, - }]); - root_derivation_path - } - - pub fn asset_lock_address_top_up_path(network: Network, index: u32) -> Self { - let mut root_derivation_path: DerivationPath = match network { - Network::Mainnet => ASSET_LOCK_ADDRESS_TOPUP_PATH_MAINNET, - _ => ASSET_LOCK_ADDRESS_TOPUP_PATH_TESTNET, - } - .into(); - root_derivation_path.0.extend(&[ChildNumber::Hardened { - index, - }]); - root_derivation_path - } - - pub fn asset_lock_shielded_address_top_up_path(network: Network, index: u32) -> Self { - let mut root_derivation_path: DerivationPath = match network { - Network::Mainnet => ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_MAINNET, - _ => ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_TESTNET, - } - .into(); - root_derivation_path.0.extend(&[ChildNumber::Hardened { - index, - }]); - root_derivation_path - } - - pub fn identity_authentication_path( - network: Network, - key_type: KeyDerivationType, - identity_index: u32, - key_index: u32, - ) -> Self { - let mut root_derivation_path: DerivationPath = match network { - Network::Mainnet => IDENTITY_AUTHENTICATION_PATH_MAINNET, - _ => IDENTITY_AUTHENTICATION_PATH_TESTNET, - } - .into(); - root_derivation_path.0.extend(&[ - ChildNumber::Hardened { - index: key_type.into(), - }, - ChildNumber::Hardened { - index: identity_index, - }, - ChildNumber::Hardened { - index: key_index, - }, - ]); - root_derivation_path - } - - /// DIP-13 application session authentication key path, - /// `m/9'/coin_type'/5'/6'/0'/identity_id'/request_id'`. The identity id and request id are - /// DIP-14 256-bit hardened children, which only secp256k1 derivation defines, so the key type - /// level is always ECDSA (`0'`). - pub fn application_session_authentication_path( - network: Network, - identity_id: [u8; 32], - request_id: [u8; 32], - ) -> Self { - let mut root_derivation_path: DerivationPath = match network { - Network::Mainnet => APPLICATION_SESSION_AUTHENTICATION_PATH_MAINNET, - _ => APPLICATION_SESSION_AUTHENTICATION_PATH_TESTNET, - } - .into(); - root_derivation_path.0.extend(&[ - ChildNumber::Hardened { - index: KeyDerivationType::ECDSA.into(), - }, - ChildNumber::Hardened256 { - index: identity_id, - }, - ChildNumber::Hardened256 { - index: request_id, - }, - ]); - root_derivation_path - } - - /// DIP-13 application encryption key path, - /// `m/9'/coin_type'/5'/7'/0'/identity_id'/contract_id'/key_purpose'`. The identity id and - /// contract id are DIP-14 256-bit hardened children, which only secp256k1 derivation defines, - /// so the key type level is always ECDSA (`0'`). - pub fn application_encryption_path( - network: Network, - identity_id: [u8; 32], - contract_id: [u8; 32], - key_purpose: ApplicationKeyPurpose, - ) -> Self { - let mut root_derivation_path: DerivationPath = match network { - Network::Mainnet => APPLICATION_ENCRYPTION_PATH_MAINNET, - _ => APPLICATION_ENCRYPTION_PATH_TESTNET, - } - .into(); - root_derivation_path.0.extend(&[ - ChildNumber::Hardened { - index: KeyDerivationType::ECDSA.into(), - }, - ChildNumber::Hardened256 { - index: identity_id, - }, - ChildNumber::Hardened256 { - index: contract_id, - }, - ChildNumber::Hardened { - index: key_purpose.into(), - }, - ]); - root_derivation_path - } - pub fn derive_priv_ecdsa_for_master_seed( &self, seed: &[u8], diff --git a/key-wallet/src/dip9.rs b/key-wallet/src/dip9.rs index 1f2683dd9..309bab3cf 100644 --- a/key-wallet/src/dip9.rs +++ b/key-wallet/src/dip9.rs @@ -597,3 +597,237 @@ pub const PLATFORM_PAYMENT_ROOT_PATH_TESTNET: IndexConstPath<3> = IndexConstPath reference: DerivationPathReference::PlatformPayment, path_type: DerivationPathType::CLEAR_FUNDS, }; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +#[repr(u32)] +pub enum KeyDerivationType { + ECDSA = 0, + BLS = 1, +} + +impl From for u32 { + fn from(val: KeyDerivationType) -> Self { + match val { + KeyDerivationType::ECDSA => 0, + KeyDerivationType::BLS => 1, + } + } +} + +/// The `key_purpose'` level of a DIP-13 application encryption path: the Platform identity key +/// purpose the derived key is registered with. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +#[repr(u32)] +pub enum ApplicationKeyPurpose { + Encryption = 1, + Decryption = 2, +} + +impl From for u32 { + fn from(val: ApplicationKeyPurpose) -> Self { + match val { + ApplicationKeyPurpose::Encryption => 1, + ApplicationKeyPurpose::Decryption => 2, + } + } +} + +impl DerivationPath { + pub fn bip_44_account(network: Network, account: u32) -> Self { + let root_derivation_path: DerivationPath = match network { + Network::Mainnet => DASH_BIP44_PATH_MAINNET, + _ => DASH_BIP44_PATH_TESTNET, + } + .into(); + root_derivation_path.extend([ChildNumber::Hardened { + index: account, + }]) + } + pub fn bip_44_payment_path( + network: Network, + account: u32, + change: bool, + address_index: u32, + ) -> Self { + let root_derivation_path: DerivationPath = match network { + Network::Mainnet => DASH_BIP44_PATH_MAINNET, + _ => DASH_BIP44_PATH_TESTNET, + } + .into(); + root_derivation_path.extend([ + ChildNumber::Hardened { + index: account, + }, + ChildNumber::Normal { + index: change.into(), + }, + ChildNumber::Normal { + index: address_index, + }, + ]) + } + pub fn coinjoin_path(network: Network, account: u32) -> Self { + let root_derivation_path: DerivationPath = match network { + Network::Mainnet => COINJOIN_PATH_MAINNET, + _ => COINJOIN_PATH_TESTNET, + } + .into(); + root_derivation_path.extend([ChildNumber::Hardened { + index: account, + }]) + } + + /// This might have been used in the past + pub fn identity_registration_path_child_non_hardened(network: Network, index: u32) -> Self { + let root_derivation_path: DerivationPath = match network { + Network::Mainnet => IDENTITY_REGISTRATION_PATH_MAINNET, + _ => IDENTITY_REGISTRATION_PATH_TESTNET, + } + .into(); + root_derivation_path.extend([ChildNumber::Normal { + index, + }]) + } + + pub fn identity_registration_path(network: Network, index: u32) -> Self { + let root_derivation_path: DerivationPath = match network { + Network::Mainnet => IDENTITY_REGISTRATION_PATH_MAINNET, + _ => IDENTITY_REGISTRATION_PATH_TESTNET, + } + .into(); + root_derivation_path.extend([ChildNumber::Hardened { + index, + }]) + } + + pub fn identity_top_up_path(network: Network, identity_index: u32, top_up_index: u32) -> Self { + let root_derivation_path: DerivationPath = match network { + Network::Mainnet => IDENTITY_TOPUP_PATH_MAINNET, + _ => IDENTITY_TOPUP_PATH_TESTNET, + } + .into(); + root_derivation_path.extend([ + ChildNumber::Hardened { + index: identity_index, + }, + ChildNumber::Normal { + index: top_up_index, + }, + ]) + } + + pub fn identity_invitation_path(network: Network, index: u32) -> Self { + let root_derivation_path: DerivationPath = match network { + Network::Mainnet => IDENTITY_INVITATION_PATH_MAINNET, + _ => IDENTITY_INVITATION_PATH_TESTNET, + } + .into(); + root_derivation_path.extend([ChildNumber::Hardened { + index, + }]) + } + + pub fn asset_lock_address_top_up_path(network: Network, index: u32) -> Self { + let root_derivation_path: DerivationPath = match network { + Network::Mainnet => ASSET_LOCK_ADDRESS_TOPUP_PATH_MAINNET, + _ => ASSET_LOCK_ADDRESS_TOPUP_PATH_TESTNET, + } + .into(); + root_derivation_path.extend([ChildNumber::Hardened { + index, + }]) + } + + pub fn asset_lock_shielded_address_top_up_path(network: Network, index: u32) -> Self { + let root_derivation_path: DerivationPath = match network { + Network::Mainnet => ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_MAINNET, + _ => ASSET_LOCK_SHIELDED_ADDRESS_TOPUP_PATH_TESTNET, + } + .into(); + root_derivation_path.extend([ChildNumber::Hardened { + index, + }]) + } + + pub fn identity_authentication_path( + network: Network, + key_type: KeyDerivationType, + identity_index: u32, + key_index: u32, + ) -> Self { + let root_derivation_path: DerivationPath = match network { + Network::Mainnet => IDENTITY_AUTHENTICATION_PATH_MAINNET, + _ => IDENTITY_AUTHENTICATION_PATH_TESTNET, + } + .into(); + root_derivation_path.extend([ + ChildNumber::Hardened { + index: key_type.into(), + }, + ChildNumber::Hardened { + index: identity_index, + }, + ChildNumber::Hardened { + index: key_index, + }, + ]) + } + + /// DIP-13 application session authentication key path, + /// `m/9'/coin_type'/5'/6'/0'/identity_id'/request_id'`. The identity id and request id are + /// DIP-14 256-bit hardened children, which only secp256k1 derivation defines, so the key type + /// level is always ECDSA (`0'`). + pub fn application_session_authentication_path( + network: Network, + identity_id: [u8; 32], + request_id: [u8; 32], + ) -> Self { + let root_derivation_path: DerivationPath = match network { + Network::Mainnet => APPLICATION_SESSION_AUTHENTICATION_PATH_MAINNET, + _ => APPLICATION_SESSION_AUTHENTICATION_PATH_TESTNET, + } + .into(); + root_derivation_path.extend([ + ChildNumber::Hardened { + index: KeyDerivationType::ECDSA.into(), + }, + ChildNumber::Hardened256 { + index: identity_id, + }, + ChildNumber::Hardened256 { + index: request_id, + }, + ]) + } + + /// DIP-13 application encryption key path, + /// `m/9'/coin_type'/5'/7'/0'/identity_id'/contract_id'/key_purpose'`. The identity id and + /// contract id are DIP-14 256-bit hardened children, which only secp256k1 derivation defines, + /// so the key type level is always ECDSA (`0'`). + pub fn application_encryption_path( + network: Network, + identity_id: [u8; 32], + contract_id: [u8; 32], + key_purpose: ApplicationKeyPurpose, + ) -> Self { + let root_derivation_path: DerivationPath = match network { + Network::Mainnet => APPLICATION_ENCRYPTION_PATH_MAINNET, + _ => APPLICATION_ENCRYPTION_PATH_TESTNET, + } + .into(); + root_derivation_path.extend([ + ChildNumber::Hardened { + index: KeyDerivationType::ECDSA.into(), + }, + ChildNumber::Hardened256 { + index: identity_id, + }, + ChildNumber::Hardened256 { + index: contract_id, + }, + ChildNumber::Hardened { + index: key_purpose.into(), + }, + ]) + } +} From ef9ae9c6a9b26c9a828f249faac52f6fd46a0cb2 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:29:58 +0530 Subject: [PATCH 10/11] refactor(dash)!: drop the redundant hex helpers on PubkeyHash --- .../special_transaction/provider_registration.rs | 16 ++++++++-------- .../provider_update_registrar.rs | 4 ++-- dash/src/hash_types.rs | 16 +++------------- key-wallet-ffi/src/special_payload.rs | 2 +- key-wallet/tests/derivation_tests.rs | 3 ++- 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/dash/src/blockdata/transaction/special_transaction/provider_registration.rs b/dash/src/blockdata/transaction/special_transaction/provider_registration.rs index 1da71e53b..1f9e31ec3 100644 --- a/dash/src/blockdata/transaction/special_transaction/provider_registration.rs +++ b/dash/src/blockdata/transaction/special_transaction/provider_registration.rs @@ -411,7 +411,7 @@ mod tests { let owner_key_hash_hex = "3dd03f9ec192b5f275a433bfc90f468ee1a3eb4c"; assert_eq!( owner_key_hash_hex, - expected_provider_registration_payload.owner_key_hash.to_hex() + expected_provider_registration_payload.owner_key_hash.to_string() ); let operator_key_hex = "157b10706659e25eb362b5d902d809f9160b1688e201ee6e94b40f9b5062d7074683ef05a2d5efb7793c47059c878dfa"; @@ -423,7 +423,7 @@ mod tests { let voting_key_hash_hex = "d38a30fafe61575db40f05ab0a08d55119b0aad3"; assert_eq!( voting_key_hash_hex, - expected_provider_registration_payload.voting_key_hash.to_hex() + expected_provider_registration_payload.voting_key_hash.to_string() ); let inputs_hash_hex = "7ba273b835b1017da314a3363760835ff5ac20278c160604cb8773750b997734"; @@ -487,9 +487,9 @@ mod tests { masternode_mode: provider_mode, collateral_outpoint, service_address: SocketAddr::V4(SocketAddrV4::new(address, port)), - owner_key_hash: PubkeyHash::from_hex(owner_key_hash_hex).unwrap(), + owner_key_hash: PubkeyHash::from_str(owner_key_hash_hex).unwrap(), operator_public_key: BLSPublicKey::from_hex(operator_key_hex).unwrap(), - voting_key_hash: PubkeyHash::from_hex(voting_key_hash_hex).unwrap(), + voting_key_hash: PubkeyHash::from_str(voting_key_hash_hex).unwrap(), operator_reward, script_payout, inputs_hash: InputsHash::from_hex(inputs_hash_hex).unwrap(), @@ -591,7 +591,7 @@ mod tests { let owner_key_hash_hex = "3dd03f9ec192b5f275a433bfc90f468ee1a3eb4c"; assert_eq!( owner_key_hash_hex, - expected_provider_registration_payload.owner_key_hash.to_hex() + expected_provider_registration_payload.owner_key_hash.to_string() ); let operator_key_hex = "157b10706659e25eb362b5d902d809f9160b1688e201ee6e94b40f9b5062d7074683ef05a2d5efb7793c47059c878dfa"; @@ -603,7 +603,7 @@ mod tests { let voting_key_hash_hex = "d38a30fafe61575db40f05ab0a08d55119b0aad3"; assert_eq!( voting_key_hash_hex, - expected_provider_registration_payload.voting_key_hash.to_hex() + expected_provider_registration_payload.voting_key_hash.to_string() ); let inputs_hash_hex = "7ba273b835b1017da314a3363760835ff5ac20278c160604cb8773750b997734"; @@ -667,9 +667,9 @@ mod tests { masternode_mode: provider_mode, collateral_outpoint, service_address, - owner_key_hash: PubkeyHash::from_hex(owner_key_hash_hex).unwrap(), + owner_key_hash: PubkeyHash::from_str(owner_key_hash_hex).unwrap(), operator_public_key: BLSPublicKey::from_hex(operator_key_hex).unwrap(), - voting_key_hash: PubkeyHash::from_hex(voting_key_hash_hex).unwrap(), + voting_key_hash: PubkeyHash::from_str(voting_key_hash_hex).unwrap(), operator_reward, script_payout, inputs_hash: InputsHash::from_hex(inputs_hash_hex).unwrap(), diff --git a/dash/src/blockdata/transaction/special_transaction/provider_update_registrar.rs b/dash/src/blockdata/transaction/special_transaction/provider_update_registrar.rs index 71a643a5d..372133e48 100644 --- a/dash/src/blockdata/transaction/special_transaction/provider_update_registrar.rs +++ b/dash/src/blockdata/transaction/special_transaction/provider_update_registrar.rs @@ -222,7 +222,7 @@ mod tests { ); // We should verify the script payouts match - let pubkey_hash = PubkeyHash::from_hex("56bcf3cac49235537d6ce0fb3214d8850a6db777") + let pubkey_hash = PubkeyHash::from_str("56bcf3cac49235537d6ce0fb3214d8850a6db777") .expect("expected to get pubkey hash"); let script_payout = ScriptBuf::new_p2pkh(&pubkey_hash); assert_eq!(expected_provider_update_registrar_payload.script_payout, script_payout); @@ -249,7 +249,7 @@ mod tests { pro_tx_hash, provider_mode, operator_public_key: BLSPublicKey::from_hex(operator_key_hex).unwrap(), - voting_key_hash: PubkeyHash::from_hex(voting_key_hash_hex).unwrap(), + voting_key_hash: PubkeyHash::from_str(voting_key_hash_hex).unwrap(), script_payout, inputs_hash: InputsHash::from_hex(inputs_hash_hex).unwrap(), payload_sig, diff --git a/dash/src/hash_types.rs b/dash/src/hash_types.rs index 78ea92e1a..a750f4e3c 100644 --- a/dash/src/hash_types.rs +++ b/dash/src/hash_types.rs @@ -376,22 +376,12 @@ mod newtypes { self.0.to_string() } } - - impl PubkeyHash { - /// Create a PubkeyHash from a string - pub fn from_hex(s: &str) -> Result { - Ok(Self(hash160::Hash::from_str(s)?)) - } - - /// Convert a PubkeyHash to a string - pub fn to_hex(&self) -> String { - self.0.to_string() - } - } } #[cfg(all(test, feature = "serde"))] mod tests { + use core::str::FromStr; + use super::*; use serde_derive::{Deserialize, Serialize}; @@ -495,7 +485,7 @@ mod tests { } let original = Tagged::A(WithPubkeyHash { - pkh: PubkeyHash::from_hex("e8b43025641eea4fd21190f01bd870ef90f1a8b1").unwrap(), + pkh: PubkeyHash::from_str("e8b43025641eea4fd21190f01bd870ef90f1a8b1").unwrap(), }); let value = serde_json::to_value(&original).unwrap(); diff --git a/key-wallet-ffi/src/special_payload.rs b/key-wallet-ffi/src/special_payload.rs index 527de521a..495304306 100644 --- a/key-wallet-ffi/src/special_payload.rs +++ b/key-wallet-ffi/src/special_payload.rs @@ -386,7 +386,7 @@ pub(crate) fn mainnet_shape_proreg_payload() -> ProviderRegistrationPayload { use dashcore::blockdata::transaction::special_transaction::provider_registration::ProviderMasternodeType; use std::str::FromStr; - let key_hash = dashcore::PubkeyHash::from_hex("70993555a01f7e8d6179d6135b5c56809d2d1d36") + let key_hash = dashcore::PubkeyHash::from_str("70993555a01f7e8d6179d6135b5c56809d2d1d36") .expect("hash160"); ProviderRegistrationPayload { version: 1, diff --git a/key-wallet/tests/derivation_tests.rs b/key-wallet/tests/derivation_tests.rs index 3cdcc92ce..0d7a7ae5f 100644 --- a/key-wallet/tests/derivation_tests.rs +++ b/key-wallet/tests/derivation_tests.rs @@ -5,10 +5,11 @@ //! is covered by the per-module tests in `key-wallet/src/derivation.rs`, //! `key-wallet/src/dip9.rs`, and `key-wallet/src/tests/account_tests.rs`. +use std::str::FromStr; + use dashcore::hashes::Hash; use key_wallet::mnemonic::Mnemonic; use key_wallet::{DerivationPath, ExtendedPrivKey, ExtendedPubKey, Network}; -use std::str::FromStr; // ============================================================================= // DIP-17 Platform Payment Key Derivation Test Vectors From a03df50595953d99f0b14694c0a37433df008b6d Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:34:51 +0530 Subject: [PATCH 11/11] refactor!: move keys and public key hashes into `dashcore-crypto` `git diff --color-moved=dimmed-zebra --color-moved-ws=ignore-all-space` --- crypto/Cargo.toml | 5 +- crypto/src/key.rs | 846 +++++++++++++++++++++++++++++++++++++++++ crypto/src/lib.rs | 1 + dash/src/crypto/key.rs | 845 +--------------------------------------- dash/src/hash_types.rs | 8 +- 5 files changed, 859 insertions(+), 846 deletions(-) create mode 100644 crypto/src/key.rs diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index 55d06e1f8..e97a68e9b 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -12,14 +12,15 @@ edition = "2021" [features] default = [] -bincode = ["dep:bincode", "dep:bincode_derive", "dashcore_hashes/bincode"] +bincode = ["dep:bincode", "dep:bincode_derive", "dashcore_hashes/bincode", "dash-network/bincode"] bls = ["dep:dash-pkc", "dep:tracing"] -serde = ["dep:serde", "dash-types/serde", "dashcore_hashes/serde", "secp256k1/serde"] +serde = ["dep:serde", "dash-types/serde", "dashcore_hashes/serde", "dash-network/serde", "secp256k1/serde"] [dependencies] base58ck = { version = "0.1.0" } bincode = { workspace = true, optional = true } bincode_derive = { workspace = true, optional = true } +dash-network = { path = "../dash-network" } dash-pkc = { version = "=0.1.0-beta", default-features = false, features = ["bls", "std"], optional = true } dash-types = { version = "=0.1.0-beta", default-features = false, features = ["codec"] } dashcore_hashes = { path = "../hashes" } diff --git a/crypto/src/key.rs b/crypto/src/key.rs new file mode 100644 index 000000000..f2dfe0dd9 --- /dev/null +++ b/crypto/src/key.rs @@ -0,0 +1,846 @@ +// +// This file is a part of rust-dashcore. +// Portions written by Andrew Poelstra for rust-bitcoin. +// SPDX-License-Identifier: CC0-1.0 +// See the accompanying file LICENSE or https://creativecommons.org/publicdomain/zero/1.0 +// + +//! Dash keys. +//! +//! This module provides keys used in Dash that can be roundtrip +//! (de)serialized. + +use core::fmt::{self, Write}; +use core::ops; +use core::str::FromStr; +use std::io; + +use dash_network::Network; +use hashes::hex::FromHex; +use hashes::{hash160, hash_newtype, hex, Hash as _}; +use internals::write_err; +pub use secp256k1::{self, constants, Keypair, Parity, Secp256k1, Verification, XOnlyPublicKey}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::base58; + +/// A key-related error. +#[derive(Clone, PartialEq, Eq, Debug)] +#[non_exhaustive] +pub enum Error { + /// Base58 encoding error + Base58(base58::Error), + /// secp256k1-related error + Secp256k1(secp256k1::Error), + /// Invalid key prefix error + InvalidKeyPrefix(u8), + /// The WIF or extended key version byte was not one we recognise. + InvalidAddressVersion(u8), + /// The base58 decoded correctly but the payload was the wrong length. + InvalidBase58PayloadLength(usize), + /// Hex decoding error + Hex(hex::Error), + /// `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 { + 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::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) + } + Error::NotSupported(string) => { + write!(f, "{}", string.as_str()) + } + } + } +} + +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(_) + | InvalidHexLength(_) => None, + NotSupported(_) => None, + } + } +} + +#[doc(hidden)] +impl From for Error { + fn from(e: base58::Error) -> 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::Error) -> Self { + Error::Hex(e) + } +} + +/// A Dash ECDSA public key +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PublicKey { + /// Whether this public key should be serialized as compressed + pub compressed: bool, + /// The actual ECDSA key + pub inner: secp256k1::PublicKey, +} + +impl PublicKey { + /// Constructs compressed ECDSA public key from the provided generic Secp256k1 public key + pub fn new(key: impl Into) -> PublicKey { + PublicKey { + compressed: true, + inner: key.into(), + } + } + + /// Constructs uncompressed (legacy) ECDSA public key from the provided generic Secp256k1 + /// public key + pub fn new_uncompressed(key: impl Into) -> PublicKey { + PublicKey { + compressed: false, + inner: key.into(), + } + } + + fn with_serialized R>(&self, f: F) -> R { + if self.compressed { + f(&self.inner.serialize()) + } else { + f(&self.inner.serialize_uncompressed()) + } + } + + /// Write the public key into a writer + pub fn write_into(&self, mut writer: W) -> Result<(), io::Error> { + 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(); + self.write_into(&mut buf).expect("vecs don't error"); + 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() { + 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)); + } + }; + + Ok(PublicKey { + compressed, + inner, + }) + } + + /// Computes the public key as supposed to be used with this secret + pub fn from_private_key(sk: &PrivateKey) -> PublicKey { + sk.public_key() + } + + /// Returns dash 160-bit hash of the public key + pub fn pubkey_hash(&self) -> PubkeyHash { + self.with_serialized(PubkeyHash::hash) + } + + /// 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 + } + } +} + +hash_newtype! { + /// A hash of a public key. + pub struct PubkeyHash(hash160::Hash); + /// SegWit version of a public key hash. + pub struct WPubkeyHash(hash160::Hash); +} + +impl From for PubkeyHash { + fn from(key: PublicKey) -> PubkeyHash { + key.pubkey_hash() + } +} + +/// 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 { + // TODO: fast hex encoding + self.with_serialized(|bytes| { + for ch in bytes { + write!(f, "{:02x}", ch)?; + } + Ok(()) + }) + } +} + +impl FromStr for PublicKey { + type Err = Error; + fn from_str(s: &str) -> Result { + match s.len() { + 66 => PublicKey::from_slice(&<[u8; 33]>::from_hex(s)?), + 130 => PublicKey::from_slice(&<[u8; 65]>::from_hex(s)?), + len => Err(Error::InvalidHexLength(len)), + } + } +} + +/// A Dash ECDSA private key +#[derive(Copy, 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, +} + +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 { + PrivateKey { + compressed: true, + network, + inner: key, + } + } + + /// 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 { + PrivateKey { + compressed: false, + network, + inner: key, + } + } + + /// Creates a public key from this private key + pub fn public_key(&self) -> PublicKey { + PublicKey { + compressed: self.compressed, + inner: self.inner.public_key(), + } + } + + /// Serialize the private key to bytes + pub fn to_bytes(self) -> Vec { + self.inner[..].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))?; + 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)) + } + + /// Format the private key to WIF format. + pub fn fmt_wif(&self, fmt: &mut dyn Write) -> fmt::Result { + let mut ret = [0; 34]; + ret[0] = match self.network { + Network::Mainnet => 204, + Network::Testnet | Network::Devnet | Network::Regtest => 239, + }; + ret[1..33].copy_from_slice(&self.inner[..]); + let privkey = if self.compressed { + ret[33] = 1; + base58::encode_check(&ret[..]) + } else { + base58::encode_check(&ret[..33]) + }; + fmt.write_str(&privkey) + } + + /// Get WIF encoding of this private key. + pub fn to_wif(self) -> String { + let mut buf = String::new(); + buf.write_fmt(format_args!("{}", self)).unwrap(); + buf.shrink_to_fit(); + buf + } + + /// Parse WIF encoded private key. + pub fn from_wif(wif: &str) -> Result { + let data = base58::decode_check(wif)?; + + let compressed = match data.len() { + 33 => false, + 34 => true, + _ => { + return Err(Error::InvalidBase58PayloadLength(data.len())); + } + }; + + let network = match data[0] { + 204 => Network::Mainnet, + 239 => Network::Testnet, + x => { + return Err(Error::InvalidAddressVersion(x)); + } + }; + + let secret = data[1..] + .first_chunk::<{ constants::SECRET_KEY_SIZE }>() + .ok_or(Error::InvalidBase58PayloadLength(data.len()))?; + + Ok(PrivateKey { + compressed, + network, + inner: secp256k1::SecretKey::from_secret_bytes(*secret)?, + }) + } +} + +impl fmt::Display for PrivateKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.fmt_wif(f) + } +} + +impl FromStr for PrivateKey { + type Err = Error; + fn from_str(s: &str) -> Result { + PrivateKey::from_wif(s) + } +} + +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 { + s.collect_str(self) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for PrivateKey { + fn deserialize>(d: D) -> Result { + struct WifVisitor; + + impl<'de> serde::de::Visitor<'de> for WifVisitor { + type Value = PrivateKey; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an ASCII WIF string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + PrivateKey::from_str(v).map_err(E::custom) + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + if let Ok(s) = core::str::from_utf8(v) { + PrivateKey::from_str(s).map_err(E::custom) + } else { + Err(E::invalid_value(serde::de::Unexpected::Bytes(v), &self)) + } + } + } + + d.deserialize_str(WifVisitor) + } +} + +#[cfg(feature = "serde")] +#[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)) + } + } +} + +#[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) + } +} + +#[cfg(test)] +mod tests { + use hashes::hex::FromHex; + + 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, + <[u8; 32]>::from_hex( + "ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8", + ) + .unwrap(), + [0_u8; 32], + ); + let expected2 = SortKey( + 4, + <[u8; 32]>::from_hex( + "ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8", + ) + .unwrap(), + <[u8; 32]>::from_hex( + "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/crypto/src/lib.rs b/crypto/src/lib.rs index d956e60dc..6d024877d 100644 --- a/crypto/src/lib.rs +++ b/crypto/src/lib.rs @@ -25,6 +25,7 @@ pub(crate) mod serde_utils; pub mod bls; pub mod ecdsa; +pub mod key; pub mod sighash; pub mod taproot; diff --git a/dash/src/crypto/key.rs b/dash/src/crypto/key.rs index 265c60576..b065b2ade 100644 --- a/dash/src/crypto/key.rs +++ b/dash/src/crypto/key.rs @@ -1,713 +1,22 @@ -// Rust Dash Library -// Originally written in 2014 by -// Andrew Poelstra -// For Dash -// Updated for Dash in 2022 by -// The Dash Core Developers // -// To the extent possible under law, the author(s) have dedicated all -// copyright and related and neighboring rights to this software to -// the public domain worldwide. This software is distributed without -// any warranty. -// -// You should have received a copy of the CC0 Public Domain Dedication -// along with this software. -// If not, see . +// This file is a part of rust-dashcore. +// Portions written by Andrew Poelstra for rust-bitcoin. +// SPDX-License-Identifier: CC0-1.0 +// See the accompanying file LICENSE or https://creativecommons.org/publicdomain/zero/1.0 // //! Dash keys. -//! -//! This module provides keys used in Dash that can be roundtrip -//! (de)serialized. - -use core::fmt::{self, Write}; -use core::ops; -use core::str::FromStr; - -use hashes::hex::FromHex; -use hashes::{Hash, hash160, hex}; -use internals::write_err; -pub use secp256k1::{self, Keypair, Parity, Secp256k1, Verification, XOnlyPublicKey, constants}; - -use crate::hash_types::{PubkeyHash, WPubkeyHash}; -use crate::network::constants::Network; -use crate::prelude::*; -use crate::{base58, io}; - -/// A key-related error. -#[derive(Clone, PartialEq, Eq, Debug)] -#[non_exhaustive] -pub enum Error { - /// Base58 encoding error - Base58(base58::Error), - /// secp256k1-related error - Secp256k1(secp256k1::Error), - /// Invalid key prefix error - InvalidKeyPrefix(u8), - /// The WIF or extended key version byte was not one we recognise. - InvalidAddressVersion(u8), - /// The base58 decoded correctly but the payload was the wrong length. - InvalidBase58PayloadLength(usize), - /// Hex decoding error - Hex(hex::Error), - /// `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 { - 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::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) - } - Error::NotSupported(string) => { - write!(f, "{}", string.as_str()) - } - } - } -} - -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(_) - | InvalidHexLength(_) => None, - NotSupported(_) => None, - } - } -} - -#[doc(hidden)] -impl From for Error { - fn from(e: base58::Error) -> 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::Error) -> Self { - Error::Hex(e) - } -} - -/// A Dash ECDSA public key -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PublicKey { - /// Whether this public key should be serialized as compressed - pub compressed: bool, - /// The actual ECDSA key - pub inner: secp256k1::PublicKey, -} - -impl PublicKey { - /// Constructs compressed ECDSA public key from the provided generic Secp256k1 public key - pub fn new(key: impl Into) -> PublicKey { - PublicKey { - compressed: true, - inner: key.into(), - } - } - - /// Constructs uncompressed (legacy) ECDSA public key from the provided generic Secp256k1 - /// public key - pub fn new_uncompressed(key: impl Into) -> PublicKey { - PublicKey { - compressed: false, - inner: key.into(), - } - } - - fn with_serialized R>(&self, f: F) -> R { - if self.compressed { - f(&self.inner.serialize()) - } else { - f(&self.inner.serialize_uncompressed()) - } - } - - /// Returns dash 160-bit hash of the public key - pub fn pubkey_hash(&self) -> PubkeyHash { - self.with_serialized(PubkeyHash::hash) - } - - /// 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 - } - } - - /// Write the public key into a writer - pub fn write_into(&self, mut writer: W) -> Result<(), io::Error> { - 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(); - self.write_into(&mut buf).expect("vecs don't error"); - 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::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() { - 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)); - } - }; - - Ok(PublicKey { - compressed, - inner, - }) - } - - /// Computes the public key as supposed to be used with this secret - pub fn from_private_key(sk: &PrivateKey) -> PublicKey { - sk.public_key() - } -} - -/// 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 { - // TODO: fast hex encoding - self.with_serialized(|bytes| { - for ch in bytes { - write!(f, "{:02x}", ch)?; - } - Ok(()) - }) - } -} - -impl FromStr for PublicKey { - type Err = Error; - fn from_str(s: &str) -> Result { - match s.len() { - 66 => PublicKey::from_slice(&<[u8; 33]>::from_hex(s)?), - 130 => PublicKey::from_slice(&<[u8; 65]>::from_hex(s)?), - len => Err(Error::InvalidHexLength(len)), - } - } -} - -impl From for PubkeyHash { - fn from(key: PublicKey) -> PubkeyHash { - key.pubkey_hash() - } -} - -/// A Dash ECDSA private key -#[derive(Copy, 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, -} - -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 { - PrivateKey { - compressed: true, - network, - inner: key, - } - } - - /// 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 { - PrivateKey { - compressed: false, - network, - inner: key, - } - } - - /// Creates a public key from this private key - pub fn public_key(&self) -> PublicKey { - PublicKey { - compressed: self.compressed, - inner: self.inner.public_key(), - } - } - - /// Serialize the private key to bytes - pub fn to_bytes(self) -> Vec { - self.inner[..].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))?; - 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)) - } - - /// Format the private key to WIF format. - pub fn fmt_wif(&self, fmt: &mut dyn Write) -> fmt::Result { - let mut ret = [0; 34]; - ret[0] = match self.network { - Network::Mainnet => 204, - Network::Testnet | Network::Devnet | Network::Regtest => 239, - }; - ret[1..33].copy_from_slice(&self.inner[..]); - let privkey = if self.compressed { - ret[33] = 1; - base58::encode_check(&ret[..]) - } else { - base58::encode_check(&ret[..33]) - }; - fmt.write_str(&privkey) - } - - /// Get WIF encoding of this private key. - pub fn to_wif(self) -> String { - let mut buf = String::new(); - buf.write_fmt(format_args!("{}", self)).unwrap(); - buf.shrink_to_fit(); - buf - } - - /// Parse WIF encoded private key. - pub fn from_wif(wif: &str) -> Result { - let data = base58::decode_check(wif)?; - - let compressed = match data.len() { - 33 => false, - 34 => true, - _ => { - return Err(Error::InvalidBase58PayloadLength(data.len())); - } - }; - let network = match data[0] { - 204 => Network::Mainnet, - 239 => Network::Testnet, - x => { - return Err(Error::InvalidAddressVersion(x)); - } - }; - - let secret = data[1..] - .first_chunk::<{ constants::SECRET_KEY_SIZE }>() - .ok_or(Error::InvalidBase58PayloadLength(data.len()))?; - - Ok(PrivateKey { - compressed, - network, - inner: secp256k1::SecretKey::from_secret_bytes(*secret)?, - }) - } -} - -impl fmt::Display for PrivateKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.fmt_wif(f) - } -} - -impl FromStr for PrivateKey { - type Err = Error; - fn from_str(s: &str) -> Result { - PrivateKey::from_wif(s) - } -} - -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 { - s.collect_str(self) - } -} - -#[cfg(feature = "serde")] -impl<'de> serde::Deserialize<'de> for PrivateKey { - fn deserialize>(d: D) -> Result { - struct WifVisitor; - - impl<'de> serde::de::Visitor<'de> for WifVisitor { - type Value = PrivateKey; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an ASCII WIF string") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - PrivateKey::from_str(v).map_err(E::custom) - } - - fn visit_bytes(self, v: &[u8]) -> Result - where - E: serde::de::Error, - { - if let Ok(s) = core::str::from_utf8(v) { - PrivateKey::from_str(s).map_err(E::custom) - } else { - Err(E::invalid_value(serde::de::Unexpected::Bytes(v), &self)) - } - } - } - - d.deserialize_str(WifVisitor) - } -} - -#[cfg(feature = "serde")] -#[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)) - } - } -} - -#[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) - } -} +pub use dashcore_crypto::key::*; #[cfg(test)] mod tests { use std::str::FromStr; - use hashes::hex::FromHex; - use super::*; use crate::Network::{Mainnet, Testnet}; use crate::address::Address; + use crate::crypto::key::{PrivateKey, PublicKey}; use crate::io; #[test] @@ -879,148 +188,6 @@ mod tests { assert!(PublicKey::read_from(io::Cursor::new(&[4; 64][..])).is_err()); } - #[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, - <[u8; 32]>::from_hex( - "ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8", - ) - .unwrap(), - [0_u8; 32], - ); - let expected2 = SortKey( - 4, - <[u8; 32]>::from_hex( - "ff12471208c14bd580709cb2358d98975247d8765f92bc25eab3b2763ed605f8", - ) - .unwrap(), - <[u8; 32]>::from_hex( - "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); - } - } - #[test] #[cfg(feature = "rand-std")] fn public_key_constructors() { diff --git a/dash/src/hash_types.rs b/dash/src/hash_types.rs index a750f4e3c..79f05471a 100644 --- a/dash/src/hash_types.rs +++ b/dash/src/hash_types.rs @@ -28,7 +28,8 @@ macro_rules! impl_hashencode { ($hashtype:ident) => { impl $crate::consensus::Encodable for $hashtype { fn consensus_encode(&self, w: &mut W) -> Result { - self.0.consensus_encode(w) + use $crate::hashes::Hash; + self.to_byte_array().consensus_encode(w) } } @@ -66,6 +67,7 @@ macro_rules! impl_asref_push_bytes { pub use newtypes::*; mod newtypes { + pub use dashcore_crypto::key::{PubkeyHash, WPubkeyHash}; use core::str::FromStr; use std::cmp::Ordering; @@ -97,12 +99,8 @@ mod newtypes { /// A dash witness transaction ID. pub struct Wtxid(sha256d::Hash); - /// A hash of a public key. - pub struct PubkeyHash(hash160::Hash); /// A hash of Dash Script bytecode. pub struct ScriptHash(hash160::Hash); - /// SegWit version of a public key hash. - pub struct WPubkeyHash(hash160::Hash); /// SegWit version of a Dash Script bytecode hash. pub struct WScriptHash(sha256::Hash);