diff --git a/CHANGELOG.md b/CHANGELOG.md index 12163765c..f8538e67a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `EcdsaPublicKey`, `EcdsaSecretKey`, `EcdsaSignature`, `EcdsaRecSignature`, `EcdsaError` and the `ECDSA_*_LEN` constants in `dashcore::ecdsa` - `EcdsaSecretKey::generate` with the `rand` feature +- `EcdsaSignature::is_low_s` - `TryFrom<&[u8]>` for `PublicKey` ### Changed @@ -71,6 +72,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). shape. Use `EddsaPublicKey::try_from`, `EddsaPkHash::from` and `EddsaSecretKey::public_key` instead of `EddsaPkBytes::validate`, `EddsaPkBytes::hash` and `EddsaSkBytes::public_key` +- **Breaking:** `dashcore-crypto` re-exports `dash-pkc` 0.2. + `EddsaPublicKey::verify` and `EddsaSecretKey::sign` are renamed to + `verify_message` and `sign_message`, the BLS and ECDSA types under + `dash_pkc` split signing and verification into `_digest` and `_message` + methods, and `dash_pkc`'s `BlsPkBytes`, `BlsSigBytes` and `BlsSkBytes` lose + their scheme parameter +- **Breaking:** `EcdsaPublicKey::verify`, and `signer::verify_data_signature` + with it, accept a high-S signature like its low-S twin, as Dash Core does. + Check `EcdsaSignature::is_low_s` to refuse malleable signatures +- **Breaking:** `dash-pow` replaces `rs-x11-hash` for X11, so + `dashcore_hashes` loses its `rs-x11-hash` feature and no longer needs a C + toolchain. X11 hashes inputs of any length, where `rs-x11-hash` always read + 80 bytes. AES runs in hardware when the build enables the target features + `dash-pow` needs, and in software otherwise + - Cargo ignores a dependency's profile overrides, so set `opt-level = 3` + under `[profile.dev.package.dash-pow]` to keep debug builds fast enough + for header sync - **Breaking:** `QualifiedMasternodeListEntry::entry_hash` and `MessageVerificationError::ThresholdSignatureNotValid` hold `Sha256dHash` - **Breaking:** `dashcore::base58` is `base58ck` 0.5.0, so `Base58CkString` diff --git a/Cargo.toml b/Cargo.toml index 34dff55b1..7c1413a1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,9 +12,10 @@ serde = { version = "1.0.219", default-features = false } zeroize = { version = "1.8", features = ["derive"] } # Packages from `dashpay/base-sdk`, MUST share same version ACROSS crates -dash-num = { git = "https://github.com/dashpay/base-sdk", rev = "d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5", default-features = false } -dash-pkc = { git = "https://github.com/dashpay/base-sdk", rev = "d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5", default-features = false } -dash-types = { git = "https://github.com/dashpay/base-sdk", rev = "d9e0d241e2e06edff455f88c9cb3b9ce0d8a8ac5", default-features = false } +dash-num = { git = "https://github.com/dashpay/base-sdk", rev = "addedfa16a56c77f435b41804cd2c0668b776e97", default-features = false } +dash-pkc = { git = "https://github.com/dashpay/base-sdk", rev = "addedfa16a56c77f435b41804cd2c0668b776e97", default-features = false } +dash-pow = { git = "https://github.com/dashpay/base-sdk", rev = "addedfa16a56c77f435b41804cd2c0668b776e97", default-features = false } +dash-types = { git = "https://github.com/dashpay/base-sdk", rev = "addedfa16a56c77f435b41804cd2c0668b776e97", default-features = false } # Packages from `rust-bitcoin/rust-bitcoin`, `bitcoin-crypto` uses revision due to `secp256k1` base58ck = { version = "0.5.0" } @@ -32,8 +33,6 @@ version = "0.46.0" [patch.crates-io] dashcore_hashes = { path = "hashes" } -# Use directly from GitHub while we don't have ownership of the crate on crates.io -rs-x11-hash = { git = "https://github.com/dashpay/rs-x11-hash", tag = "0.1.9" } [profile.release] # Default to unwinding for most crates @@ -46,3 +45,7 @@ debug = 1 [profile.dev] # Default to unwinding for most crates + +[profile.dev.package.dash-pow] +# Unoptimized X11 is slow enough to stall header sync +opt-level = 3 diff --git a/crypto/src/bls.rs b/crypto/src/bls.rs index 7255828a6..3fcd311ab 100644 --- a/crypto/src/bls.rs +++ b/crypto/src/bls.rs @@ -212,10 +212,10 @@ impl BlsPublicKey { match self.scheme { BlsScheme::Legacy => self .point::()? - .verify(digest, &signature.as_scheme(self.scheme).point::()?), + .verify_digest(digest, &signature.as_scheme(self.scheme).point::()?), BlsScheme::Modern => self .point::()? - .verify(&digest[..], &signature.as_scheme(self.scheme).point::()?), + .verify_digest(digest, &signature.as_scheme(self.scheme).point::()?), } .map_err(|_| BlsError::VerificationFailed("signature did not verify".to_string())) } @@ -398,8 +398,8 @@ impl BlsSignature { I: IntoIterator, { match self.scheme { - BlsScheme::Legacy => self.verify_secure_in::(digest, keys, digest), - BlsScheme::Modern => self.verify_secure_in::(digest, keys, &digest[..]), + BlsScheme::Legacy => self.verify_secure_in::(digest, keys), + BlsScheme::Modern => self.verify_secure_in::(digest, keys), } } @@ -407,17 +407,11 @@ impl BlsSignature { PkcSignature::::from_bytes(self.bytes.as_bytes()) .map_err(|_| BlsError::InvalidSignature(self.bytes.as_bytes().to_lower_hex_string())) } - fn verify_secure_in<'a, S, I>( - self, - digest: &[u8; 32], - keys: I, - msg: &S::Msg, - ) -> Result<(), BlsError> + fn verify_secure_in<'a, S, I>(self, digest: &[u8; 32], keys: I) -> Result<(), BlsError> where S: PkcScheme, I: IntoIterator, { - let _ = digest; let carried: Vec> = keys .into_iter() .filter_map(|(encoding, key)| { @@ -430,7 +424,7 @@ impl BlsSignature { let refs: Vec<&PkcPublicKey> = carried.iter().collect(); self.point::()? - .secure_verify_aggregates(msg, &refs) + .secure_verify_aggregates_digest(digest, &refs) .map_err(|_| BlsError::VerificationFailed("aggregate did not verify".to_string())) } } diff --git a/crypto/src/ecdsa.rs b/crypto/src/ecdsa.rs index 4fe4ea361..11087f11e 100644 --- a/crypto/src/ecdsa.rs +++ b/crypto/src/ecdsa.rs @@ -71,6 +71,11 @@ impl EcdsaPublicKey { pub fn add_tweak(&self, tweak: &[u8; ECDSA_SK_LEN]) -> Result; /// Checks `sig` over the 32-byte prehashed message. + /// + /// A high-S signature verifies like its low-S twin, as in Dash + /// Core. Callers refusing malleable signatures check + /// [`EcdsaSignature::is_low_s`]. + #[call(verify_digest)] pub fn verify( &self, msg_hash: &[u8; 32], @@ -154,6 +159,7 @@ impl EcdsaSecretKey { pub fn add_tweak(&self, tweak: &[u8; ECDSA_SK_LEN]) -> Result; /// Signs a 32-byte prehashed message (RFC 6979, low-S, low-R ground). + #[call(sign_digest)] #[expr(EcdsaSignature($))] pub fn sign(&self, msg_hash: &[u8; 32]) -> EcdsaSignature; @@ -216,6 +222,9 @@ impl EcdsaSignature { to self.0 { /// The compact (`r || s`) encoding. pub fn to_bytes(&self) -> [u8; ECDSA_SIG_LEN]; + + /// Whether S is in the lower half of the curve order. + pub fn is_low_s(&self) -> bool; } } @@ -273,7 +282,7 @@ impl EcdsaRecSignature { } fn parse(&self) -> Result { - PkcRecSigBytes::from_raw(self.0).ok_or(EcdsaError::InvalidRecoveryId)?.try_into() + PkcRecSigBytes::from_bytes(self.0).ok_or(EcdsaError::InvalidRecoveryId)?.try_into() } delegate! { @@ -295,6 +304,8 @@ impl EcdsaRecSignature { #[cfg(test)] mod tests { + use hex_conservative::hex; + use super::*; // A DER signature with `SIGHASH_ALL` appended. @@ -334,6 +345,31 @@ mod tests { } } + /// A high-S signature verifies like its low-S twin, and `is_low_s` tells + /// them apart. + #[test] + fn verify_accepts_high_s() { + const ORDER: [u8; 32] = + hex!("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + + let sk = EcdsaSecretKey::from_bytes(&[0x42; ECDSA_SK_LEN]).expect("valid secret key"); + let msg = [0x01; 32]; + let sig = sk.sign(&msg); + assert!(sig.is_low_s()); + + let mut high = sig.to_bytes(); + let mut borrow = 0; + for (s, n) in high[32..].iter_mut().zip(ORDER).rev() { + let (diff, b1) = n.overflowing_sub(*s); + let (diff, b2) = diff.overflowing_sub(borrow); + *s = diff; + borrow = u8::from(b1 || b2); + } + let high = EcdsaSignature::from_bytes(&high).expect("valid signature"); + assert!(!high.is_low_s()); + assert_eq!(sk.public_key().verify(&msg, high), Ok(())); + } + #[test] fn rec_signature_keeps_compression() { let sk = EcdsaSecretKey::from_bytes(&[0x42; ECDSA_SK_LEN]).expect("valid secret key"); diff --git a/hashes/Cargo.toml b/hashes/Cargo.toml index d380e95a6..a0b948cb1 100644 --- a/hashes/Cargo.toml +++ b/hashes/Cargo.toml @@ -16,7 +16,7 @@ version.workspace = true default = [] serde = ["dep:serde", "bitcoin_hashes/serde"] serde-std = ["serde", "serde/std"] -x11 = ["rs-x11-hash"] +x11 = ["dep:dash-pow"] [package.metadata.docs.rs] all-features = true @@ -26,9 +26,23 @@ rustdoc-args = ["--cfg", "docsrs"] bincode = { workspace = true, optional = true } bitcoin_hashes = { workspace = true, features = ["std"] } hex-conservative = { workspace = true } -rs-x11-hash = { version = "0.1.8", optional = true } serde = { workspace = true, optional = true } +# `dash-pow` supports hardware acceleration on platforms that meet system requirements. All other platforms must use `portable`. +dash-pow = { workspace = true, optional = true } + +[target.'cfg(all(target_arch = "aarch64", not(all(target_feature = "neon", target_feature = "aes"))))'.dependencies] +dash-pow = { workspace = true, optional = true, features = ["portable"] } + +[target.'cfg(all(target_arch = "x86_64", not(all(target_feature = "aes", target_feature = "avx", target_feature = "avx2", target_feature = "bmi1", target_feature = "bmi2", target_feature = "f16c", target_feature = "fma", target_feature = "lzcnt", target_feature = "movbe", target_feature = "xsave"))))'.dependencies] +dash-pow = { workspace = true, optional = true, features = ["portable"] } + +[target.'cfg(all(target_arch = "wasm32", not(target_feature = "simd128")))'.dependencies] +dash-pow = { workspace = true, optional = true, features = ["portable"] } + +[target.'cfg(not(any(target_arch = "aarch64", target_arch = "x86_64", target_arch = "wasm32")))'.dependencies] +dash-pow = { workspace = true, optional = true, features = ["portable"] } + [dev-dependencies] serde_test = "1.0" criterion = "0.5" @@ -45,5 +59,10 @@ harness = false name = "siphash" harness = false +[[bench]] +name = "x11" +harness = false +required-features = ["x11"] + [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(bench)', 'cfg(fuzzing)'] } diff --git a/hashes/benches/x11.rs b/hashes/benches/x11.rs new file mode 100644 index 000000000..b71dc53a1 --- /dev/null +++ b/hashes/benches/x11.rs @@ -0,0 +1,41 @@ +// +// This file is a part of rust-dashcore. +// SPDX-License-Identifier: CC0-1.0 +// See the accompanying file LICENSE or https://creativecommons.org/publicdomain/zero/1.0 +// + +//! Benchmarks for the proof of work scheme + +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use dashcore_hashes::{hash_x11, Hash, HashEngine}; + +fn bench_x11(c: &mut Criterion) { + let mut group = c.benchmark_group("x11"); + + for (size, label) in + [(32, "32b"), (64, "64b"), (80, "80b"), (128, "128b"), (1024, "1k"), (65536, "64k")] + { + group.throughput(Throughput::Bytes(size as u64)); + group.bench_with_input(BenchmarkId::from_parameter(label), &vec![1u8; size], |b, bytes| { + b.iter(|| hash_x11::Hash::hash(black_box(bytes))) + }); + } + + // A block header fed field by field, as consensus encoding does. + group.throughput(Throughput::Bytes(80)); + group.bench_function("header_fields", |b| { + let header = [1u8; 80]; + b.iter(|| { + let header = black_box(&header); + let mut engine = hash_x11::Hash::engine(); + for field in [0..4, 4..36, 36..68, 68..72, 72..76, 76..80] { + engine.input(&header[field]); + } + hash_x11::Hash::from_engine(engine) + }) + }); + group.finish(); +} + +criterion_group!(x11_benches, bench_x11); +criterion_main!(x11_benches); diff --git a/hashes/src/hash_x11.rs b/hashes/src/hash_x11.rs index 301739f2a..06811a7c5 100644 --- a/hashes/src/hash_x11.rs +++ b/hashes/src/hash_x11.rs @@ -1,28 +1,18 @@ -// Rust Dash Library -// Originally written in 2014 by -// Dmitrii Golubev -// 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. +// SPDX-License-Identifier: CC0-1.0 +// See the accompanying file LICENSE or https://creativecommons.org/publicdomain/zero/1.0 // -//! An implementation of a hash engine to support the X11 hash, -//! which is a wrapper around the rs-x11-hash library. +//! An implementation of a hash engine wrapper for the proof of work hash. use core::ops::Index; use core::slice::SliceIndex; use core::str; use std::io; -use std::vec::Vec; + +use dash_pow::__deps::digest::Digest; +use dash_pow::X11; use crate::{FromSliceError, HashEngine as _}; @@ -75,7 +65,7 @@ impl crate::Hash for Hash { const DISPLAY_BACKWARD: bool = true; fn from_engine(e: HashEngine) -> Self { - Hash(e.midstate()) + Hash(e.hasher.finalize().into()) } fn from_slice(sl: &[u8]) -> Result { @@ -101,10 +91,11 @@ impl crate::Hash for Hash { } } -/// An X11 hashing engine. X11 is not incremental, so the input is buffered. +/// An X11 hashing engine. #[derive(Clone, Default)] pub struct HashEngine { - buf: Vec, + hasher: X11, + length: usize, } impl crate::HashEngine for HashEngine { @@ -113,15 +104,16 @@ impl crate::HashEngine for HashEngine { const BLOCK_SIZE: usize = 32; fn midstate(&self) -> [u8; 32] { - rs_x11_hash::get_x11_hash(self.buf.as_slice()) + self.hasher.clone().finalize().into() } fn input(&mut self, data: &[u8]) { - self.buf.extend_from_slice(data); + self.hasher.update(data); + self.length += data.len(); } fn n_bytes_hashed(&self) -> usize { - self.buf.len() + self.length } } @@ -135,3 +127,25 @@ impl io::Write for HashEngine { Ok(()) } } + +#[cfg(test)] +mod tests { + use hex_conservative::hex; + use test_case::test_case; + + use crate::{hash_x11, Hash, HashEngine}; + + #[test_case(&[], hex!("51b572209083576ea221c27e62b4e22063257571ccb6cc3dc3cd17eb67584eba"); "empty")] + #[test_case(&hex!("020000002cc0081be5039a54b686d24d5d8747ee9770d9973ec1ace02e5c0500000000008d7139724b11c52995db4370284c998b9114154b120ad3486f1a360a1d4253d310d40e55b8f70a1be8e32300"), hex!("f29c0f286fd8071669286c6987eb941181134ff5f3978bf89f34070000000000"); "header")] + #[test_case(&(0..123).collect::>(), hex!("132ead25e64bf5cda54d198b3dc77bd7032e856f724dcd39c503ef3183462b5d"); "123 bytes")] + fn hash(data: &[u8], expected: [u8; 32]) { + assert_eq!(hash_x11::Hash::hash(data).to_byte_array(), expected); + + let mut engine = hash_x11::Hash::engine(); + let (head, tail) = data.split_at(data.len() / 2); + engine.input(head); + engine.input(tail); + assert_eq!(engine.n_bytes_hashed(), data.len()); + assert_eq!(hash_x11::Hash::from_engine(engine).to_byte_array(), expected); + } +}