Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand Down
13 changes: 8 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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
Expand All @@ -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
18 changes: 6 additions & 12 deletions crypto/src/bls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,10 @@ impl BlsPublicKey {
match self.scheme {
BlsScheme::Legacy => self
.point::<BlsScChia>()?
.verify(digest, &signature.as_scheme(self.scheme).point::<BlsScChia>()?),
.verify_digest(digest, &signature.as_scheme(self.scheme).point::<BlsScChia>()?),
BlsScheme::Modern => self
.point::<BlsScIetf>()?
.verify(&digest[..], &signature.as_scheme(self.scheme).point::<BlsScIetf>()?),
.verify_digest(digest, &signature.as_scheme(self.scheme).point::<BlsScIetf>()?),
}
.map_err(|_| BlsError::VerificationFailed("signature did not verify".to_string()))
}
Expand Down Expand Up @@ -398,26 +398,20 @@ impl BlsSignature {
I: IntoIterator<Item = (BlsScheme, &'a BlsPkBytes)>,
{
match self.scheme {
BlsScheme::Legacy => self.verify_secure_in::<BlsScChia, I>(digest, keys, digest),
BlsScheme::Modern => self.verify_secure_in::<BlsScIetf, I>(digest, keys, &digest[..]),
BlsScheme::Legacy => self.verify_secure_in::<BlsScChia, I>(digest, keys),
BlsScheme::Modern => self.verify_secure_in::<BlsScIetf, I>(digest, keys),
}
}

fn point<S: PkcScheme>(self) -> Result<PkcSignature<S>, BlsError> {
PkcSignature::<S>::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<Item = (BlsScheme, &'a BlsPkBytes)>,
{
let _ = digest;
let carried: Vec<PkcPublicKey<S>> = keys
.into_iter()
.filter_map(|(encoding, key)| {
Expand All @@ -430,7 +424,7 @@ impl BlsSignature {
let refs: Vec<&PkcPublicKey<S>> = carried.iter().collect();

self.point::<S>()?
.secure_verify_aggregates(msg, &refs)
.secure_verify_aggregates_digest(digest, &refs)
.map_err(|_| BlsError::VerificationFailed("aggregate did not verify".to_string()))
}
}
Expand Down
38 changes: 37 additions & 1 deletion crypto/src/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ impl EcdsaPublicKey {
pub fn add_tweak(&self, tweak: &[u8; ECDSA_SK_LEN]) -> Result<Self, EcdsaError>;

/// 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],
Expand Down Expand Up @@ -154,6 +159,7 @@ impl EcdsaSecretKey {
pub fn add_tweak(&self, tweak: &[u8; ECDSA_SK_LEN]) -> Result<Self, EcdsaError>;

/// 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;

Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -273,7 +282,7 @@ impl EcdsaRecSignature {
}

fn parse(&self) -> Result<PkcRecSignature, EcdsaError> {
PkcRecSigBytes::from_raw(self.0).ok_or(EcdsaError::InvalidRecoveryId)?.try_into()
PkcRecSigBytes::from_bytes(self.0).ok_or(EcdsaError::InvalidRecoveryId)?.try_into()
}

delegate! {
Expand All @@ -295,6 +304,8 @@ impl EcdsaRecSignature {

#[cfg(test)]
mod tests {
use hex_conservative::hex;

use super::*;

// A DER signature with `SIGHASH_ALL` appended.
Expand Down Expand Up @@ -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");
Expand Down
23 changes: 21 additions & 2 deletions hashes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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)'] }
41 changes: 41 additions & 0 deletions hashes/benches/x11.rs
Original file line number Diff line number Diff line change
@@ -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);
60 changes: 37 additions & 23 deletions hashes/src/hash_x11.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
// Rust Dash Library
// Originally written in 2014 by
// Dmitrii Golubev <dmitrii.golubev@dash.org>
// 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 <http://creativecommons.org/publicdomain/zero/1.0/>.
// 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 _};

Expand Down Expand Up @@ -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<Self, FromSliceError> {
Expand All @@ -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<u8>,
hasher: X11,
length: usize,
}

impl crate::HashEngine for HashEngine {
Expand All @@ -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
}
}

Expand All @@ -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::<Vec<u8>>(), 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);
}
}
Loading