diff --git a/.github/ci-groups.yml b/.github/ci-groups.yml index aae748262..59e114a86 100644 --- a/.github/ci-groups.yml +++ b/.github/ci-groups.yml @@ -4,6 +4,7 @@ groups: core: - dashcore + - dashcore-crypto - dashcore_hashes - dashcore-private - dash-network diff --git a/Cargo.toml b/Cargo.toml index 83db5f348..f362e2aa5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["dash", "dash-network", "hashes", "internals", "fuzz", "rpc-client", "rpc-json", "rpc-integration-test", "key-wallet", "key-wallet-manager", "key-wallet-ffi", "dash-spv", "dash-spv-ffi", "git-state", "dash-network-seeds", "masternode-seeds-fetcher", "dash-spv-bench"] +members = ["dash", "crypto", "dash-network", "hashes", "internals", "fuzz", "rpc-client", "rpc-json", "rpc-integration-test", "key-wallet", "key-wallet-manager", "key-wallet-ffi", "dash-spv", "dash-spv-ffi", "git-state", "dash-network-seeds", "masternode-seeds-fetcher", "dash-spv-bench"] resolver = "2" [workspace.dependencies] diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml new file mode 100644 index 000000000..87ce32809 --- /dev/null +++ b/crypto/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "dashcore-crypto" +version = { workspace = true } +authors = ["The Dash Core Developers"] +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"] +edition = "2021" + +[features] +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"] + +[dependencies] +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 } +dash-types = { version = "=0.1.0-beta", default-features = false, features = ["codec"] } +dashcore_hashes = { path = "../hashes" } +hex = { version = "0.4" } +internals = { path = "../internals", package = "dashcore-private" } +serde = { version = "1.0.219", default-features = false, features = [ "derive", "alloc" ], optional = true } +thiserror = "2" +tracing = { version = "0.1", optional = true } + +[lints.rust] +unexpected_cfgs = { level = "deny", check-cfg = ['cfg(bench)', 'cfg(fuzzing)', 'cfg(kani)'] } + +[dev-dependencies] +hex_lit = { version = "0.1.1", features = ["rust_v_1_46"] } diff --git a/crypto/src/bls.rs b/crypto/src/bls.rs new file mode 100644 index 000000000..c1b140339 --- /dev/null +++ b/crypto/src/bls.rs @@ -0,0 +1,652 @@ +// +// 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 +// + +//! BLS12-381 public key and signatures. + +use core::str::FromStr; + +#[cfg(feature = "bls")] +use dash_pkc::__deps::ff::PrimeField; +#[cfg(feature = "bls")] +use dash_pkc::bls::{ + BlsPublicKey as PkcPublicKey, BlsScChia, BlsScIetf, BlsScheme as PkcScheme, + BlsSecretKey as PkcSecretKey, BlsSignature as PkcSignature, Fr, +}; +use dash_types::{make_bytes, make_sbytes, type_cvrt}; +use hex::FromHexError; +#[cfg(feature = "bls")] +use thiserror::Error as ThisError; +#[cfg(feature = "bls")] +use tracing::error; + +/// Raw BLS public key length (G1 compressed). +pub const BLS_PK_LEN: usize = 48; + +/// Raw BLS secret key length (big-endian scalar). +pub const BLS_SK_LEN: usize = 32; + +/// Raw BLS signature length (G2 compressed). +pub const BLS_SIG_LEN: usize = 96; + +/// Errors produced by BLS operations. +#[cfg(feature = "bls")] +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, ThisError)] +pub enum BlsError { + /// Public key bytes are not a valid G1 point. + #[error("Invalid BLS public key: {0}")] + InvalidPublicKey(String), + + /// Signature bytes are not a valid G2 point. + #[error("Invalid BLS signature: {0}")] + InvalidSignature(String), + + /// Signature verification failed. + #[error("BLS verification failed: {0}")] + VerificationFailed(String), + + /// Secret key bytes are not a valid scalar. + #[error("Invalid BLS secret key")] + InvalidSecretKey, + + /// Tweak is not a valid scalar. + #[error("Invalid BLS tweak")] + InvalidTweak, +} + +/// Which BLS scheme a 48- or 96-byte blob was written under. +#[cfg(feature = "bls")] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum BlsScheme { + /// The pre-V19 legacy scheme. + Legacy, + /// The post-V19 basic scheme. + Modern, +} + +/// Reduces 32 big-endian bytes to the scalar they denote. +#[cfg(feature = "bls")] +fn reduce(bytes: &[u8; BLS_SK_LEN]) -> Result<[u8; BLS_SK_LEN], BlsError> { + let reduced = Fr::from_bendian_reduce(bytes).map_err(|_| BlsError::InvalidTweak)?; + + // `to_repr` is little-endian; these bytes are big-endian. + let mut out = [0u8; BLS_SK_LEN]; + out.copy_from_slice(reduced.to_repr().as_ref()); + out.reverse(); + + Ok(out) +} + +make_bytes! { + /// BLS public key (48 bytes, unvalidated). + BlsPkBytes, BLS_PK_LEN +} + +impl BlsPkBytes { + /// Pairs these bytes with `scheme`. + #[cfg(feature = "bls")] + pub fn as_scheme(self, scheme: BlsScheme) -> BlsPublicKey { + BlsPublicKey { + bytes: self, + scheme, + } + } + + /// Reads these bytes from a hex string. + pub fn from_hex(s: &str) -> Result { + let mut bytes = [0u8; BLS_PK_LEN]; + hex::decode_to_slice(s, &mut bytes)?; + Ok(Self::from_bytes(bytes)) + } + + /// Returns `true` when every byte is zero. + pub fn is_zeroed(&self) -> bool { + self.is_null() + } +} + +impl FromStr for BlsPkBytes { + type Err = FromHexError; + + fn from_str(s: &str) -> Result { + Self::from_hex(s) + } +} + +#[cfg(feature = "bincode")] +impl bincode::Encode for BlsPkBytes { + fn encode( + &self, + encoder: &mut E, + ) -> Result<(), bincode::error::EncodeError> { + bincode::Encode::encode(self.as_bytes(), encoder) + } +} + +#[cfg(feature = "bincode")] +impl bincode::Decode for BlsPkBytes { + fn decode>( + decoder: &mut D, + ) -> Result { + <[u8; BLS_PK_LEN] as bincode::Decode>::decode(decoder).map(Self::from_bytes) + } +} + +#[cfg(feature = "bincode")] +impl<'de, C> bincode::BorrowDecode<'de, C> for BlsPkBytes { + fn borrow_decode>( + decoder: &mut D, + ) -> Result { + >::decode(decoder) + } +} + +type_cvrt!( + for[] TryFrom<&[u8]> for BlsPkBytes, + core::array::TryFromSliceError, + |v| Ok(Self::from_bytes(<[u8; BLS_PK_LEN]>::try_from(*v)?)) +); + +/// A [`BlsPkBytes`] paired with the scheme to read it under. +#[cfg(feature = "bls")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BlsPublicKey { + bytes: BlsPkBytes, + scheme: BlsScheme, +} + +#[cfg(feature = "bls")] +impl BlsPublicKey { + /// Adds `tweak * G` to the point, written back under the same scheme. + /// + /// # Errors + /// + /// Returns `InvalidPublicKey` when the bytes are not a G1 point, or + /// `InvalidTweak` when the tweak is not a valid scalar. + pub fn add_tweak(self, tweak: &[u8; 32]) -> Result { + let sum = match self.scheme { + BlsScheme::Legacy => self + .point::()? + .add_tweak(&reduce(tweak)?) + .map_err(|_| BlsError::InvalidTweak)? + .to_bytes(), + BlsScheme::Modern => self + .point::()? + .add_tweak(&reduce(tweak)?) + .map_err(|_| BlsError::InvalidTweak)? + .to_bytes(), + }; + + Ok(BlsPkBytes::from_bytes(sum)) + } + + /// Checks the bytes are a point and writes it back under the same scheme. + /// + /// # Errors + /// + /// Returns `InvalidPublicKey` when the bytes are not a G1 point. + pub fn canonicalize(self) -> Result { + self.reencode(self.scheme) + } + + /// Re-encodes the same point under `to`. + /// + /// # Errors + /// + /// Returns `InvalidPublicKey` when the bytes are not a G1 point under the + /// scheme they were read with. + pub fn reencode(self, to: BlsScheme) -> Result { + let bytes = match (self.scheme, to) { + (BlsScheme::Legacy, BlsScheme::Legacy) => self.point::()?.to_bytes(), + (BlsScheme::Legacy, BlsScheme::Modern) => self + .point::()? + .to_scheme::() + .map_err(|e| BlsError::InvalidPublicKey(e.to_string()))? + .to_bytes(), + (BlsScheme::Modern, BlsScheme::Legacy) => self + .point::()? + .to_scheme::() + .map_err(|e| BlsError::InvalidPublicKey(e.to_string()))? + .to_bytes(), + (BlsScheme::Modern, BlsScheme::Modern) => self.point::()?.to_bytes(), + }; + + Ok(BlsPkBytes::from_bytes(bytes)) + } + + /// Verifies `signature` over a 32-byte message digest. + /// + /// The signature is read under this same scheme. + /// + /// # Errors + /// + /// Returns `InvalidPublicKey` or `InvalidSignature` when either side is + /// not a curve point, or `VerificationFailed` when it does not verify. + pub fn verify(self, digest: &[u8; 32], signature: &BlsSigBytes) -> Result<(), BlsError> { + match self.scheme { + BlsScheme::Legacy => self + .point::()? + .verify(digest, &signature.as_scheme(self.scheme).point::()?), + BlsScheme::Modern => self + .point::()? + .verify(&digest[..], &signature.as_scheme(self.scheme).point::()?), + } + .map_err(|_| BlsError::VerificationFailed("signature did not verify".to_string())) + } + + /// Reads the key in its own encoding and carries it to `S`. + fn carry_to(self) -> Result, BlsError> { + match self.scheme { + BlsScheme::Legacy => self.point::()?.to_scheme::(), + BlsScheme::Modern => self.point::()?.to_scheme::(), + } + .map_err(|e| BlsError::InvalidPublicKey(e.to_string())) + } + + fn point(self) -> Result, BlsError> { + PkcPublicKey::::from_bytes(self.bytes.as_bytes()) + .map_err(|e| BlsError::InvalidPublicKey(e.to_string())) + } +} + +make_sbytes! { + /// BLS secret key bytes (32 big-endian bytes, unvalidated). + BlsSkBytes, BLS_SK_LEN +} + +#[cfg(feature = "bls")] +impl BlsSkBytes { + /// Pairs these bytes with `scheme`. + pub fn as_scheme(&self, scheme: BlsScheme) -> BlsSecretKey<'_> { + BlsSecretKey { + bytes: self, + scheme, + } + } +} + +/// A [`BlsSkBytes`] paired with the scheme to operate under. +#[cfg(feature = "bls")] +#[derive(Clone, Copy, Debug)] +pub struct BlsSecretKey<'a> { + bytes: &'a BlsSkBytes, + scheme: BlsScheme, +} + +#[cfg(feature = "bls")] +impl BlsSecretKey<'_> { + /// Adds `tweak` to the scalar, for hardened derivation. + /// + /// # Errors + /// + /// Returns `InvalidSecretKey` when the bytes are not a valid scalar, or + /// `InvalidTweak` when the tweak or the sum is not one. + pub fn add_tweak(self, tweak: &[u8; 32]) -> Result { + let sum = match self.scheme { + BlsScheme::Legacy => *self + .scalar::()? + .add_tweak(&reduce(tweak)?) + .map_err(|_| BlsError::InvalidTweak)? + .to_bytes(), + BlsScheme::Modern => *self + .scalar::()? + .add_tweak(&reduce(tweak)?) + .map_err(|_| BlsError::InvalidTweak)? + .to_bytes(), + }; + + Ok(BlsSkBytes::from_bytes(sum)) + } + + /// Reduces the scalar modulo the group order and writes it back. + /// + /// # Errors + /// + /// Returns `InvalidSecretKey` when the bytes cannot be reduced. + pub fn canonicalize(self) -> Result { + reduce(self.bytes.as_bytes()) + .map(BlsSkBytes::from_bytes) + .map_err(|_| BlsError::InvalidSecretKey) + } + + /// Derives the public key, written under the scheme. + /// + /// # Errors + /// + /// Returns `InvalidSecretKey` when the bytes are not a valid scalar. + pub fn public_key(self) -> Result { + match self.scheme { + BlsScheme::Legacy => { + Ok(BlsPkBytes::from_bytes(self.scalar::()?.public_key().to_bytes())) + } + BlsScheme::Modern => { + Ok(BlsPkBytes::from_bytes(self.scalar::()?.public_key().to_bytes())) + } + } + } + + fn scalar(self) -> Result, BlsError> { + PkcSecretKey::::from_bytes(self.bytes.as_bytes()).map_err(|_| BlsError::InvalidSecretKey) + } +} + +make_bytes! { + /// BLS signature (96 bytes, unvalidated). + BlsSigBytes, BLS_SIG_LEN +} + +impl BlsSigBytes { + /// Pairs these bytes with `scheme`. + #[cfg(feature = "bls")] + pub fn as_scheme(self, scheme: BlsScheme) -> BlsSignature { + BlsSignature { + bytes: self, + scheme, + } + } + + /// Reads these bytes from a hex string. + pub fn from_hex(s: &str) -> Result { + let mut bytes = [0u8; BLS_SIG_LEN]; + hex::decode_to_slice(s, &mut bytes)?; + Ok(Self::from_bytes(bytes)) + } + + /// Returns `true` when every byte is zero. + pub fn is_zeroed(&self) -> bool { + self.is_null() + } +} + +impl FromStr for BlsSigBytes { + type Err = FromHexError; + + fn from_str(s: &str) -> Result { + Self::from_hex(s) + } +} + +#[cfg(feature = "bincode")] +impl bincode::Encode for BlsSigBytes { + fn encode( + &self, + encoder: &mut E, + ) -> Result<(), bincode::error::EncodeError> { + bincode::Encode::encode(self.as_bytes(), encoder) + } +} + +#[cfg(feature = "bincode")] +impl bincode::Decode for BlsSigBytes { + fn decode>( + decoder: &mut D, + ) -> Result { + <[u8; BLS_SIG_LEN] as bincode::Decode>::decode(decoder).map(Self::from_bytes) + } +} + +#[cfg(feature = "bincode")] +impl<'de, C> bincode::BorrowDecode<'de, C> for BlsSigBytes { + fn borrow_decode>( + decoder: &mut D, + ) -> Result { + >::decode(decoder) + } +} + +type_cvrt!( + for[] TryFrom<&[u8]> for BlsSigBytes, + core::array::TryFromSliceError, + |v| Ok(Self::from_bytes(<[u8; BLS_SIG_LEN]>::try_from(*v)?)) +); + +/// A [`BlsSigBytes`] paired with the scheme to read it under. +#[cfg(feature = "bls")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct BlsSignature { + bytes: BlsSigBytes, + scheme: BlsScheme, +} + +#[cfg(feature = "bls")] +impl BlsSignature { + /// Verifies this aggregate against `keys` with rogue-key binding. + /// + /// Each key carries the scheme its own encoding was written with, which + /// for a masternode list entry follows that entry's version. The scheme + /// the aggregate verifies in is one value for the quorum. + /// + /// # Errors + /// + /// Returns `InvalidSignature` when the signature bytes are not a G2 + /// point, or `VerificationFailed` when the aggregate does not verify. + /// + /// A key that will not decode is dropped rather than reported, though the + /// failure is logged. + pub fn verify_secure_aggregate<'a, I>(self, digest: &[u8; 32], keys: I) -> Result<(), BlsError> + where + I: IntoIterator, + { + match self.scheme { + BlsScheme::Legacy => self.verify_secure_in::(digest, keys, digest), + BlsScheme::Modern => self.verify_secure_in::(digest, keys, &digest[..]), + } + } + + fn point(self) -> Result, BlsError> { + PkcSignature::::from_bytes(self.bytes.as_bytes()) + .map_err(|_| BlsError::InvalidSignature(hex::encode(self.bytes.as_bytes()))) + } + fn verify_secure_in<'a, S, I>( + self, + digest: &[u8; 32], + keys: I, + msg: &S::Msg, + ) -> Result<(), BlsError> + where + S: PkcScheme, + I: IntoIterator, + { + let _ = digest; + let carried: Vec> = keys + .into_iter() + .filter_map(|(encoding, key)| { + key.as_scheme(encoding) + .carry_to::() + .inspect_err(|e| error!("Failed to deserialize operator key: {}", e)) + .ok() + }) + .collect(); + let refs: Vec<&PkcPublicKey> = carried.iter().collect(); + + self.point::()? + .secure_verify_aggregates(msg, &refs) + .map_err(|_| BlsError::VerificationFailed("aggregate did not verify".to_string())) + } +} + +#[cfg(all(test, feature = "bls"))] +mod tests { + use hex_lit::hex; + + /// Operator public keys from the mainnet quorum at height 2300832. + const OPERATOR_KEYS: [[u8; 48]; 3] = [ + hex!("86e7ea34cc084da3ed0e90649ad444df0ca25d638164a596b4fbec9567bbcf3e635a8d8457107e7fe76326f3816e34d9"), + hex!("8b02bec7d70bb6c386ef4e201f3c01d062902079920cb037d7257110f9b6112ecad30cf20daf373813a816b0df845cfa"), + hex!("8455cd00d19792377ac915614b06cc46f161662aaab1d5f1e73f3c3cac48a1f2991d75ba14decb308294ceaf7185ef21"), + ]; + + /// Quorum public key for the ChainLock at height 2301027. + const QUORUM_PUBKEY: [u8; 48] = hex!("880d92cdfdcb2def08ee224b036dac1c52d39443c82576bfa2b9fe215265bffa129b936653bc655c3668d73c977d2e5a"); + + /// ChainLock signature from height 2301027. + const CHAINLOCK_SIG: [u8; 96] = hex!("ad47488b86dc296b4cc582afe99e7e32489e0f7840e40ebfb4ea959481caf757575f7a7e9c388c21b16d7c9979d4906d000fe14851dbc42e89802bab0932ac40b8cbad2076da9365e1587d53d1dec3f25a776c2fe0de2fca87e9c03408809181"); + + /// The block ChainLock at height 2301027 covers. + const CHAINLOCK_BLOCK_HASH: [u8; 32] = + hex!("00000000000000029eabbaa19ca5f694b863b3f64a682c376fa50b4119ae0029"); + + #[cfg(test)] + mod compatibility_tests { + use super::super::*; + use super::{CHAINLOCK_BLOCK_HASH, CHAINLOCK_SIG, OPERATOR_KEYS, QUORUM_PUBKEY}; + + #[test] + fn test_real_operator_key_compatibility() { + // Test modern format deserialization + for (i, key_bytes) in OPERATOR_KEYS.iter().enumerate() { + let pk = + BlsPkBytes::from_bytes(*key_bytes).as_scheme(BlsScheme::Modern).canonicalize(); + assert!(pk.is_ok(), "Modern format deserialization failed for key {}", i); + } + } + + #[test] + fn test_chainlock_signature_format() { + let sig = BlsSigBytes::from_bytes(CHAINLOCK_SIG) + .as_scheme(BlsScheme::Modern) + .point::(); + assert!(sig.is_ok(), "ChainLock signature deserialization failed"); + } + + #[test] + fn test_quorum_public_key_verification() { + // Parse keys + let _pk = BlsPkBytes::from_bytes(QUORUM_PUBKEY) + .as_scheme(BlsScheme::Modern) + .canonicalize() + .unwrap(); + let _sig = BlsSigBytes::from_bytes(CHAINLOCK_SIG) + .as_scheme(BlsScheme::Modern) + .point::() + .unwrap(); + + // According to DIP-8, ChainLocks sign: + // SHA256(llmqType, quorumHash, SHA256(height), blockHash) + // + // Since we don't have the quorum hash and exact LLMQ type for this test data, + // we'll skip this test but document why it fails. + // + // To properly test this, we would need: + // - llmqType (likely LLMQ_400_60 for ChainLocks) + // - quorumHash (the hash identifying the specific quorum) + // - height (2301027 based on the comment) + // - blockHash (which we have) + + println!( + "SKIPPING: ChainLock verification requires composite message format per DIP-8" + ); + println!("Message should be: SHA256(llmqType, quorumHash, SHA256(height), blockHash)"); + println!("We only have the block hash, not the other required components."); + + // Comment out the assertion since we know it will fail without proper message construction + // assert!(verified.is_ok(), "Real chainlock signature should verify"); + } + + #[test] + fn test_verify_secure_with_real_operators() { + // Real operator keys for testing the secure aggregate API + let operator_keys = OPERATOR_KEYS.map(BlsPkBytes::from_bytes); + + for key in &operator_keys { + assert!(key.as_scheme(BlsScheme::Modern).canonicalize().is_ok()); + } + + // Note: For a complete test, we would need the actual commitment hash and aggregated signature + // from the quorum formation process. This test verifies the API works with real keys. + println!( + "Successfully parsed {} real operator keys for verify_secure", + operator_keys.len() + ); + } + + #[test] + fn debug_chainlock_verification() { + // Try both schemes for the quorum key + let key = BlsPkBytes::from_bytes(QUORUM_PUBKEY); + println!("Trying modern scheme for quorum key..."); + let pk_modern = key.as_scheme(BlsScheme::Modern).canonicalize(); + println!("Modern scheme result: {:?}", pk_modern.is_ok()); + + println!("\nTrying legacy scheme for quorum key..."); + let pk_legacy = key.as_scheme(BlsScheme::Legacy).canonicalize(); + println!("Legacy scheme result: {:?}", pk_legacy.is_ok()); + + // Whichever reads, the signature is checked under the same scheme + for scheme in [BlsScheme::Modern, BlsScheme::Legacy] { + let verified = key + .as_scheme(scheme) + .verify(&CHAINLOCK_BLOCK_HASH, &BlsSigBytes::from_bytes(CHAINLOCK_SIG)); + println!("{:?} verification: {:?}", scheme, verified.is_ok()); + } + } + + #[test] + fn test_legacy_format_detection() { + // Test the ability to detect and handle legacy format keys + // Note: To properly test this, we need actual legacy format keys from older blocks + // The detection logic should try legacy format when modern format fails + + let test_key = BlsPkBytes::from_bytes(OPERATOR_KEYS[0]); + + // Try modern format first + let modern_result = test_key.as_scheme(BlsScheme::Modern).canonicalize(); + + // If modern fails, try legacy + if modern_result.is_err() { + let legacy_result = test_key.as_scheme(BlsScheme::Legacy).canonicalize(); + println!("Key requires legacy format: {}", legacy_result.is_ok()); + } else { + println!("Key uses modern format"); + } + } + } + + #[cfg(test)] + mod benchmarks { + use super::super::*; + use super::{CHAINLOCK_SIG, OPERATOR_KEYS}; + use hex_lit::hex; + use std::time::Instant; + + #[test] + fn bench_verify_secure() { + // Setup test data - real operator keys + let operator_keys = OPERATOR_KEYS.map(BlsPkBytes::from_bytes); + + // Create a dummy signature for benchmarking + let sig = BlsSigBytes::from_bytes(CHAINLOCK_SIG); + + // A 32-byte digest, since verification takes the message pre-hashed + let msg = hex!("74657374206d65737361676520666f722062656e63686d61726b696e67000000"); + + let run = || { + let _ = sig.as_scheme(BlsScheme::Modern).verify_secure_aggregate( + &msg, + operator_keys.iter().map(|k| (BlsScheme::Modern, k)), + ); + }; + + // Warm up + for _ in 0..10 { + run(); + } + + // Measure verification time + let iterations = 100; + let start = Instant::now(); + + for _ in 0..iterations { + run(); + } + + let duration = start.elapsed(); + + println!("{} verify_secure operations took: {:?}", iterations, duration); + println!("Average per operation: {:?}", duration / iterations); + println!("Operations per second: {:.2}", iterations as f64 / duration.as_secs_f64()); + } + } +} diff --git a/crypto/src/lib.rs b/crypto/src/lib.rs new file mode 100644 index 000000000..ce3fc450f --- /dev/null +++ b/crypto/src/lib.rs @@ -0,0 +1,20 @@ +// +// 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 +// + +//! Cryptographic primitives shared by rust-dashcore crates + +#![doc(html_logo_url = "https://media.dash.org/wp-content/uploads/dash-d-logo.svg")] +#![doc(html_favicon_url = "https://media.dash.org/wp-content/uploads/dash-d-logo.svg")] + +extern crate alloc; + +#[cfg(feature = "bls")] +pub extern crate dash_pkc; +pub extern crate dashcore_hashes as hashes; +#[cfg(feature = "serde")] +pub extern crate serde; + +pub mod bls; diff --git a/dash/Cargo.toml b/dash/Cargo.toml index ebb1b64e7..65ebd8e99 100644 --- a/dash/Cargo.toml +++ b/dash/Cargo.toml @@ -23,16 +23,16 @@ default = ["secp-recovery", "bincode" ] base64 = [ "base64-compat" ] rand-std = ["secp256k1/rand"] rand = ["secp256k1/rand"] -serde = ["dep:serde", "dashcore_hashes/serde", "secp256k1/serde", "dash-network/serde"] +serde = ["dep:serde", "dashcore-crypto/serde", "dashcore_hashes/serde", "secp256k1/serde", "dash-network/serde"] secp-lowmemory = ["secp256k1/lowmemory"] secp-recovery = ["secp256k1/recovery"] signer = ["secp-recovery", "rand", "base64"] core-block-hash-use-x11 = ["dashcore_hashes/x11"] -bls = ["blsful"] +bls = ["dashcore-crypto/bls"] eddsa = ["ed25519-dalek"] quorum_validation = ["bls"] message_verification = ["bls"] -bincode = [ "dep:bincode", "dep:bincode_derive", "dashcore_hashes/bincode", "dash-network/bincode" ] +bincode = [ "dep:bincode", "dep:bincode_derive", "dashcore-crypto/bincode", "dashcore_hashes/bincode", "dash-network/bincode" ] qrinfo-capture = ["bincode"] test-utils = [] @@ -44,9 +44,10 @@ rustdoc-args = ["--cfg", "docsrs"] internals = { path = "../internals", package = "dashcore-private" } bech32 = { version = "0.9.1" } dashcore_hashes = { path = "../hashes" } +dashcore-crypto = { path = "../crypto" } dash-network = { path = "../dash-network" } +dash-types = { version = "=0.1.0-beta", default-features = false, features = ["codec"] } secp256k1 = { version = "0.33.1" } -rustversion = { version="1.0.20"} serde = { version = "1.0.219", default-features = false, features = [ "derive", "alloc" ], optional = true } base64-compat = { version = "1.0.0", optional = true } @@ -56,7 +57,6 @@ anyhow = { version= "1.0" } hex = { version= "0.4" } bincode = { workspace = true, optional = true } bincode_derive = { workspace = true, optional = true } -blsful = { git = "https://github.com/dashpay/agora-blsful", rev = "0c34a7a488a0bd1c9a9a2196e793b303ad35c900", optional = true } ed25519-dalek = { version = "2.1", features = ["rand_core"], optional = true } blake3 = "1.8.1" thiserror = "2" diff --git a/dash/src/base58.rs b/dash/src/base58.rs index 297700ca6..4ca76d68d 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), - /// blsful related error - #[cfg(feature = "blsful")] - BLSError(String), - /// edwards 25519 related error #[cfg(feature = "ed25519-dalek")] Ed25519Dalek(String), @@ -81,8 +77,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 = "blsful")] - Error::BLSError(ref e) => write!(f, "BLS 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), @@ -422,8 +416,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 = "blsful")] - key::Error::BLSError(e) => Error::BLSError(e), #[cfg(feature = "ed25519-dalek")] key::Error::Ed25519Dalek(e) => Error::Ed25519Dalek(e), key::Error::NotSupported(e) => Error::NotSupported(e), diff --git a/dash/src/bls_sig_utils.rs b/dash/src/bls_sig_utils.rs index 376689396..ccdf1354f 100644 --- a/dash/src/bls_sig_utils.rs +++ b/dash/src/bls_sig_utils.rs @@ -1,213 +1,14 @@ -// Rust Dash Library -// Written by -// The Rust Dash 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. +// 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 // -// You should have received a copy of the CC0 Public Domain Dedication -// along with this software. -// If not, see . -// - -//! Dash BLS elements -//! Convenience wrappers around fixed size arrays of 48 and 96 bytes representing the public key -//! and signature. -//! - -#[cfg(feature = "bincode")] -use bincode::{Decode, Encode}; -#[cfg(feature = "bls")] -use blsful::{Bls12381G2Impl, Pairing}; -use hex::{FromHexError, ToHex}; -use internals::impl_array_newtype; - -use crate::core::fmt; -use crate::internal_macros::impl_bytes_newtype; -use crate::prelude::String; -#[cfg(feature = "bls")] -use crate::sml::quorum_validation_error::QuorumValidationError; - -/// A BLS Public key is 48 bytes in the scheme used for Dash Core -#[rustversion::attr(since(1.48), derive(PartialEq, Eq, Ord, PartialOrd, Hash))] -#[derive(Clone, Copy, Debug)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -pub struct BLSPublicKey([u8; 48]); - -impl BLSPublicKey { - pub fn is_zeroed(&self) -> bool { - self.0 == [0; 48] - } -} - -impl_array_newtype!(BLSPublicKey, u8, 48); - -#[cfg(feature = "bls")] -impl TryFrom for blsful::PublicKey { - type Error = QuorumValidationError; - - fn try_from(value: BLSPublicKey) -> Result { - Self::try_from(value.0.as_slice()) - .map_err(|e| QuorumValidationError::InvalidBLSPublicKey(e.to_string())) - } -} - -#[cfg(feature = "bls")] -impl TryFrom<&BLSPublicKey> for blsful::PublicKey { - type Error = QuorumValidationError; - - fn try_from(value: &BLSPublicKey) -> Result { - Self::try_from(value.0.as_slice()) - .map_err(|e| QuorumValidationError::InvalidBLSPublicKey(e.to_string())) - } -} - -impl BLSPublicKey { - /// Create a new BLS Public Key from a hex string - pub fn from_hex(s: &str) -> Result { - hex::decode(s).map(|v| { - let mut payload: [u8; 48] = [0; 48]; - payload.copy_from_slice(v.as_slice()); - Self(payload) - }) - } -} - -#[cfg(feature = "serde")] -crate::serde_utils::serde_string_impl!(BLSPublicKey, "a BLS Public Key"); - -impl core::str::FromStr for BLSPublicKey { - type Err = FromHexError; - - fn from_str(s: &str) -> Result { - BLSPublicKey::from_hex(s) - } -} - -impl fmt::Display for BLSPublicKey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.encode_hex::()) - } -} - -/// A BLS Signature is 96 bytes in the scheme used for Dash Core -#[rustversion::attr(since(1.48), derive(PartialEq, Eq, Ord, PartialOrd, Hash))] -#[derive(Clone, Copy)] -#[cfg_attr(feature = "bincode", derive(Encode, Decode))] -pub struct BLSSignature([u8; 96]); - -impl BLSSignature { - pub fn is_zeroed(&self) -> bool { - self.0 == [0; 96] - } -} - -#[cfg(feature = "bls")] -impl TryFrom for blsful::Signature { - type Error = QuorumValidationError; - - fn try_from(value: BLSSignature) -> Result { - let Some(g2_element) = - ::Signature::from_compressed(&value.to_bytes()) - .into_option() - else { - return Err(QuorumValidationError::InvalidBLSSignature(hex::encode(value.to_bytes()))); - // We should not error because the signature could be given by an invalid source - }; - - Ok(blsful::Signature::Basic(g2_element)) - } -} - -#[cfg(feature = "bls")] -impl TryFrom<&BLSSignature> for blsful::Signature { - type Error = QuorumValidationError; - - fn try_from(value: &BLSSignature) -> Result { - let Some(g2_element) = - ::Signature::from_compressed(&value.to_bytes()) - .into_option() - else { - return Err(QuorumValidationError::InvalidBLSSignature(hex::encode(value.to_bytes()))); - // We should not error because the signature could be given by an invalid source - }; - - Ok(blsful::Signature::Basic(g2_element)) - } -} -#[cfg(feature = "bls")] -impl TryFrom for blsful::MultiSignature { - type Error = QuorumValidationError; +//! BLS12-381 public key and signatures. - fn try_from(value: BLSSignature) -> Result { - let Some(g2_element) = - ::Signature::from_compressed(&value.to_bytes()) - .into_option() - else { - return Err(QuorumValidationError::InvalidBLSSignature(hex::encode(value.to_bytes()))); - // We should not error because the signature could be given by an invalid source - }; - - Ok(blsful::MultiSignature::Basic(g2_element)) - } -} - -#[cfg(feature = "bls")] -impl TryFrom<&BLSSignature> for blsful::MultiSignature { - type Error = QuorumValidationError; - - fn try_from(value: &BLSSignature) -> Result { - let Some(g2_element) = - ::Signature::from_compressed(&value.to_bytes()) - .into_option() - else { - return Err(QuorumValidationError::InvalidBLSSignature(hex::encode(value.to_bytes()))); - // We should not error because the signature could be given by an invalid source - }; - - Ok(blsful::MultiSignature::Basic(g2_element)) - } -} - -#[cfg(feature = "bls")] -impl TryFrom for blsful::AggregateSignature { - type Error = QuorumValidationError; - - fn try_from(value: BLSSignature) -> Result { - let Some(g2_element) = - ::Signature::from_compressed(&value.to_bytes()) - .into_option() - else { - return Err(QuorumValidationError::InvalidBLSSignature(hex::encode(value.to_bytes()))); - // We should not error because the signature could be given by an invalid source - }; - - Ok(blsful::AggregateSignature::Basic(g2_element)) - } -} - -#[cfg(feature = "bls")] -impl TryFrom<&BLSSignature> for blsful::AggregateSignature { - type Error = QuorumValidationError; - - fn try_from(value: &BLSSignature) -> Result { - let Some(g2_element) = - ::Signature::from_compressed(&value.to_bytes()) - .into_option() - else { - return Err(QuorumValidationError::InvalidBLSSignature(hex::encode(value.to_bytes()))); - // We should not error because the signature could be given by an invalid source - }; - - Ok(blsful::AggregateSignature::Basic(g2_element)) - } -} - -impl_array_newtype!(BLSSignature, u8, 96); -impl_bytes_newtype!(BLSSignature, 96); +pub use dashcore_crypto::bls::BlsPkBytes as BLSPublicKey; +pub use dashcore_crypto::bls::BlsSigBytes as BLSSignature; +pub use dashcore_crypto::bls::*; macro_rules! impl_elementencode { ($element:ident, $len:expr) => { @@ -216,7 +17,7 @@ macro_rules! impl_elementencode { &self, w: &mut W, ) -> Result { - self.0.consensus_encode(w) + self.as_bytes().consensus_encode(w) } } @@ -226,57 +27,11 @@ macro_rules! impl_elementencode { ) -> Result { let mut data: [u8; $len] = [0u8; $len]; r.read_exact(&mut data)?; - Ok($element(data)) - } - } - }; -} - -#[rustversion::before(1.48)] -macro_rules! impl_eq_ord_hash { - ($element:ident, $len:expr) => { - #[rustversion::before(1.48)] - impl core::hash::Hash for $element { - fn hash(&self, state: &mut H) { - self.0.to_vec().hash(state) - } - } - - #[rustversion::before(1.48)] - impl core::cmp::PartialEq<$element> for $element { - fn eq(&self, other: &$element) -> bool { - for i in 0..$len { - if self[i] != other[i] { - return false; - } - } - true - } - } - - #[rustversion::before(1.48)] - impl core::cmp::Eq for $element {} - - #[rustversion::before(1.48)] - impl core::cmp::PartialOrd for $element { - fn partial_cmp(&self, other: &Self) -> Option { - self.0.to_vec().partial_cmp(&other.0.to_vec()) - } - } - - #[rustversion::before(1.48)] - impl core::cmp::Ord for $element { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.0.to_vec().cmp(&other.0.to_vec()) + Ok($element::from_bytes(data)) } } }; } -#[rustversion::before(1.48)] -impl_eq_ord_hash!(BLSPublicKey, 48); -#[rustversion::before(1.48)] -impl_eq_ord_hash!(BLSSignature, 96); - impl_elementencode!(BLSPublicKey, 48); impl_elementencode!(BLSSignature, 96); diff --git a/dash/src/crypto/key.rs b/dash/src/crypto/key.rs index d2a8e315d..3b0cff47d 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), - /// bls related error - #[cfg(feature = "blsful")] - BLSError(String), /// edwards 25519 related error #[cfg(feature = "ed25519-dalek")] Ed25519Dalek(String), @@ -72,8 +69,6 @@ impl fmt::Display for Error { Error::NotSupported(string) => { write!(f, "{}", string.as_str()) } - #[cfg(feature = "blsful")] - Error::BLSError(string) => write!(f, "{}", string.as_str()), #[cfg(feature = "ed25519-dalek")] Error::Ed25519Dalek(string) => write!(f, "{}", string.as_str()), } @@ -90,8 +85,6 @@ impl std::error::Error for Error { Hex(e) => Some(e), InvalidKeyPrefix(_) | InvalidHexLength(_) => None, NotSupported(_) => None, - #[cfg(feature = "blsful")] - BLSError(_) => None, #[cfg(feature = "ed25519-dalek")] Ed25519Dalek(_) => None, } diff --git a/dash/src/lib.rs b/dash/src/lib.rs index 85b527b89..65b45d09a 100644 --- a/dash/src/lib.rs +++ b/dash/src/lib.rs @@ -69,8 +69,6 @@ pub extern crate bitcoinconsensus; pub extern crate dashcore_hashes as hashes; pub extern crate secp256k1; -#[cfg(feature = "blsful")] -pub use blsful; #[cfg(feature = "ed25519-dalek")] pub use ed25519_dalek; diff --git a/dash/src/sml/masternode_list_engine/message_request_verification.rs b/dash/src/sml/masternode_list_engine/message_request_verification.rs index 662626ace..de684d1dc 100644 --- a/dash/src/sml/masternode_list_engine/message_request_verification.rs +++ b/dash/src/sml/masternode_list_engine/message_request_verification.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use hashes::Hash; +use crate::bls_sig_utils::BlsScheme; use crate::hash_types::QuorumOrderingHash; use crate::sml::llmq_type::network::NetworkLLMQExt; use crate::sml::masternode_list::MasternodeList; @@ -205,7 +206,11 @@ impl MasternodeListEngine { instant_lock.signature ); - match quorum.verify_message_digest(sign_id.to_byte_array(), instant_lock.signature) { + match quorum.verify_message_digest( + sign_id.to_byte_array(), + instant_lock.signature, + BlsScheme::Modern, + ) { Ok(()) => { tracing::info!( "IS lock verified: txid={}, quorum_index={}, quorum_hash={}", @@ -411,7 +416,11 @@ impl MasternodeListEngine { ) .map_err(|e| e.to_string())?; - quorum.verify_message_digest(sign_id.to_byte_array(), chain_lock.signature) + quorum.verify_message_digest( + sign_id.to_byte_array(), + chain_lock.signature, + BlsScheme::Modern, + ) } } @@ -420,7 +429,6 @@ mod tests { use crate::bls_sig_utils::BLSSignature; use crate::consensus::deserialize; use crate::hashes::Hash; - use crate::hashes::hex::FromHex; use crate::sml::llmq_type::LLMQType; use crate::sml::masternode_list_engine::MasternodeListEngine; use crate::{BlockHash, ChainLock, InstantLock, QuorumHash}; diff --git a/dash/src/sml/masternode_list_engine/validation.rs b/dash/src/sml/masternode_list_engine/validation.rs index 0083ed73d..ad9af7a3b 100644 --- a/dash/src/sml/masternode_list_engine/validation.rs +++ b/dash/src/sml/masternode_list_engine/validation.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use crate::QuorumHash; +use crate::bls_sig_utils::BlsScheme; use crate::sml::llmq_entry_verification::LLMQEntryVerificationStatus; use crate::sml::masternode_list_engine::MasternodeListEngine; use crate::sml::masternode_list_entry::qualified_masternode_list_entry::QualifiedMasternodeListEntry; @@ -30,15 +31,16 @@ impl MasternodeListEngine { quorum.quorum_entry.validate_structure()?; let masternodes = self.find_valid_masternodes_for_quorum(quorum)?; - quorum.validate(masternodes.iter().enumerate().filter_map( - |(i, qualified_masternode_list_entry)| { + quorum.validate( + masternodes.iter().enumerate().filter_map(|(i, qualified_masternode_list_entry)| { if *quorum.quorum_entry.signers.get(i)? { Some(&qualified_masternode_list_entry.masternode_list_entry) } else { None } - }, - )) + }), + BlsScheme::Modern, + ) } pub fn validate_rotation_cycle_quorums( @@ -66,15 +68,18 @@ impl MasternodeListEngine { ))? .as_ref() .map_err(Clone::clone)?; - quorum.validate(masternodes.iter().enumerate().filter_map( - |(i, qualified_masternode_list_entry)| { - if *quorum.quorum_entry.signers.get(i)? { - Some(&qualified_masternode_list_entry.masternode_list_entry) - } else { - None - } - }, - ))?; + quorum.validate( + masternodes.iter().enumerate().filter_map( + |(i, qualified_masternode_list_entry)| { + if *quorum.quorum_entry.signers.get(i)? { + Some(&qualified_masternode_list_entry.masternode_list_entry) + } else { + None + } + }, + ), + BlsScheme::Modern, + )?; } Ok(()) } @@ -132,15 +137,18 @@ impl MasternodeListEngine { continue; } }; - match quorum.validate(masternodes.iter().enumerate().filter_map( - |(i, qualified_masternode_list_entry)| { - if *quorum.quorum_entry.signers.get(i)? { - Some(&qualified_masternode_list_entry.masternode_list_entry) - } else { - None - } - }, - )) { + match quorum.validate( + masternodes.iter().enumerate().filter_map( + |(i, qualified_masternode_list_entry)| { + if *quorum.quorum_entry.signers.get(i)? { + Some(&qualified_masternode_list_entry.masternode_list_entry) + } else { + None + } + }, + ), + BlsScheme::Modern, + ) { Ok(_) => { return_statuses.insert( quorum.quorum_entry.quorum_hash, diff --git a/dash/src/sml/message_verification_error.rs b/dash/src/sml/message_verification_error.rs index ce475dc9f..6a64f73c9 100644 --- a/dash/src/sml/message_verification_error.rs +++ b/dash/src/sml/message_verification_error.rs @@ -70,3 +70,20 @@ impl From for MessageVerificationError { } } } + +#[cfg(feature = "bls")] +impl From for MessageVerificationError { + fn from(e: crate::bls_sig_utils::BlsError) -> Self { + use crate::bls_sig_utils::BlsError; + + match e { + BlsError::InvalidPublicKey(s) => Self::InvalidBLSPublicKey(s), + BlsError::InvalidSignature(s) => Self::InvalidBLSSignature(s), + BlsError::InvalidSecretKey => { + Self::InvalidBLSPublicKey("invalid secret key".to_string()) + } + BlsError::InvalidTweak => Self::InvalidBLSPublicKey("invalid tweak".to_string()), + BlsError::VerificationFailed(s) => Self::InvalidBLSSignature(s), + } + } +} diff --git a/dash/src/sml/quorum_entry/validation.rs b/dash/src/sml/quorum_entry/validation.rs index df8979e61..bf4b4f62e 100644 --- a/dash/src/sml/quorum_entry/validation.rs +++ b/dash/src/sml/quorum_entry/validation.rs @@ -1,9 +1,8 @@ +use crate::bls_sig_utils::BlsScheme; use crate::sml::masternode_list_entry::MasternodeListEntry; use crate::sml::quorum_entry::qualified_quorum_entry::QualifiedQuorumEntry; use crate::sml::quorum_validation_error::QuorumValidationError; -use blsful::{Bls12381G2Impl, PublicKey, SerializationFormat, Signature}; use hashes::Hash; -use tracing::error; impl QualifiedQuorumEntry { /// Verifies the aggregated commitment signature for the quorum. @@ -23,48 +22,35 @@ impl QualifiedQuorumEntry { /// # Notes /// /// * Supports both legacy and modern BLS key formats. - /// * Uses `blsful` with secure aggregated verification. + /// * Applies the rogue-key binding of secure aggregated verification. pub fn verify_aggregated_commitment_signature<'a, I>( &self, operator_keys: I, + scheme: BlsScheme, ) -> Result<(), QuorumValidationError> where I: IntoIterator, { let message = self.commitment_hash.to_byte_array(); - let message = message.as_slice(); - // Collect public keys with proper legacy/modern deserialization - let public_keys: Vec> = operator_keys - .into_iter() - .filter_map(|masternode_list_entry| { - let bytes = masternode_list_entry.operator_public_key.as_ref(); - let is_legacy = masternode_list_entry.use_legacy_bls_keys(); - - let format = if is_legacy { - SerializationFormat::Legacy - } else { - SerializationFormat::Modern - }; - let result = PublicKey::::from_bytes_with_mode(bytes, format); - - match result { - Ok(public_key) => Some(public_key), - Err(e) => { - error!("Failed to deserialize operator key: {}", e); - None - } - } + // A key's encoding follows its own entry's version; the scheme the + // aggregate is verified in is one value for the whole quorum. + let keys = operator_keys.into_iter().map(|entry| { + let encoding = if entry.use_legacy_bls_keys() { + BlsScheme::Legacy + } else { + BlsScheme::Modern + }; + (encoding, &entry.operator_public_key) + }); + + self.quorum_entry + .all_commitment_aggregated_signature + .as_scheme(scheme) + .verify_secure_aggregate(&message, keys) + .map_err(|e| { + QuorumValidationError::AllCommitmentAggregatedSignatureNotValid(e.to_string()) }) - .collect(); - - // Deserialize the aggregated signature - let signature: Signature = - self.quorum_entry.all_commitment_aggregated_signature.try_into()?; - - signature.verify_secure(&public_keys, message).map_err(|e| { - QuorumValidationError::AllCommitmentAggregatedSignatureNotValid(e.to_string()) - }) } /// Verifies the quorum's threshold signature. @@ -79,16 +65,12 @@ impl QualifiedQuorumEntry { /// /// # Notes /// - /// * Uses `blsful::Signature` and `blsful::PublicKey` for verification. - /// * Converts the quorum's public key and signature into `blsful` types before verification. - pub fn verify_quorum_signature(&self) -> Result<(), QuorumValidationError> { - let message = &self.commitment_hash; - let public_key: blsful::PublicKey = - self.quorum_entry.quorum_public_key.try_into()?; - let signature: blsful::Signature = - self.quorum_entry.threshold_sig.try_into()?; - signature - .verify(&public_key, message) + /// * Reads the quorum's public key and signature under `scheme`. + pub fn verify_quorum_signature(&self, scheme: BlsScheme) -> Result<(), QuorumValidationError> { + self.quorum_entry + .quorum_public_key + .as_scheme(scheme) + .verify(&self.commitment_hash.to_byte_array(), &self.quorum_entry.threshold_sig) .map_err(|e| QuorumValidationError::ThresholdSignatureNotValid(e.to_string())) } @@ -111,317 +93,17 @@ impl QualifiedQuorumEntry { /// /// * Calls `verify_aggregated_commitment_signature` first. /// * Calls `verify_quorum_signature` second. - pub fn validate<'a, I>(&self, valid_masternodes: I) -> Result<(), QuorumValidationError> + pub fn validate<'a, I>( + &self, + valid_masternodes: I, + scheme: BlsScheme, + ) -> Result<(), QuorumValidationError> where I: IntoIterator, { - self.verify_aggregated_commitment_signature(valid_masternodes)?; - self.verify_quorum_signature()?; + self.verify_aggregated_commitment_signature(valid_masternodes, scheme)?; + self.verify_quorum_signature(scheme)?; Ok(()) } } - -#[cfg(test)] -mod tests { - #[cfg(test)] - mod compatibility_tests { - use super::super::*; - use blsful::{Bls12381G2Impl, PublicKey, Signature, SignatureSchemes}; - use hex_lit::hex; - - #[test] - fn test_real_operator_key_compatibility() { - // Real operator public keys from mainnet quorum at height 2300832 - let real_keys = [ - hex!( - "86e7ea34cc084da3ed0e90649ad444df0ca25d638164a596b4fbec9567bbcf3e635a8d8457107e7fe76326f3816e34d9" - ), - hex!( - "8b02bec7d70bb6c386ef4e201f3c01d062902079920cb037d7257110f9b6112ecad30cf20daf373813a816b0df845cfa" - ), - hex!( - "8455cd00d19792377ac915614b06cc46f161662aaab1d5f1e73f3c3cac48a1f2991d75ba14decb308294ceaf7185ef21" - ), - ]; - - // Test modern format deserialization - for (i, key_bytes) in real_keys.iter().enumerate() { - let pk = PublicKey::::from_bytes_with_mode( - key_bytes, - SerializationFormat::Modern, - ); - assert!(pk.is_ok(), "Modern format deserialization failed for key {}", i); - } - } - - #[test] - fn test_chainlock_signature_format() { - // Real ChainLock signature from height 2301027 - let chainlock_sig = hex!( - "ad47488b86dc296b4cc582afe99e7e32489e0f7840e40ebfb4ea959481caf757575f7a7e9c388c21b16d7c9979d4906d000fe14851dbc42e89802bab0932ac40b8cbad2076da9365e1587d53d1dec3f25a776c2fe0de2fca87e9c03408809181" - ); - - let sig = Signature::::from_bytes_with_mode( - &chainlock_sig, - SignatureSchemes::Basic, - SerializationFormat::Modern, // Assume modern format for chainlock - ); - assert!(sig.is_ok(), "ChainLock signature deserialization failed"); - } - - #[test] - fn test_quorum_public_key_verification() { - // Real quorum public key and chainlock data - let quorum_pubkey = hex!( - "880d92cdfdcb2def08ee224b036dac1c52d39443c82576bfa2b9fe215265bffa129b936653bc655c3668d73c977d2e5a" - ); - let chainlock_sig = hex!( - "ad47488b86dc296b4cc582afe99e7e32489e0f7840e40ebfb4ea959481caf757575f7a7e9c388c21b16d7c9979d4906d000fe14851dbc42e89802bab0932ac40b8cbad2076da9365e1587d53d1dec3f25a776c2fe0de2fca87e9c03408809181" - ); - let _block_hash = - hex!("00000000000000029eabbaa19ca5f694b863b3f64a682c376fa50b4119ae0029"); - - // Parse keys - let _pk = PublicKey::::from_bytes_with_mode( - &quorum_pubkey, - SerializationFormat::Modern, - ) - .unwrap(); - let _sig = Signature::::from_bytes_with_mode( - &chainlock_sig, - SignatureSchemes::Basic, - SerializationFormat::Modern, // Assume modern format - ) - .unwrap(); - - // According to DIP-8, ChainLocks sign: - // SHA256(llmqType, quorumHash, SHA256(height), blockHash) - // - // Since we don't have the quorum hash and exact LLMQ type for this test data, - // we'll skip this test but document why it fails. - // - // To properly test this, we would need: - // - llmqType (likely LLMQ_400_60 for ChainLocks) - // - quorumHash (the hash identifying the specific quorum) - // - height (2301027 based on the comment) - // - blockHash (which we have) - - println!( - "SKIPPING: ChainLock verification requires composite message format per DIP-8" - ); - println!("Message should be: SHA256(llmqType, quorumHash, SHA256(height), blockHash)"); - println!("We only have the block hash, not the other required components."); - - // Comment out the assertion since we know it will fail without proper message construction - // assert!(verified.is_ok(), "Real chainlock signature should verify"); - } - - #[test] - fn test_verify_secure_with_real_operators() { - // Real operator keys for testing verify_secure API - let operator_keys = [ - PublicKey::::from_bytes_with_mode( - &hex!("86e7ea34cc084da3ed0e90649ad444df0ca25d638164a596b4fbec9567bbcf3e635a8d8457107e7fe76326f3816e34d9"), - SerializationFormat::Modern - ).unwrap(), - PublicKey::::from_bytes_with_mode( - &hex!("8b02bec7d70bb6c386ef4e201f3c01d062902079920cb037d7257110f9b6112ecad30cf20daf373813a816b0df845cfa"), - SerializationFormat::Modern - ).unwrap(), - PublicKey::::from_bytes_with_mode( - &hex!("8455cd00d19792377ac915614b06cc46f161662aaab1d5f1e73f3c3cac48a1f2991d75ba14decb308294ceaf7185ef21"), - SerializationFormat::Modern - ).unwrap(), - ]; - - // Note: For a complete test, we would need the actual commitment hash and aggregated signature - // from the quorum formation process. This test verifies the API works with real keys. - println!( - "Successfully parsed {} real operator keys for verify_secure", - operator_keys.len() - ); - } - - #[test] - fn debug_chainlock_verification() { - let quorum_pubkey = hex!( - "880d92cdfdcb2def08ee224b036dac1c52d39443c82576bfa2b9fe215265bffa129b936653bc655c3668d73c977d2e5a" - ); - let chainlock_sig = hex!( - "ad47488b86dc296b4cc582afe99e7e32489e0f7840e40ebfb4ea959481caf757575f7a7e9c388c21b16d7c9979d4906d000fe14851dbc42e89802bab0932ac40b8cbad2076da9365e1587d53d1dec3f25a776c2fe0de2fca87e9c03408809181" - ); - let block_hash = - hex!("00000000000000029eabbaa19ca5f694b863b3f64a682c376fa50b4119ae0029"); - - // Try both legacy and modern formats for the quorum key - println!("Trying modern format for quorum key..."); - let pk_modern = PublicKey::::from_bytes_with_mode( - &quorum_pubkey, - SerializationFormat::Modern, - ); - println!("Modern format result: {:?}", pk_modern.is_ok()); - - println!("\nTrying legacy format for quorum key..."); - let pk_legacy = PublicKey::::from_bytes_with_mode( - &quorum_pubkey, - SerializationFormat::Legacy, - ); - println!("Legacy format result: {:?}", pk_legacy.is_ok()); - - // Use whichever succeeded (prefer modern, then legacy) - let pk = pk_modern.or(pk_legacy); - - // If we get a valid key, try signature with different formats - if let Ok(pk) = pk { - println!("\nGot valid public key, trying signature formats..."); - - // Try modern format signature - println!("\nTrying modern format signature..."); - let sig_modern = Signature::::from_bytes_with_mode( - &chainlock_sig, - SignatureSchemes::Basic, - SerializationFormat::Modern, - ); - match &sig_modern { - Ok(_) => println!("Modern signature deserialization: OK"), - Err(e) => println!("Modern signature deserialization failed: {:?}", e), - } - - if let Ok(sig) = sig_modern { - let result = sig.verify(&pk, &block_hash); - println!("Verification with modern sig format: {:?}", result); - - // Try with reversed block hash (endianness) - let mut reversed_hash = block_hash; - reversed_hash.reverse(); - let result_reversed = sig.verify(&pk, &reversed_hash); - println!("Verification with reversed block hash: {:?}", result_reversed); - } - - // Try legacy format signature - println!("\nTrying legacy format signature..."); - let sig_legacy = Signature::::from_bytes_with_mode( - &chainlock_sig, - SignatureSchemes::Basic, - SerializationFormat::Legacy, - ); - match &sig_legacy { - Ok(_) => println!("Legacy signature deserialization: OK"), - Err(e) => println!("Legacy signature deserialization failed: {:?}", e), - } - - if let Ok(sig) = sig_legacy { - let result = sig.verify(&pk, &block_hash); - println!("Verification with legacy sig format: {:?}", result); - } - } else { - println!("Failed to deserialize public key in any format!"); - } - } - - #[test] - fn test_legacy_format_detection() { - // Test the ability to detect and handle legacy format keys - // Note: To properly test this, we need actual legacy format keys from older blocks - // The detection logic should try legacy format when modern format fails - - let test_key = hex!( - "86e7ea34cc084da3ed0e90649ad444df0ca25d638164a596b4fbec9567bbcf3e635a8d8457107e7fe76326f3816e34d9" - ); - - // Try modern format first - let modern_result = PublicKey::::from_bytes_with_mode( - &test_key, - SerializationFormat::Modern, - ); - - // If modern fails, try legacy - if modern_result.is_err() { - let legacy_result = PublicKey::::from_bytes_with_mode( - &test_key, - SerializationFormat::Legacy, - ); - println!("Key requires legacy format: {}", legacy_result.is_ok()); - } else { - println!("Key uses modern format"); - } - } - } - - #[cfg(test)] - mod benchmarks { - use super::super::*; - use blsful::{ - Bls12381G2Impl, PublicKey, Signature, SignatureSchemes, verify_secure_basic_with_mode, - }; - use hex_lit::hex; - use std::time::Instant; - - #[test] - fn bench_verify_secure() { - // Setup test data - real operator keys - let operator_keys = vec![ - PublicKey::::from_bytes_with_mode( - &hex!("86e7ea34cc084da3ed0e90649ad444df0ca25d638164a596b4fbec9567bbcf3e635a8d8457107e7fe76326f3816e34d9"), - SerializationFormat::Modern - ).unwrap(), - PublicKey::::from_bytes_with_mode( - &hex!("8b02bec7d70bb6c386ef4e201f3c01d062902079920cb037d7257110f9b6112ecad30cf20daf373813a816b0df845cfa"), - SerializationFormat::Modern - ).unwrap(), - PublicKey::::from_bytes_with_mode( - &hex!("8455cd00d19792377ac915614b06cc46f161662aaab1d5f1e73f3c3cac48a1f2991d75ba14decb308294ceaf7185ef21"), - SerializationFormat::Modern - ).unwrap(), - ]; - - // Create a dummy signature for benchmarking - let sig_bytes = hex!( - "ad47488b86dc296b4cc582afe99e7e32489e0f7840e40ebfb4ea959481caf757575f7a7e9c388c21b16d7c9979d4906d000fe14851dbc42e89802bab0932ac40b8cbad2076da9365e1587d53d1dec3f25a776c2fe0de2fca87e9c03408809181" - ); - let sig = Signature::::from_bytes_with_mode( - &sig_bytes, - SignatureSchemes::Basic, - SerializationFormat::Modern, - ) - .unwrap(); - - let inner_sig = match sig { - Signature::Basic(s) => s, - _ => panic!("Expected Basic signature"), - }; - - let msg = b"test message for benchmarking"; - - // Warm up - for _ in 0..10 { - let _ = verify_secure_basic_with_mode::( - &operator_keys, - inner_sig, - msg, - SerializationFormat::Modern, - ); - } - - // Measure verification time - let iterations = 100; - let start = Instant::now(); - - for _ in 0..iterations { - let _ = verify_secure_basic_with_mode::( - &operator_keys, - inner_sig, - msg, - SerializationFormat::Modern, - ); - } - - let duration = start.elapsed(); - - println!("{} verify_secure operations took: {:?}", iterations, duration); - println!("Average per operation: {:?}", duration / iterations); - println!("Operations per second: {:.2}", iterations as f64 / duration.as_secs_f64()); - } - } -} diff --git a/dash/src/sml/quorum_entry/verify_message.rs b/dash/src/sml/quorum_entry/verify_message.rs index 4b652d7ee..c95095f8c 100644 --- a/dash/src/sml/quorum_entry/verify_message.rs +++ b/dash/src/sml/quorum_entry/verify_message.rs @@ -1,7 +1,6 @@ -use blsful::Bls12381G2Impl; use hashes::{Hash, sha256d}; -use crate::bls_sig_utils::BLSSignature; +use crate::bls_sig_utils::{BLSSignature, BlsScheme}; use crate::sml::message_verification_error::MessageVerificationError; use crate::sml::quorum_entry::qualified_quorum_entry::QualifiedQuorumEntry; @@ -25,14 +24,14 @@ impl QualifiedQuorumEntry { /// # Errors /// /// Returns `MessageVerificationError::ThresholdSignatureNotValid` if: - /// - The quorum's public key cannot be converted to the required `blsful::PublicKey`. - /// - The provided signature cannot be converted to `blsful::Signature`. + /// - The quorum's public key cannot be read under `scheme`. + /// - The provided signature cannot be read under `scheme`. /// - The BLS verification process determines that the signature is invalid. /// /// # Implementation Details /// - /// - The function retrieves the quorum's public key and attempts to convert it into the expected `blsful::PublicKey` type. - /// - It converts the provided `BLSSignature` into a `blsful::Signature`. + /// - The function reads the quorum's public key in the scheme the caller names. + /// - It reads the provided `BLSSignature` in that same scheme. /// - It then calls the `verify` method, which checks if the signature is valid for the given message digest. /// - If verification fails, it returns a `MessageVerificationError::ThresholdSignatureNotValid` with relevant details. /// @@ -40,19 +39,21 @@ impl QualifiedQuorumEntry { &self, message_digest: [u8; 32], signature: BLSSignature, + scheme: BlsScheme, ) -> Result<(), MessageVerificationError> { - let public_key: blsful::PublicKey = - self.quorum_entry.quorum_public_key.try_into()?; - let bls_signature: blsful::Signature = signature.try_into()?; - bls_signature.verify(&public_key, message_digest).map_err(|e| { - MessageVerificationError::ThresholdSignatureNotValid( - Box::new(signature), - Box::new(sha256d::Hash::from_byte_array(message_digest)), - Box::new(self.quorum_entry.quorum_public_key), - self.quorum_entry.quorum_hash, - self.quorum_entry.llmq_type, - e.to_string(), - ) - }) + self.quorum_entry + .quorum_public_key + .as_scheme(scheme) + .verify(&message_digest, &signature) + .map_err(|e| { + MessageVerificationError::ThresholdSignatureNotValid( + Box::new(signature), + Box::new(sha256d::Hash::from_byte_array(message_digest)), + Box::new(self.quorum_entry.quorum_public_key), + self.quorum_entry.quorum_hash, + self.quorum_entry.llmq_type, + e.to_string(), + ) + }) } } diff --git a/dash/src/sml/quorum_validation_error.rs b/dash/src/sml/quorum_validation_error.rs index b8f895d71..e24bb88eb 100644 --- a/dash/src/sml/quorum_validation_error.rs +++ b/dash/src/sml/quorum_validation_error.rs @@ -111,3 +111,20 @@ impl From for QuorumValidationError { QuorumValidationError::SMLError(value) } } + +#[cfg(feature = "bls")] +impl From for QuorumValidationError { + fn from(e: crate::bls_sig_utils::BlsError) -> Self { + use crate::bls_sig_utils::BlsError; + + match e { + BlsError::InvalidPublicKey(s) => Self::InvalidBLSPublicKey(s), + BlsError::InvalidSignature(s) => Self::InvalidBLSSignature(s), + BlsError::InvalidSecretKey => { + Self::InvalidBLSPublicKey("invalid secret key".to_string()) + } + BlsError::InvalidTweak => Self::InvalidBLSPublicKey("invalid tweak".to_string()), + BlsError::VerificationFailed(s) => Self::ThresholdSignatureNotValid(s), + } + } +} diff --git a/deny.toml b/deny.toml index 7c296296e..d9c7eb3fa 100644 --- a/deny.toml +++ b/deny.toml @@ -30,6 +30,7 @@ allow = [ "MPL-2.0", "Unicode-3.0", "MITNFA", + "BSL-1.0", ] confidence-threshold = 0.8 diff --git a/key-wallet-ffi/src/account_derivation.rs b/key-wallet-ffi/src/account_derivation.rs index a8827a305..49c87c2ea 100644 --- a/key-wallet-ffi/src/account_derivation.rs +++ b/key-wallet-ffi/src/account_derivation.rs @@ -88,7 +88,7 @@ pub unsafe extern "C" fn bls_account_derive_private_key_from_seed( account.inner().derive_from_seed_private_key_at(seed_slice, index), error ); - unwrap_or_return!(CString::new(hex::encode(sk.to_be_bytes())), error).into_raw() + unwrap_or_return!(CString::new(hex::encode(*sk.to_bytes())), error).into_raw() } /// Derive a BLS private key from a mnemonic + optional passphrase at the given index. @@ -127,7 +127,7 @@ pub unsafe extern "C" fn bls_account_derive_private_key_from_mnemonic( account.inner().derive_from_mnemonic_private_key_at(mnemonic_str, passphrase_str, index,), error ); - unwrap_or_return!(CString::new(hex::encode(sk.to_be_bytes())), error).into_raw() + unwrap_or_return!(CString::new(hex::encode(*sk.to_bytes())), error).into_raw() } // ========================= EdDSA (feature = "eddsa") ========================= diff --git a/key-wallet/src/account/bls_account.rs b/key-wallet/src/account/bls_account.rs index 226a83b32..905ae94f8 100644 --- a/key-wallet/src/account/bls_account.rs +++ b/key-wallet/src/account/bls_account.rs @@ -18,11 +18,9 @@ use serde::{Deserialize, Serialize}; use crate::bip32::{ChainCode, Fingerprint}; #[cfg(feature = "bincode")] use bincode_derive::{Decode, Encode}; -use dashcore::blsful::{Bls12381G2Impl, SerializationFormat}; use crate::account::derivation::AccountDerivation; -pub use dashcore::blsful::PublicKey as BLSPublicKey; -pub use dashcore::blsful::SecretKey; +pub use dashcore::bls_sig_utils::{BLSPublicKey, BlsScheme, BlsSkBytes}; /// BLS account structure for Platform and masternode operations #[derive(Debug, Clone)] @@ -65,12 +63,10 @@ impl BLSAccount { bls_public_key: [u8; 48], network: Network, ) -> Result { - // Create a BlsPublicKey from bytes - let public_key = BLSPublicKey::::from_bytes_with_mode( - &bls_public_key, - SerializationFormat::Modern, - ) - .map_err(|e| Error::InvalidParameter(format!("Invalid BLS public key: {}", e)))?; + let public_key = BLSPublicKey::from_bytes(bls_public_key) + .as_scheme(BlsScheme::Modern) + .canonicalize() + .map_err(|_| Error::InvalidParameter("Invalid BLS public key".to_string()))?; // Create an extended public key with default metadata let extended_key = ExtendedBLSPubKey { @@ -98,7 +94,7 @@ impl BLSAccount { bls_private_key: ExtendedBLSPrivKey, network: Network, ) -> Result { - let bls_public_key = ExtendedBLSPubKey::from_private_key(&bls_private_key); + let bls_public_key = ExtendedBLSPubKey::from_private_key(&bls_private_key)?; Ok(Self { parent_wallet_id, @@ -125,7 +121,7 @@ impl BLSAccount { let master = ExtendedBLSPrivKey::new_master(network, seed)?; let path = account_type.derivation_path(network)?; let account_xpriv = master.derive_path_legacy(&path)?; - let bls_public_key = ExtendedBLSPubKey::from_private_key(&account_xpriv); + let bls_public_key = ExtendedBLSPubKey::from_private_key(&account_xpriv)?; Ok(Self { parent_wallet_id, @@ -177,12 +173,12 @@ impl BLSAccount { seed: &[u8], network: Network, index: u32, - ) -> Result> { + ) -> Result { let master = ExtendedBLSPrivKey::new_master(network, seed)?; let path = AccountType::ProviderOperatorKeys.derivation_path(network)?; let account_xpriv = master.derive_path_legacy(&path)?; let child = account_xpriv.derive_priv_legacy(ChildNumber::from_normal_idx(index)?)?; - Ok(child.private_key.clone()) + Ok(child.private_key().clone()) } /// Derive a BLS key at a specific path (watch-only, non-hardened paths only) @@ -281,13 +277,8 @@ impl fmt::Display for BLSAccount { } } -impl - AccountDerivation< - ExtendedBLSPrivKey, - ExtendedBLSPubKey, - BLSPublicKey, - SecretKey, - > for BLSAccount +impl AccountDerivation + for BLSAccount { fn defaults_to_hardened_derivation(&self) -> bool { false @@ -407,7 +398,7 @@ impl address_pool_type: AddressPoolType, index: u32, use_hardened_with_priv_key: Option, - ) -> Result> { + ) -> Result { let extended_pubkey = self.derive_extended_public_key_at( address_pool_type, index, @@ -442,7 +433,7 @@ impl // This is already the account key, derive the child self.derive_child_xpriv_from_account_xpriv(&priv_key, &derivation_path)? }; - Ok(ExtendedBLSPubKey::from_private_key(&derived_priv)) + Ok(ExtendedBLSPubKey::from_private_key(&derived_priv)?) } else { // Derive using public key (only non-hardened) self.derive_child_xpub(&derivation_path) @@ -453,9 +444,9 @@ impl &self, master_xpriv: &ExtendedBLSPrivKey, index: u32, - ) -> Result> { + ) -> Result { let xpriv = self.derive_from_master_xpriv_extended_xpriv_at(master_xpriv, index)?; - Ok(xpriv.private_key.clone()) + Ok(xpriv.private_key().clone()) } fn derive_from_seed_extended_xpriv_at( @@ -468,13 +459,9 @@ impl self.derive_from_master_xpriv_extended_xpriv_at(&master, index) } - fn derive_from_seed_private_key_at( - &self, - seed: &[u8], - index: u32, - ) -> Result> { + fn derive_from_seed_private_key_at(&self, seed: &[u8], index: u32) -> Result { let xpriv = self.derive_from_seed_extended_xpriv_at(seed, index)?; - Ok(xpriv.private_key.clone()) + Ok(xpriv.private_key().clone()) } } @@ -489,7 +476,8 @@ mod tests { let seed = [42u8; 32]; let bls_private = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed) .expect("Failed to create BLS private key from seed"); - let bls_public = ExtendedBLSPubKey::from_private_key(&bls_private); + let bls_public = ExtendedBLSPubKey::from_private_key(&bls_private) + .expect("Failed to derive BLS public key"); let public_key_bytes = bls_public.to_bytes(); // Now create account from the valid public key bytes diff --git a/key-wallet/src/account/serialization.rs b/key-wallet/src/account/serialization.rs index 493a6b198..a125ec6a1 100644 --- a/key-wallet/src/account/serialization.rs +++ b/key-wallet/src/account/serialization.rs @@ -87,7 +87,8 @@ mod tests { let seed = [42u8; 32]; let bls_private = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed) .expect("Failed to create BLS private key from seed"); - let bls_public = ExtendedBLSPubKey::from_private_key(&bls_private); + let bls_public = ExtendedBLSPubKey::from_private_key(&bls_private) + .expect("Failed to derive BLS public key"); let public_key_bytes = bls_public.to_bytes(); let account = BLSAccount::from_public_key_bytes( diff --git a/key-wallet/src/derivation_bls_bip32.rs b/key-wallet/src/derivation_bls_bip32.rs index e67cea9e7..4c045ca91 100644 --- a/key-wallet/src/derivation_bls_bip32.rs +++ b/key-wallet/src/derivation_bls_bip32.rs @@ -27,7 +27,7 @@ //! serialization (`fLegacy = true`). This is what dashbls/DashSync use for //! masternode operator keys (DIP-3 `m/9'/coin'/3'/3'`), so the provider-key //! account layer derives with these. -//! - The `*_with_mode` variants take an explicit [`SerializationFormat`]. +//! - The `*_with_mode` variants take an explicit [`BlsScheme`]. //! //! Hardened derivation never serializes the public key, so the mode only //! matters for non-hardened children. Output serialization is likewise @@ -39,8 +39,12 @@ use dashcore_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; use std::error; // NOTE: We use Bls12381G2Impl for BLS keys (48-byte public keys) -use dashcore::blsful::SerializationFormat; -use dashcore::blsful::{Bls12381G2Impl, PublicKey as BlsPublicKey, SecretKey as BlsSecretKey}; +use dashcore::bls_sig_utils::{BLSPublicKey, BlsScheme, BlsSkBytes}; + +/// The scheme an [`ExtendedBLSPubKey`] stores its key bytes under. +/// +/// Storage is fixed; the derivation mode only decides what gets hashed. +const CANONICAL: BlsScheme = BlsScheme::Modern; use dashcore::Network; #[cfg(feature = "serde")] @@ -100,20 +104,19 @@ pub struct ExtendedBLSPrivKey { /// Child number pub child_number: ChildNumber, /// Private key (BLS secret key) - pub private_key: BlsSecretKey, + private_key: BlsSkBytes, /// Chain code for derivation pub chain_code: ChainCode, } -// Hand-written (not `#[derive(Zeroize)]`): `BlsSecretKey` has no `Zeroize` -// impl of its own, but its inner scalar (public field `0`) does, so we wipe -// the value field by field. `Drop` (below) calls this, so the key is wiped -// automatically on scope exit with no caller action required. +// Hand-written (not `#[derive(Zeroize)]`) so every field is named and the +// derivation metadata gets cleared alongside the key. `Drop` (below) calls +// this, so the key is wiped on scope exit with no caller action required. // Cf. `ExtendedPrivKey` in `bip32`. impl zeroize::Zeroize for ExtendedBLSPrivKey { fn zeroize(&mut self) { // Secret key material. - self.private_key.0.zeroize(); + self.private_key.zeroize(); self.chain_code.zeroize(); // Derivation metadata — cleared too so the whole value is wiped. self.depth.zeroize(); @@ -168,15 +171,15 @@ impl ExtendedBLSPrivKey { // eprintln!("HMAC output (hex): {}", hex::encode(private_key_bytes)); // } - // The C++ implementation does modulo reduction by curve order - // We need to do the same before converting to BLS private key - let private_key = BlsSecretKey::::from_be_bytes(&private_key_bytes) - .into_option() - .ok_or(Error::InvalidPrivateKey)?; + // The C++ implementation reduces modulo the curve order + let private_key = BlsSkBytes::from_bytes(private_key_bytes) + .as_scheme(CANONICAL) + .canonicalize() + .map_err(|_| Error::InvalidPrivateKey)?; // #[cfg(test)] // { - // eprintln!("After from_be_bytes (hex): {}", hex::encode(private_key.to_be_bytes())); + // eprintln!("After from_be_bytes (hex): {}", hex::encode(*private_key.to_bytes())); // } // Second HMAC with seed||1 for the chain code @@ -199,7 +202,7 @@ impl ExtendedBLSPrivKey { /// `fLegacy = false`. For Dash masternode operator keys use /// [`Self::derive_priv_legacy`], which matches DashSync. pub fn derive_priv(&self, child: ChildNumber) -> Result { - self.derive_priv_with_mode(child, SerializationFormat::Modern) + self.derive_priv_with_mode(child, BlsScheme::Modern) } /// Derive a child private key using the legacy Dash public key @@ -208,7 +211,7 @@ impl ExtendedBLSPrivKey { /// Equivalent to dashbls `PrivateChild(i, fLegacy = true)` — the mode /// dashbls/DashSync use for masternode operator keys. pub fn derive_priv_legacy(&self, child: ChildNumber) -> Result { - self.derive_priv_with_mode(child, SerializationFormat::Legacy) + self.derive_priv_with_mode(child, BlsScheme::Legacy) } /// Derive a child private key with an explicit serialization mode. @@ -219,7 +222,7 @@ impl ExtendedBLSPrivKey { pub fn derive_priv_with_mode( &self, child: ChildNumber, - format: SerializationFormat, + format: BlsScheme, ) -> Result { // Build the input data for HMAC, following dashbls // `ExtendedPrivateKey::PrivateChild` (extendedprivatekey.cpp) @@ -229,10 +232,15 @@ impl ExtendedBLSPrivKey { // Hardened derivation: private_key || index // (no leading 0x00 — that prefix belongs to secp256k1 BIP32, // where it pads the 33-byte pubkey slot; dashbls doesn't use it) - input_data.extend_from_slice(&self.private_key.to_be_bytes()); + input_data.extend_from_slice(self.private_key.as_bytes()); } else { // Non-hardened derivation: public_key || index - input_data.extend_from_slice(&self.public_key().to_bytes_with_mode(format)); + let hashed = self + .public_key()? + .as_scheme(CANONICAL) + .reencode(format) + .map_err(|_| Error::InvalidPrivateKey)?; + input_data.extend_from_slice(hashed.as_bytes()); } let child_bytes = u32::from(child).to_be_bytes(); input_data.extend_from_slice(&child_bytes); @@ -244,94 +252,109 @@ impl ExtendedBLSPrivKey { let chain_code_bytes = derivation_hmac(&self.chain_code[..], &input_data, 1); // Derive the new private key using proper scalar field arithmetic - let derived_private_key = { - // Convert tweak to secret key - let tweak_key = BlsSecretKey::::from_be_bytes(&key_bytes) - .into_option() - .ok_or(Error::InvalidPrivateKey)?; - - // Perform scalar addition in the BLS12-381 field - // The SecretKey struct has a public field (0) containing the scalar - // We add the scalars and create a new SecretKey from the result - let parent_scalar = self.private_key.0; - let tweak_scalar = tweak_key.0; - let derived_scalar = parent_scalar + tweak_scalar; - - BlsSecretKey::(derived_scalar) - }; + let derived_private_key = self + .private_key + .as_scheme(CANONICAL) + .add_tweak(&key_bytes) + .map_err(|_| Error::InvalidPrivateKey)?; Ok(ExtendedBLSPrivKey { network: self.network, depth: self.depth + 1, - parent_fingerprint: self.fingerprint(), + parent_fingerprint: self.fingerprint()?, child_number: child, private_key: derived_private_key, chain_code: ChainCode::from(chain_code_bytes), }) } + pub fn from_parts( + network: Network, + depth: u8, + parent_fingerprint: Fingerprint, + child_number: ChildNumber, + private_key: BlsSkBytes, + chain_code: ChainCode, + ) -> Result { + Ok(ExtendedBLSPrivKey { + network, + depth, + parent_fingerprint, + child_number, + private_key: private_key + .as_scheme(CANONICAL) + .canonicalize() + .map_err(|_| Error::InvalidPrivateKey)?, + chain_code, + }) + } + + /// Get the private key bytes + pub fn private_key(&self) -> &BlsSkBytes { + &self.private_key + } + /// Get the public key for this private key - pub fn public_key(&self) -> BlsPublicKey { - BlsPublicKey::from(&self.private_key) + pub fn public_key(&self) -> Result { + self.private_key.as_scheme(CANONICAL).public_key().map_err(|_| Error::InvalidPrivateKey) } /// Get the public key bytes (modern/IETF serialization) - pub fn public_key_bytes(&self) -> [u8; 48] { - let bytes = self.public_key().to_bytes(); - let mut array = [0u8; 48]; - array.copy_from_slice(&bytes[..48.min(bytes.len())]); - array + pub fn public_key_bytes(&self) -> Result<[u8; 48], Error> { + Ok(self.public_key()?.to_bytes()) } /// Get the public key bytes in Dash legacy serialization. /// /// This is the format dashbls/DashSync use throughout the BLS HD chain. - pub fn public_key_bytes_legacy(&self) -> [u8; 48] { - let bytes = self.public_key().to_bytes_with_mode(SerializationFormat::Legacy); - let mut array = [0u8; 48]; - array.copy_from_slice(&bytes[..48.min(bytes.len())]); - array + pub fn public_key_bytes_legacy(&self) -> Result<[u8; 48], Error> { + Ok(self + .public_key()? + .as_scheme(CANONICAL) + .reencode(BlsScheme::Legacy) + .map_err(|_| Error::InvalidPublicKey)? + .to_bytes()) } /// Get the fingerprint of this key - pub fn fingerprint(&self) -> Fingerprint { + pub fn fingerprint(&self) -> Result { use dashcore_hashes::hash160; - let public_key_bytes = self.public_key_bytes(); + let public_key_bytes = self.public_key_bytes()?; let hash = hash160::Hash::hash(&public_key_bytes); let mut fingerprint_bytes = [0u8; 4]; fingerprint_bytes.copy_from_slice(&hash[..4]); - Fingerprint::from_bytes(fingerprint_bytes) + Ok(Fingerprint::from_bytes(fingerprint_bytes)) } /// Get the extended public key - pub fn to_extended_pub_key(&self) -> ExtendedBLSPubKey { - ExtendedBLSPubKey { + pub fn to_extended_pub_key(&self) -> Result { + Ok(ExtendedBLSPubKey { network: self.network, depth: self.depth, parent_fingerprint: self.parent_fingerprint, child_number: self.child_number, - public_key: self.public_key(), + public_key: self.public_key()?, chain_code: self.chain_code, - } + }) } /// Derive at a path using the modern (IETF) serialization mode /// (see [`Self::derive_priv`]). pub fn derive_path(&self, path: &DerivationPath) -> Result { - self.derive_path_with_mode(path, SerializationFormat::Modern) + self.derive_path_with_mode(path, BlsScheme::Modern) } /// Derive at a path using the legacy Dash serialization mode /// (see [`Self::derive_priv_legacy`]). pub fn derive_path_legacy(&self, path: &DerivationPath) -> Result { - self.derive_path_with_mode(path, SerializationFormat::Legacy) + self.derive_path_with_mode(path, BlsScheme::Legacy) } /// Derive at a path with an explicit serialization mode. pub fn derive_path_with_mode( &self, path: &DerivationPath, - format: SerializationFormat, + format: BlsScheme, ) -> Result { let mut key = self.clone(); for child in path.as_ref() { @@ -353,22 +376,22 @@ pub struct ExtendedBLSPubKey { /// Child number pub child_number: ChildNumber, /// Public key (BLS G2 element - 48 bytes) - pub public_key: BlsPublicKey, + pub public_key: BLSPublicKey, /// Chain code for derivation pub chain_code: ChainCode, } impl ExtendedBLSPubKey { /// Create from a private key - pub fn from_private_key(priv_key: &ExtendedBLSPrivKey) -> Self { - ExtendedBLSPubKey { + pub fn from_private_key(priv_key: &ExtendedBLSPrivKey) -> Result { + Ok(ExtendedBLSPubKey { network: priv_key.network, depth: priv_key.depth, parent_fingerprint: priv_key.parent_fingerprint, child_number: priv_key.child_number, - public_key: priv_key.public_key(), + public_key: priv_key.public_key()?, chain_code: priv_key.chain_code, - } + }) } /// Derive a child public key using the modern (IETF) serialization mode @@ -384,7 +407,7 @@ impl ExtendedBLSPubKey { /// `fLegacy = false`. For Dash masternode operator keys use /// [`Self::derive_pub_legacy`], which matches DashSync. pub fn derive_pub(&self, child: ChildNumber) -> Result { - self.derive_pub_with_mode(child, SerializationFormat::Modern) + self.derive_pub_with_mode(child, BlsScheme::Modern) } /// Derive a child public key using the legacy Dash public key @@ -393,7 +416,7 @@ impl ExtendedBLSPubKey { /// Equivalent to dashbls `PublicChild(i, fLegacy = true)` — the mode /// dashbls/DashSync use for masternode operator keys. pub fn derive_pub_legacy(&self, child: ChildNumber) -> Result { - self.derive_pub_with_mode(child, SerializationFormat::Legacy) + self.derive_pub_with_mode(child, BlsScheme::Legacy) } /// Derive a child public key with an explicit serialization mode @@ -401,7 +424,7 @@ impl ExtendedBLSPubKey { pub fn derive_pub_with_mode( &self, child: ChildNumber, - format: SerializationFormat, + format: BlsScheme, ) -> Result { if child.is_hardened() { return Err(Error::CannotDeriveFromHardenedPublic); @@ -411,7 +434,12 @@ impl ExtendedBLSPubKey { // dashbls `ExtendedPublicKey::PublicChild`, whose fLegacy flag // corresponds to `format`. let mut input_data = Vec::new(); - input_data.extend_from_slice(&self.public_key.to_bytes_with_mode(format)); + let hashed = self + .public_key + .as_scheme(CANONICAL) + .reencode(format) + .map_err(|_| Error::InvalidPublicKey)?; + input_data.extend_from_slice(hashed.as_bytes()); let child_bytes = u32::from(child).to_be_bytes(); input_data.extend_from_slice(&child_bytes); @@ -421,28 +449,11 @@ impl ExtendedBLSPubKey { // Second HMAC-SHA256 with suffix 1 for the chain code let chain_code_bytes = derivation_hmac(&self.chain_code[..], &input_data, 1); - // For BLS public key derivation, we need to do elliptic curve point addition - // First, convert the tweak bytes to a scalar (private key) - let tweak_privkey = BlsSecretKey::::from_be_bytes(&tweak_bytes) - .into_option() - .ok_or(Error::InvalidPrivateKey)?; - - // Convert the scalar to a public key point (scalar * G where G is the generator) - let tweak_pubkey = BlsPublicKey::from(&tweak_privkey); - - // Now we need to add the two public key points using elliptic curve point addition - // The BLS public key type has an inner field (0) that contains the actual G2Projective point - // G2Projective implements the Group trait which supports addition - - // Access the underlying G2Projective points - let parent_point = self.public_key.0; - let tweak_point = tweak_pubkey.0; - - // Perform elliptic curve point addition - let derived_point = parent_point + tweak_point; - - // Create the new public key with the derived point - let derived_pubkey = BlsPublicKey(derived_point); + let derived_pubkey = self + .public_key + .as_scheme(CANONICAL) + .add_tweak(&tweak_bytes) + .map_err(|_| Error::InvalidPublicKey)?; Ok(ExtendedBLSPubKey { network: self.network, @@ -475,23 +486,25 @@ impl ExtendedBLSPubKey { /// Get the public key bytes in Dash legacy serialization. /// /// This is the format dashbls/DashSync use throughout the BLS HD chain. - pub fn to_bytes_legacy(&self) -> [u8; 48] { - let bytes = self.public_key.to_bytes_with_mode(SerializationFormat::Legacy); - let mut array = [0u8; 48]; - array.copy_from_slice(&bytes[..48.min(bytes.len())]); - array + pub fn to_bytes_legacy(&self) -> Result<[u8; 48], Error> { + Ok(self + .public_key + .as_scheme(CANONICAL) + .reencode(BlsScheme::Legacy) + .map_err(|_| Error::InvalidPublicKey)? + .to_bytes()) } /// Derive at a path using the modern (IETF) serialization mode /// (only non-hardened paths allowed; see [`Self::derive_pub`]). pub fn derive_path(&self, path: &DerivationPath) -> Result { - self.derive_path_with_mode(path, SerializationFormat::Modern) + self.derive_path_with_mode(path, BlsScheme::Modern) } /// Derive at a path using the legacy Dash serialization mode /// (only non-hardened paths allowed; see [`Self::derive_pub_legacy`]). pub fn derive_path_legacy(&self, path: &DerivationPath) -> Result { - self.derive_path_with_mode(path, SerializationFormat::Legacy) + self.derive_path_with_mode(path, BlsScheme::Legacy) } /// Derive at a path with an explicit serialization mode @@ -499,7 +512,7 @@ impl ExtendedBLSPubKey { pub fn derive_path_with_mode( &self, path: &DerivationPath, - format: SerializationFormat, + format: BlsScheme, ) -> Result { let mut key = self.clone(); for child in path.as_ref() { @@ -548,7 +561,7 @@ impl serde::Serialize for ExtendedBLSPrivKey { state.serialize_field("depth", &self.depth)?; state.serialize_field("parent_fingerprint", &self.parent_fingerprint)?; state.serialize_field("child_number", &self.child_number)?; - state.serialize_field("private_key", &self.private_key.to_be_bytes())?; + state.serialize_field("private_key", self.private_key.as_bytes().as_slice())?; state.serialize_field("chain_code", &self.chain_code)?; state.end() } @@ -571,9 +584,10 @@ impl<'de> serde::Deserialize<'de> for ExtendedBLSPrivKey { } let helper = Helper::deserialize(deserializer)?; - let private_key = BlsSecretKey::::from_be_bytes(&helper.private_key) - .into_option() - .ok_or_else(|| serde::de::Error::custom("Invalid BLS private key"))?; + let private_key = BlsSkBytes::from_bytes(helper.private_key) + .as_scheme(CANONICAL) + .canonicalize() + .map_err(|_| serde::de::Error::custom("Invalid BLS private key"))?; Ok(ExtendedBLSPrivKey { network: helper.network, @@ -599,7 +613,7 @@ impl serde::Serialize for ExtendedBLSPubKey { state.serialize_field("depth", &self.depth)?; state.serialize_field("parent_fingerprint", &self.parent_fingerprint)?; state.serialize_field("child_number", &self.child_number)?; - state.serialize_field("public_key", &self.public_key.to_bytes())?; + state.serialize_field("public_key", self.public_key.as_bytes().as_slice())?; state.serialize_field("chain_code", &self.chain_code)?; state.end() } @@ -622,11 +636,11 @@ impl<'de> serde::Deserialize<'de> for ExtendedBLSPubKey { } let helper = Helper::deserialize(deserializer)?; - let public_key = BlsPublicKey::::from_bytes_with_mode( - &helper.public_key, - SerializationFormat::Modern, - ) - .map_err(|e| serde::de::Error::custom(format!("Invalid BLS public key: {}", e)))?; + let public_key = BLSPublicKey::try_from(helper.public_key.as_slice()) + .map_err(|e| serde::de::Error::custom(format!("Invalid BLS public key: {}", e)))? + .as_scheme(CANONICAL) + .canonicalize() + .map_err(|e| serde::de::Error::custom(format!("Invalid BLS public key: {}", e)))?; Ok(ExtendedBLSPubKey { network: helper.network, @@ -651,8 +665,7 @@ impl bincode::Encode for ExtendedBLSPrivKey { self.parent_fingerprint.encode(encoder)?; self.child_number.encode(encoder)?; // Encode private key as bytes - let private_key_bytes = self.private_key.to_be_bytes(); - private_key_bytes.encode(encoder)?; + (*self.private_key.to_bytes()).encode(encoder)?; self.chain_code.encode(encoder)?; Ok(()) } @@ -668,11 +681,10 @@ impl bincode::Decode for ExtendedBLSPrivKey { let parent_fingerprint = Fingerprint::decode(decoder)?; let child_number = ChildNumber::decode(decoder)?; let private_key_bytes: [u8; 32] = <[u8; 32]>::decode(decoder)?; - let private_key = BlsSecretKey::::from_be_bytes(&private_key_bytes) - .into_option() - .ok_or_else(|| { - bincode::error::DecodeError::OtherString("Invalid BLS private key".to_string()) - })?; + let private_key = + BlsSkBytes::from_bytes(private_key_bytes).as_scheme(CANONICAL).canonicalize().map_err( + |_| bincode::error::DecodeError::OtherString("Invalid BLS private key".to_string()), + )?; let chain_code = ChainCode::decode(decoder)?; Ok(ExtendedBLSPrivKey { @@ -707,8 +719,8 @@ impl bincode::Encode for ExtendedBLSPubKey { self.parent_fingerprint.encode(encoder)?; self.child_number.encode(encoder)?; // Encode public key as bytes - let public_key_bytes = self.public_key.to_bytes(); - public_key_bytes.encode(encoder)?; + // A `Vec`, as before: the decoder reads a length-prefixed field. + self.public_key.to_bytes().to_vec().encode(encoder)?; self.chain_code.encode(encoder)?; Ok(()) } @@ -724,13 +736,15 @@ impl bincode::Decode for ExtendedBLSPubKey { let parent_fingerprint = Fingerprint::decode(decoder)?; let child_number = ChildNumber::decode(decoder)?; let public_key_bytes: Vec = Vec::::decode(decoder)?; - let public_key = BlsPublicKey::::from_bytes_with_mode( - &public_key_bytes, - SerializationFormat::Modern, - ) - .map_err(|e| { - bincode::error::DecodeError::OtherString(format!("Invalid BLS public key: {}", e)) - })?; + let public_key = BLSPublicKey::try_from(public_key_bytes.as_slice()) + .map_err(|e| { + bincode::error::DecodeError::OtherString(format!("Invalid BLS public key: {}", e)) + })? + .as_scheme(CANONICAL) + .canonicalize() + .map_err(|e| { + bincode::error::DecodeError::OtherString(format!("Invalid BLS public key: {}", e)) + })?; let chain_code = ChainCode::decode(decoder)?; Ok(ExtendedBLSPubKey { @@ -755,6 +769,26 @@ impl<'de, C> bincode::BorrowDecode<'de, C> for ExtendedBLSPubKey { #[cfg(test)] mod tests { + #[cfg(feature = "bincode")] + #[test] + fn stored_pub_key_that_is_not_a_point_is_rejected() { + use super::*; + + // 48 bytes of the right length but off the curve. Decoding has to + // say so, rather than hand back a key that blows up on first use. + let cfg = bincode::config::standard(); + let sk = ExtendedBLSPrivKey::new_master(Network::Testnet, &[7u8; 32]).unwrap(); + let pk = sk.to_extended_pub_key().unwrap(); + + let mut buf = bincode::encode_to_vec(&pk, cfg).unwrap(); + let valid = pk.public_key.to_bytes(); + let at = buf.windows(48).position(|w| w == valid).expect("key bytes are in the buffer"); + buf[at..at + 48].copy_from_slice(&[0xAAu8; 48]); + + let decoded: Result<(ExtendedBLSPubKey, usize), _> = bincode::decode_from_slice(&buf, cfg); + assert!(decoded.is_err(), "a key that is not a point decoded anyway"); + } + use super::*; /// BIP39 seed for "abandon abandon ... about" (empty passphrase). @@ -783,19 +817,19 @@ mod tests { let child_hardened = master.derive_priv(ChildNumber::from_hardened_idx(0).unwrap()).unwrap(); assert_eq!(child_hardened.depth, 1); - assert_eq!(child_hardened.parent_fingerprint, master.fingerprint()); + assert_eq!(child_hardened.parent_fingerprint, master.fingerprint().unwrap()); // Test non-hardened derivation let child_normal = master.derive_priv(ChildNumber::from_normal_idx(0).unwrap()).unwrap(); assert_eq!(child_normal.depth, 1); - assert_eq!(child_normal.parent_fingerprint, master.fingerprint()); + assert_eq!(child_normal.parent_fingerprint, master.fingerprint().unwrap()); } #[test] fn test_public_key_derivation() { let seed = b"test seed for BLS public key derivation"; let master = ExtendedBLSPrivKey::new_master(Network::Testnet, seed).unwrap(); - let master_pub = master.to_extended_pub_key(); + let master_pub = master.to_extended_pub_key().unwrap(); // Should be able to derive non-hardened child let child_pub = master_pub.derive_pub(ChildNumber::from_normal_idx(0).unwrap()).unwrap(); @@ -813,7 +847,7 @@ mod tests { let seed = vec![1u8, 50, 6, 244, 24, 199, 1, 25]; let master_priv = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed).unwrap(); - let master_pub = master_priv.to_extended_pub_key(); + let master_pub = master_priv.to_extended_pub_key().unwrap(); // Test single child derivation // Child index: 238757 @@ -822,7 +856,7 @@ mod tests { // Derive public key through private key let child_priv = master_priv.derive_priv(ChildNumber::from_normal_idx(child_index).unwrap()).unwrap(); - let pk1 = child_priv.to_extended_pub_key().public_key; + let pk1 = child_priv.to_extended_pub_key().unwrap().public_key; // Derive public key directly from parent public key let child_pub = @@ -844,7 +878,7 @@ mod tests { let seed = vec![1u8, 50, 6, 244, 24, 199, 1, 25]; let master_priv = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed).unwrap(); - let master_pub = master_priv.to_extended_pub_key(); + let master_pub = master_priv.to_extended_pub_key().unwrap(); // Derive through private keys let derived_priv = master_priv @@ -857,7 +891,7 @@ mod tests { .derive_priv(ChildNumber::from_normal_idx(1).unwrap()) .unwrap(); - let pk_from_priv = derived_priv.to_extended_pub_key().public_key; + let pk_from_priv = derived_priv.to_extended_pub_key().unwrap().public_key; // Derive through public keys let derived_pub = master_pub @@ -887,7 +921,7 @@ mod tests { let seed = vec![1u8, 50, 6, 244, 24, 199, 1, 0, 0, 0]; let master_priv = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed).unwrap(); - let master_pub = master_priv.to_extended_pub_key(); + let master_pub = master_priv.to_extended_pub_key().unwrap(); // Child index: 13 let child_index = 13; @@ -896,7 +930,8 @@ mod tests { let pk1 = master_priv .derive_priv(ChildNumber::from_normal_idx(child_index).unwrap()) .unwrap() - .to_extended_pub_key(); + .to_extended_pub_key() + .unwrap(); // Get public key from public derivation let pk2 = @@ -917,7 +952,7 @@ mod tests { let seed = vec![1u8, 50, 6, 244, 24, 199, 1, 25]; let master_priv = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed).unwrap(); - let master_pub = master_priv.to_extended_pub_key(); + let master_pub = master_priv.to_extended_pub_key().unwrap(); // Hardened index: (1 << 31) + 3 let hardened_index = (1u32 << 31) + 3; @@ -943,7 +978,7 @@ mod tests { // Test multiple unhardened derivations let seed = b"test seed for unhardened BLS derivation"; let master = ExtendedBLSPrivKey::new_master(Network::Testnet, seed).unwrap(); - let master_pub = master.to_extended_pub_key(); + let master_pub = master.to_extended_pub_key().unwrap(); // Test with child 42 let child_priv_42 = master.derive_priv(ChildNumber::from_normal_idx(42).unwrap()).unwrap(); @@ -951,7 +986,7 @@ mod tests { master_pub.derive_pub(ChildNumber::from_normal_idx(42).unwrap()).unwrap(); assert_eq!( - child_priv_42.to_extended_pub_key().public_key.to_bytes(), + child_priv_42.to_extended_pub_key().unwrap().public_key.to_bytes(), child_pub_42.public_key.to_bytes() ); @@ -962,7 +997,7 @@ mod tests { child_pub_42.derive_pub(ChildNumber::from_normal_idx(12142).unwrap()).unwrap(); assert_eq!( - grandchild_priv.to_extended_pub_key().public_key.to_bytes(), + grandchild_priv.to_extended_pub_key().unwrap().public_key.to_bytes(), grandchild_pub.public_key.to_bytes() ); } @@ -973,7 +1008,7 @@ mod tests { let seed = vec![1u8, 50, 6, 244, 24, 199, 1, 25]; let master_priv = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed).unwrap(); - let master_pub = master_priv.to_extended_pub_key(); + let master_pub = master_priv.to_extended_pub_key().unwrap(); // Create a non-hardened path let path = DerivationPath::from(vec![ @@ -991,7 +1026,7 @@ mod tests { // They should match assert_eq!( - derived_priv.to_extended_pub_key().public_key.to_bytes(), + derived_priv.to_extended_pub_key().unwrap().public_key.to_bytes(), derived_pub.public_key.to_bytes() ); } @@ -1035,7 +1070,7 @@ mod tests { let seed = vec![1u8, 50, 6, 244, 25, 199, 1, 25]; // C++ test vector let master_priv = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed).unwrap(); - let master_pub = master_priv.to_extended_pub_key(); + let master_pub = master_priv.to_extended_pub_key().unwrap(); // Test private key serialization with serde #[cfg(feature = "serde")] @@ -1050,10 +1085,7 @@ mod tests { assert_eq!(master_priv.parent_fingerprint, deserialized.parent_fingerprint); assert_eq!(master_priv.child_number, deserialized.child_number); assert_eq!(master_priv.chain_code, deserialized.chain_code); - assert_eq!( - master_priv.private_key.to_be_bytes(), - deserialized.private_key.to_be_bytes() - ); + assert_eq!(master_priv.private_key.to_bytes(), deserialized.private_key.to_bytes()); // Test public key serialization let pub_serialized = serde_json::to_string(&master_pub).unwrap(); @@ -1080,7 +1112,7 @@ mod tests { assert_eq!(master_priv.parent_fingerprint, decoded.parent_fingerprint); assert_eq!(master_priv.child_number, decoded.child_number); assert_eq!(master_priv.chain_code, decoded.chain_code); - assert_eq!(master_priv.private_key.to_be_bytes(), decoded.private_key.to_be_bytes()); + assert_eq!(master_priv.private_key.to_bytes(), decoded.private_key.to_bytes()); // Test public key let pub_encoded = @@ -1102,13 +1134,14 @@ mod tests { let seed = vec![1u8, 50, 6, 244, 25, 199, 1, 25]; let esk = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed).unwrap(); - let epk = esk.to_extended_pub_key(); + let epk = esk.to_extended_pub_key().unwrap(); // Derive child 238757 through private key let pk1 = esk .derive_priv(ChildNumber::from_normal_idx(238757).unwrap()) .unwrap() .to_extended_pub_key() + .unwrap() .public_key; // Derive child 238757 through public key @@ -1137,7 +1170,10 @@ mod tests { .derive_pub(ChildNumber::from_normal_idx(1).unwrap()) .unwrap(); - assert_eq!(sk3.to_extended_pub_key().public_key.to_bytes(), pk4.public_key.to_bytes()); + assert_eq!( + sk3.to_extended_pub_key().unwrap().public_key.to_bytes(), + pk4.public_key.to_bytes() + ); } #[test] @@ -1155,8 +1191,8 @@ mod tests { // Keys derived with same index should be equal assert_eq!( - esk77_hardened.private_key.to_be_bytes(), - esk77_hardened_copy.private_key.to_be_bytes() + esk77_hardened.private_key.to_bytes(), + esk77_hardened_copy.private_key.to_bytes() ); assert_eq!(esk77_hardened.chain_code, esk77_hardened_copy.chain_code); @@ -1164,21 +1200,19 @@ mod tests { let esk77_normal = esk1.derive_priv(ChildNumber::from_normal_idx(77).unwrap()).unwrap(); // Hardened and non-hardened should be different - assert_ne!( - esk77_hardened.private_key.to_be_bytes(), - esk77_normal.private_key.to_be_bytes() - ); + assert_ne!(esk77_hardened.private_key.to_bytes(), esk77_normal.private_key.to_bytes()); // Test vector 2: {1, 50, 6, 244, 24, 199, 1, 0, 0, 0} let seed2 = vec![1u8, 50, 6, 244, 24, 199, 1, 0, 0, 0]; let esk2 = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed2).unwrap(); - let epk2 = esk2.to_extended_pub_key(); + let epk2 = esk2.to_extended_pub_key().unwrap(); // Test public child derivation let pk1 = esk2 .derive_priv(ChildNumber::from_normal_idx(13).unwrap()) .unwrap() - .to_extended_pub_key(); + .to_extended_pub_key() + .unwrap(); let pk2 = epk2.derive_pub(ChildNumber::from_normal_idx(13).unwrap()).unwrap(); assert_eq!(pk1.public_key.to_bytes(), pk2.public_key.to_bytes()); @@ -1192,13 +1226,14 @@ mod tests { // Test vector: {1, 50, 6, 244, 24, 199, 1, 0, 0, 0} let seed = vec![1u8, 50, 6, 244, 24, 199, 1, 0, 0, 0]; let esk = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed).unwrap(); - let epk = esk.to_extended_pub_key(); + let epk = esk.to_extended_pub_key().unwrap(); // Test PublicChild(13) derivation let pk1 = esk .derive_priv(ChildNumber::from_normal_idx(13).unwrap()) .unwrap() - .to_extended_pub_key(); + .to_extended_pub_key() + .unwrap(); let pk2 = epk.derive_pub(ChildNumber::from_normal_idx(13).unwrap()).unwrap(); // Public keys should match whether derived through private or public path @@ -1210,11 +1245,14 @@ mod tests { // Test with another seed: {1, 50, 6, 244, 25, 199, 1, 25} let seed2 = vec![1u8, 50, 6, 244, 25, 199, 1, 25]; let esk2 = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed2).unwrap(); - let epk2 = esk2.to_extended_pub_key(); + let epk2 = esk2.to_extended_pub_key().unwrap(); // Test child 238757 derivation - let pk1_238757 = - esk2.derive_priv(ChildNumber::from_normal_idx(238757).unwrap()).unwrap().public_key(); + let pk1_238757 = esk2 + .derive_priv(ChildNumber::from_normal_idx(238757).unwrap()) + .unwrap() + .public_key() + .unwrap(); let pk2_238757 = epk2.derive_pub(ChildNumber::from_normal_idx(238757).unwrap()).unwrap().public_key; @@ -1241,7 +1279,7 @@ mod tests { .derive_pub(ChildNumber::from_normal_idx(1).unwrap()) .unwrap(); - assert_eq!(sk3.public_key().to_bytes(), pk4.public_key.to_bytes()); + assert_eq!(sk3.public_key().unwrap().to_bytes(), pk4.public_key.to_bytes()); } #[test] @@ -1253,14 +1291,14 @@ mod tests { ]; let master1 = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed1).unwrap(); - let master1_pub = master1.to_extended_pub_key(); + let master1_pub = master1.to_extended_pub_key().unwrap(); // Test child 42 unhardened let child_sk = master1.derive_priv(ChildNumber::from_normal_idx(42).unwrap()).unwrap(); let child_pk = master1_pub.derive_pub(ChildNumber::from_normal_idx(42).unwrap()).unwrap(); assert_eq!( - child_sk.to_extended_pub_key().public_key.to_bytes(), + child_sk.to_extended_pub_key().unwrap().public_key.to_bytes(), child_pk.public_key.to_bytes() ); @@ -1271,7 +1309,7 @@ mod tests { child_pk.derive_pub(ChildNumber::from_normal_idx(12142).unwrap()).unwrap(); assert_eq!( - grandchild_sk.to_extended_pub_key().public_key.to_bytes(), + grandchild_sk.to_extended_pub_key().unwrap().public_key.to_bytes(), grandchild_pk.public_key.to_bytes() ); @@ -1282,7 +1320,7 @@ mod tests { ]; let master2 = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed2).unwrap(); - let master2_pub = master2.to_extended_pub_key(); + let master2_pub = master2.to_extended_pub_key().unwrap(); // Test unhardened child 42 let child_sk_unhardened = @@ -1296,17 +1334,17 @@ mod tests { // Verify unhardened derivation consistency assert_eq!( - child_sk_unhardened.to_extended_pub_key().public_key.to_bytes(), + child_sk_unhardened.to_extended_pub_key().unwrap().public_key.to_bytes(), child_pk_unhardened.public_key.to_bytes() ); // Verify hardened != unhardened assert_ne!( - child_sk_hardened.private_key.to_be_bytes(), - child_sk_unhardened.private_key.to_be_bytes() + child_sk_hardened.private_key.to_bytes(), + child_sk_unhardened.private_key.to_bytes() ); assert_ne!( - child_sk_hardened.to_extended_pub_key().public_key.to_bytes(), + child_sk_hardened.to_extended_pub_key().unwrap().public_key.to_bytes(), child_pk_unhardened.public_key.to_bytes() ); } @@ -1332,8 +1370,8 @@ mod tests { // Hardened derivation should be deterministic assert_eq!( - child_hardened.private_key.to_be_bytes(), - child_hardened_copy.private_key.to_be_bytes(), + child_hardened.private_key.to_bytes(), + child_hardened_copy.private_key.to_bytes(), "Hardened derivation should be deterministic" ); assert_eq!(child_hardened.chain_code, child_hardened_copy.chain_code); @@ -1341,8 +1379,8 @@ mod tests { // Hardened and unhardened should produce different keys assert_ne!( - child_hardened.private_key.to_be_bytes(), - child_unhardened.private_key.to_be_bytes(), + child_hardened.private_key.to_bytes(), + child_unhardened.private_key.to_bytes(), "Hardened and unhardened derivation should produce different keys" ); assert_ne!( @@ -1355,8 +1393,8 @@ mod tests { assert_eq!(child_unhardened.depth, 1); // Both should have correct parent fingerprint - assert_eq!(child_hardened.parent_fingerprint, master.fingerprint()); - assert_eq!(child_unhardened.parent_fingerprint, master.fingerprint()); + assert_eq!(child_hardened.parent_fingerprint, master.fingerprint().unwrap()); + assert_eq!(child_unhardened.parent_fingerprint, master.fingerprint().unwrap()); } /// Reference vectors generated with dashbls (dashpay/bls-signatures @ 0842b17, @@ -1374,7 +1412,7 @@ mod tests { fn master_from_seed() { let master = master_from_seed64(); assert_eq!( - hex::encode(master.private_key.to_be_bytes()), + hex::encode(master.private_key.to_bytes()), "27d1e600fe5ce42e9a18fe064aa0c1b8ee6754289013a86eb1e8af985ddc55c5" ); assert_eq!( @@ -1382,11 +1420,11 @@ mod tests { "2a680de50ab918089c65f47e6f32363eb8fbb915a61e9a10e0f882aa1c12aef9" ); assert_eq!( - hex::encode(master.public_key_bytes_legacy()), + hex::encode(master.public_key_bytes_legacy().unwrap()), "883389cd6c289b97bfa18cc7b7c873397b4d753269d47d2fa29dda1682c1565687ccb19dd016398da7c9724f8a58bdef" ); assert_eq!( - hex::encode(master.public_key_bytes()), + hex::encode(master.public_key_bytes().unwrap()), "a83389cd6c289b97bfa18cc7b7c873397b4d753269d47d2fa29dda1682c1565687ccb19dd016398da7c9724f8a58bdef" ); } @@ -1406,7 +1444,7 @@ mod tests { .unwrap(); assert_eq!( - hex::encode(account.private_key.to_be_bytes()), + hex::encode(account.private_key.to_bytes()), "5f36c0e346c6e6275d6550a09857325e3f54f2a962eb09a48f61756f7b4bbfb0" ); assert_eq!( @@ -1437,21 +1475,26 @@ mod tests { let child = account .derive_priv_legacy(ChildNumber::from_normal_idx(i as u32).unwrap()) .unwrap(); - assert_eq!(hex::encode(child.private_key.to_be_bytes()), *sk, "sk {}", i); + assert_eq!(hex::encode(child.private_key.to_bytes()), *sk, "sk {}", i); assert_eq!( - hex::encode(child.public_key_bytes_legacy()), + hex::encode(child.public_key_bytes_legacy().unwrap()), *pk_legacy, "pk_legacy {}", i ); - assert_eq!(hex::encode(child.public_key_bytes()), *pk_modern, "pk_modern {}", i); + assert_eq!( + hex::encode(child.public_key_bytes().unwrap()), + *pk_modern, + "pk_modern {}", + i + ); } // Watch-only path: same child 0 via public derivation. - let account_pub = account.to_extended_pub_key(); + let account_pub = account.to_extended_pub_key().unwrap(); let child0_pub = account_pub.derive_pub_legacy(ChildNumber::from_normal_idx(0).unwrap()).unwrap(); - assert_eq!(hex::encode(child0_pub.to_bytes_legacy()), expected[0].1); + assert_eq!(hex::encode(child0_pub.to_bytes_legacy().unwrap()), expected[0].1); } #[test] @@ -1468,17 +1511,17 @@ mod tests { .derive_priv(hardened(3)) .unwrap(); assert_eq!( - hex::encode(account.private_key.to_be_bytes()), + hex::encode(account.private_key.to_bytes()), "05e18aebbe5c73f4dde3dd6a4a204da46c6efa38a38ff4fa5548b1c171154bda" ); let child0 = account.derive_priv_legacy(ChildNumber::from_normal_idx(0).unwrap()).unwrap(); assert_eq!( - hex::encode(child0.private_key.to_be_bytes()), + hex::encode(child0.private_key.to_bytes()), "3346dfd71627f9f31cad3ee66fe7b673c32cb077b2eb38c621d7e61c30e46dbd" ); assert_eq!( - hex::encode(child0.public_key_bytes_legacy()), + hex::encode(child0.public_key_bytes_legacy().unwrap()), "09d8beabae708de1638487f1aff44b38e8c07d9b09f22d76329d6c8ec01e2ad4d030b660bca40ddbd222373a72c5bcef" ); } @@ -1489,7 +1532,7 @@ mod tests { let seed = [1u8, 50, 6, 244, 24, 199, 1, 25]; let master = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed).unwrap(); assert_eq!( - hex::encode(master.private_key.to_be_bytes()), + hex::encode(master.private_key.to_bytes()), "3e9f7b3846c1803703f94c764b51f5ace513b2f02c4d6b2c452d8ce66e5975bd" ); assert_eq!( @@ -1500,7 +1543,7 @@ mod tests { // Hardened child 77' let c77h = master.derive_priv(hardened(77)).unwrap(); assert_eq!( - hex::encode(c77h.private_key.to_be_bytes()), + hex::encode(c77h.private_key.to_bytes()), "51b31efbd83aeead1e324c5c8248f5a13bb17ba7afe29aeb5ceef7eaff49ed6f" ); assert_eq!( @@ -1511,7 +1554,7 @@ mod tests { // Non-hardened child 77 (legacy serialization in HMAC input) let c77 = master.derive_priv_legacy(ChildNumber::from_normal_idx(77).unwrap()).unwrap(); assert_eq!( - hex::encode(c77.private_key.to_be_bytes()), + hex::encode(c77.private_key.to_bytes()), "3ef4f8b4d262fb8981665532b531c7889798044f7cbe4d5fae5e30435f746044" ); assert_eq!( @@ -1528,7 +1571,7 @@ mod tests { let master = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed).unwrap(); let c77 = master.derive_priv(ChildNumber::from_normal_idx(77).unwrap()).unwrap(); assert_eq!( - hex::encode(c77.private_key.to_be_bytes()), + hex::encode(c77.private_key.to_bytes()), "0f9b101b475e449c9995032e138b432a330738b6401f675f0632385fe8d349bf" ); assert_eq!( @@ -1536,7 +1579,7 @@ mod tests { "c7b09e00d6b9b1676e8714e1060e0324787734809ae557a4bc8c07e9b1304ed0" ); assert_eq!( - hex::encode(c77.public_key_bytes()), + hex::encode(c77.public_key_bytes().unwrap()), "a63fa533db03b400030a5eb163433ac7c8700d2301c4242e03db58d516dea0d52768d1b0d29e9f28f7707ce96d2d6108" ); @@ -1555,28 +1598,26 @@ mod tests { let child0_modern = account.derive_priv(ChildNumber::from_normal_idx(0).unwrap()).unwrap(); assert_eq!( - hex::encode(child0_modern.private_key.to_be_bytes()), + hex::encode(child0_modern.private_key.to_bytes()), "1669d6cc8ac08fa377d63dafcf83f1fa6aee09e2df58c490b1b1a0b0999417ec" ); assert_eq!( - hex::encode(child0_modern.public_key_bytes()), + hex::encode(child0_modern.public_key_bytes().unwrap()), "8f5d504fee1026394728781f004fee70480335c1f53156124b23e45386c7c1e2973efee3eab4ae60650fdaa8ae4460d0" ); // Same leaf via legacy mode is a different key entirely. let child0_legacy = account.derive_priv_legacy(ChildNumber::from_normal_idx(0).unwrap()).unwrap(); - assert_ne!( - child0_modern.private_key.to_be_bytes(), - child0_legacy.private_key.to_be_bytes() - ); + assert_ne!(child0_modern.private_key.to_bytes(), child0_legacy.private_key.to_bytes()); // Private/public derivation stays consistent in modern mode too. let child0_pub = account .to_extended_pub_key() + .unwrap() .derive_pub(ChildNumber::from_normal_idx(0).unwrap()) .unwrap(); - assert_eq!(child0_pub.to_bytes(), child0_modern.public_key_bytes()); + assert_eq!(child0_pub.to_bytes(), child0_modern.public_key_bytes().unwrap()); } } @@ -1597,10 +1638,7 @@ mod tests { /// Constructs a secret key from the supplied scalar and extracts it to find the settled value. fn resolve_scalar(scalar: &[u8; 32]) -> [u8; 32] { - BlsSecretKey::::from_be_bytes(scalar) - .into_option() - .unwrap() - .to_be_bytes() + *BlsSkBytes::from_bytes(*scalar).as_scheme(CANONICAL).canonicalize().unwrap().to_bytes() } #[test] @@ -1632,12 +1670,12 @@ mod tests { // The master seed itself produces an HMAC above r, reduced. assert!(parse_bytes_32(SEED64_HMAC) >= parse_bytes_32(R)); assert_eq!( - master.private_key.to_be_bytes(), + *master.private_key.to_bytes(), resolve_scalar(&parse_bytes_32(SEED64_HMAC)) ); // Hardened child index 1 lands above r, would be reduced. - let mut input = master.private_key.to_be_bytes().to_vec(); + let mut input = master.private_key.to_bytes().to_vec(); input.extend_from_slice( &u32::from(ChildNumber::from_hardened_idx(1).unwrap()).to_be_bytes(), ); @@ -1670,13 +1708,13 @@ mod tests { assert!(reversed >= parse_bytes_32(R)); let converted = root.to_bls_extended_priv_key(Network::Testnet).unwrap(); - assert_eq!(converted.private_key.to_be_bytes(), resolve_scalar(&reversed)); + assert_eq!(*converted.private_key.to_bytes(), resolve_scalar(&reversed)); } #[test] fn zero_scalar_is_refused() { - let read = BlsSecretKey::::from_be_bytes(&[0u8; 32]); - assert!(bool::from(read.is_none())); + let read = BlsSkBytes::from_bytes([0u8; 32]).as_scheme(CANONICAL).canonicalize(); + assert!(read.is_err()); } #[test] @@ -1684,10 +1722,7 @@ mod tests { // A point is checked where a scalar is reduced; 48 bytes off the curve // has nowhere to land. let off = [0xAAu8; 48]; - let read = BlsPublicKey::::from_bytes_with_mode( - &off, - SerializationFormat::Modern, - ); + let read = BLSPublicKey::from_bytes(off).as_scheme(BlsScheme::Modern).canonicalize(); assert!(read.is_err()); } } @@ -1698,12 +1733,12 @@ mod tests { let seed = [42u8; 32]; let mut key = ExtendedBLSPrivKey::new_master(Network::Testnet, &seed).unwrap(); - assert_ne!(key.private_key.to_be_bytes(), [0u8; 32]); + assert_ne!(*key.private_key.to_bytes(), [0u8; 32]); assert_ne!(key.chain_code.as_ref(), &[0u8; 32]); key.zeroize(); - assert_eq!(key.private_key.to_be_bytes(), [0u8; 32]); + assert_eq!(*key.private_key.to_bytes(), [0u8; 32]); assert_eq!(key.chain_code.as_ref(), &[0u8; 32]); assert_eq!(key.depth, 0); assert_eq!(key.parent_fingerprint, Fingerprint::default()); diff --git a/key-wallet/src/managed_account/address_pool.rs b/key-wallet/src/managed_account/address_pool.rs index 9b483bb83..bbc1ce59f 100644 --- a/key-wallet/src/managed_account/address_pool.rs +++ b/key-wallet/src/managed_account/address_pool.rs @@ -149,7 +149,10 @@ impl KeySource { Error::InvalidParameter(format!("BLS derivation error: {:?}", e)) })?; } - Ok(DerivedKey::BLS(derived.public_key_bytes().to_vec())) + let public_key_bytes = derived.public_key_bytes().map_err(|e| { + Error::InvalidParameter(format!("BLS derivation error: {:?}", e)) + })?; + Ok(DerivedKey::BLS(public_key_bytes.to_vec())) } #[cfg(feature = "bls")] KeySource::BLSPublic(xpub) => { diff --git a/key-wallet/src/managed_account/managed_account_trait.rs b/key-wallet/src/managed_account/managed_account_trait.rs index 866e9fbe1..9591f2f18 100644 --- a/key-wallet/src/managed_account/managed_account_trait.rs +++ b/key-wallet/src/managed_account/managed_account_trait.rs @@ -18,6 +18,8 @@ use crate::managed_account::managed_account_type::ManagedAccountType; use crate::AddressInfo; use crate::ExtendedPubKey; use crate::Network; +#[cfg(feature = "bls")] +use dashcore::bls_sig_utils::{BLSPublicKey, BlsScheme}; use dashcore::{Address, ScriptBuf, Txid}; /// Common trait for "core" managed account types — both funds-bearing @@ -400,7 +402,7 @@ pub trait ManagedAccountTrait { &mut self, account_xpub: Option, add_to_state: bool, - ) -> Result, &'static str> { + ) -> Result { match self.managed_account_type_mut() { ManagedAccountType::ProviderOperatorKeys { addresses, @@ -421,14 +423,11 @@ pub trait ManagedAccountTrait { addresses.mark_index_used(info.index); - use dashcore::blsful::{Bls12381G2Impl, PublicKey, SerializationFormat}; - let public_key = PublicKey::::from_bytes_with_mode( - &pub_key_bytes, - SerializationFormat::Modern, - ) - .map_err(|_| "Failed to deserialize BLS public key")?; - - Ok(public_key) + BLSPublicKey::try_from(pub_key_bytes.as_slice()) + .map_err(|_| "BLS public key was not 48 bytes")? + .as_scheme(BlsScheme::Modern) + .canonicalize() + .map_err(|_| "Failed to deserialize BLS public key") } _ => Err("This method only works for ProviderOperatorKeys accounts"), } diff --git a/key-wallet/src/tests/provider_key_derivation_tests.rs b/key-wallet/src/tests/provider_key_derivation_tests.rs index 697ecdbcd..a6abac159 100644 --- a/key-wallet/src/tests/provider_key_derivation_tests.rs +++ b/key-wallet/src/tests/provider_key_derivation_tests.rs @@ -39,7 +39,7 @@ fn bls_operator_keys_match_dashbls_reference() { // The stored account xpub must be the account-level key at m/9'/5'/3'/3'. assert_eq!( - hex::encode(account.bls_public_key.to_bytes_legacy()), + hex::encode(account.bls_public_key.to_bytes_legacy().unwrap()), "8d794d053504db3727c1f51aea2112e440fadbade687a9c0243b61523c8ab8eb64061f0a5ec5d8df4b7ec8bdfe722c19" ); @@ -47,7 +47,7 @@ fn bls_operator_keys_match_dashbls_reference() { let key0_pub = account.bls_public_key.derive_pub_legacy(ChildNumber::from_normal_idx(0).unwrap()).unwrap(); assert_eq!( - hex::encode(key0_pub.to_bytes_legacy()), + hex::encode(key0_pub.to_bytes_legacy().unwrap()), "078cad04aae29eb76171937eb7101452b401b026efbc27db840f130374e6a9ec8443d917277f8921e0ba6678a7709875" ); // Same point in modern/IETF (basic-scheme) serialization, as it appears in @@ -61,12 +61,12 @@ fn bls_operator_keys_match_dashbls_reference() { let seed = hex::decode(TEST_SEED_HEX).unwrap(); let sk0 = account.derive_from_seed_private_key_at(&seed, 0).unwrap(); assert_eq!( - hex::encode(sk0.to_be_bytes()), + hex::encode(*sk0.to_bytes()), "11122e1ad656d0610ce0f80d40da874d67ea656a3e66ed371c915ec3a488a43a" ); let sk1 = account.derive_from_seed_private_key_at(&seed, 1).unwrap(); assert_eq!( - hex::encode(sk1.to_be_bytes()), + hex::encode(*sk1.to_bytes()), "1a4e3318640cd4e50222184d0ea111abf8a0c18a0e5dc3ed45dad85009db4e31" ); } @@ -84,14 +84,14 @@ fn bls_operator_keys_testnet_match_dashbls_reference() { let key0_pub = account.bls_public_key.derive_pub_legacy(ChildNumber::from_normal_idx(0).unwrap()).unwrap(); assert_eq!( - hex::encode(key0_pub.to_bytes_legacy()), + hex::encode(key0_pub.to_bytes_legacy().unwrap()), "09d8beabae708de1638487f1aff44b38e8c07d9b09f22d76329d6c8ec01e2ad4d030b660bca40ddbd222373a72c5bcef" ); let seed = hex::decode(TEST_SEED_HEX).unwrap(); let sk0 = account.derive_from_seed_private_key_at(&seed, 0).unwrap(); assert_eq!( - hex::encode(sk0.to_be_bytes()), + hex::encode(*sk0.to_bytes()), "3346dfd71627f9f31cad3ee66fe7b673c32cb077b2eb38c621d7e61c30e46dbd" ); } @@ -217,7 +217,7 @@ fn operator_key_at_is_wallet_state_agnostic() { for account in [resident, &watch_only] { let key0 = account.operator_public_key_at(0).expect("gate-free derivation must succeed"); assert_eq!( - hex::encode(key0.to_bytes_legacy()), + hex::encode(key0.to_bytes_legacy().unwrap()), "078cad04aae29eb76171937eb7101452b401b026efbc27db840f130374e6a9ec8443d917277f8921e0ba6678a7709875" ); assert_eq!( @@ -230,19 +230,19 @@ fn operator_key_at_is_wallet_state_agnostic() { let seed = hex::decode(TEST_SEED_HEX).unwrap(); let sk0 = BLSAccount::operator_private_key_at(&seed, Network::Mainnet, 0).unwrap(); assert_eq!( - hex::encode(sk0.to_be_bytes()), + hex::encode(*sk0.to_bytes()), "11122e1ad656d0610ce0f80d40da874d67ea656a3e66ed371c915ec3a488a43a" ); let sk1 = BLSAccount::operator_private_key_at(&seed, Network::Mainnet, 1).unwrap(); assert_eq!( - hex::encode(sk1.to_be_bytes()), + hex::encode(*sk1.to_bytes()), "1a4e3318640cd4e50222184d0ea111abf8a0c18a0e5dc3ed45dad85009db4e31" ); // Testnet vectors (coin type 1: m/9'/1'/3'/3'). let sk0_testnet = BLSAccount::operator_private_key_at(&seed, Network::Testnet, 0).unwrap(); assert_eq!( - hex::encode(sk0_testnet.to_be_bytes()), + hex::encode(*sk0_testnet.to_bytes()), "3346dfd71627f9f31cad3ee66fe7b673c32cb077b2eb38c621d7e61c30e46dbd" ); } diff --git a/key-wallet/src/tests/special_transaction_matching_tests.rs b/key-wallet/src/tests/special_transaction_matching_tests.rs index 27a8dcda6..c1ea71f0b 100644 --- a/key-wallet/src/tests/special_transaction_matching_tests.rs +++ b/key-wallet/src/tests/special_transaction_matching_tests.rs @@ -351,7 +351,7 @@ async fn provider_registration_with_owner_key_hash_matches_provider_owner_keys() ProviderMasternodeType::Regular, derive_pubkey_hash(&owner_addr), derive_pubkey_hash(&voting_addr), - operator_pk.0.to_compressed().into(), + operator_pk, ScriptBuf::new(), None, ); @@ -386,7 +386,7 @@ async fn provider_registration_with_voting_key_hash_matches_provider_voting_keys ProviderMasternodeType::Regular, derive_pubkey_hash(&owner_addr), derive_pubkey_hash(&voting_addr), - operator_pk.0.to_compressed().into(), + operator_pk, ScriptBuf::new(), None, ); @@ -422,7 +422,7 @@ async fn provider_registration_with_operator_public_key_matches_provider_operato ProviderMasternodeType::Regular, derive_pubkey_hash(&owner_addr), derive_pubkey_hash(&voting_addr), - operator_pk.0.to_compressed().into(), + operator_pk, ScriptBuf::new(), None, ); @@ -487,7 +487,7 @@ async fn provider_registration_extends_operator_key_gap_limit() { ProviderMasternodeType::Regular, derive_pubkey_hash(&owner_addr), derive_pubkey_hash(&voting_addr), - operator_pk.0.to_compressed().into(), + operator_pk, ScriptBuf::new(), None, ); @@ -560,7 +560,7 @@ async fn provider_registration_with_platform_node_id_matches_provider_platform_k ProviderMasternodeType::HighPerformance, derive_pubkey_hash(&owner_addr), derive_pubkey_hash(&voting_addr), - operator_pk.0.to_compressed().into(), + operator_pk, ScriptBuf::new(), Some(platform_node_id), ); @@ -613,7 +613,7 @@ async fn provider_update_registrar_with_voting_key_change_matches_provider_votin version: 1, pro_tx_hash: Txid::from_byte_array([1u8; 32]), provider_mode: 0, - operator_public_key: operator_pk.0.to_compressed().into(), + operator_public_key: operator_pk, voting_key_hash: derive_pubkey_hash(&voting_addr), script_payout: ScriptBuf::new(), inputs_hash: [3u8; 32].into(), @@ -666,7 +666,7 @@ async fn provider_update_registrar_with_operator_key_change_matches_provider_ope version: 1, pro_tx_hash: Txid::from_byte_array([1u8; 32]), provider_mode: 0, - operator_public_key: operator_pk.0.to_compressed().into(), + operator_public_key: operator_pk, voting_key_hash: derive_pubkey_hash(&voting_addr), script_payout: ScriptBuf::new(), inputs_hash: [3u8; 32].into(), diff --git a/key-wallet/src/transaction_checking/account_checker.rs b/key-wallet/src/transaction_checking/account_checker.rs index bee66a0db..5f4183a0b 100644 --- a/key-wallet/src/transaction_checking/account_checker.rs +++ b/key-wallet/src/transaction_checking/account_checker.rs @@ -1032,7 +1032,6 @@ impl ManagedCoreFundsAccount { // Check if operator_public_key matches any of our BLS public keys for address_info in addresses.addresses.values() { if let Some(PublicKeyType::BLS(bls_key)) = &address_info.public_key { - // Compare the byte arrays - BLSPublicKey implements AsRef<[u8; 48]> let operator_key_bytes: &[u8; 48] = operator_public_key.as_ref(); if bls_key.len() == 48 && bls_key.as_slice() == operator_key_bytes { return Some(AccountMatch { diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs b/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs index 4212c7f3c..2c12d861e 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs @@ -211,7 +211,7 @@ async fn test_provider_registration_transaction_routing_check_owner_only() { .payload() .as_pubkey_hash() .expect("Owner address should be P2PKH"), - operator_public_key: operator_public_key.0.to_compressed().into(), + operator_public_key, voting_key_hash: *voting_address .payload() .as_pubkey_hash() @@ -346,7 +346,7 @@ async fn test_provider_registration_transaction_routing_check_voting_only() { .payload() .as_pubkey_hash() .expect("Owner address should be P2PKH"), - operator_public_key: operator_public_key.0.to_compressed().into(), + operator_public_key, voting_key_hash: *voting_address .payload() .as_pubkey_hash() @@ -482,7 +482,7 @@ async fn test_provider_registration_transaction_routing_check_operator_only() { .payload() .as_pubkey_hash() .expect("Owner address should be P2PKH"), - operator_public_key: operator_public_key.0.to_compressed().into(), + operator_public_key, voting_key_hash: *voting_address .payload() .as_pubkey_hash() @@ -686,7 +686,7 @@ async fn test_provider_registration_transaction_routing_check_platform_only() { .payload() .as_pubkey_hash() .expect("Owner address should be P2PKH"), - operator_public_key: operator_public_key.0.to_compressed().into(), + operator_public_key, voting_key_hash: *voting_address .payload() .as_pubkey_hash() @@ -820,7 +820,7 @@ async fn test_provider_update_registrar_with_voting_and_operator() { version: 1, pro_tx_hash: Txid::from_byte_array([1u8; 32]), provider_mode: 0, - operator_public_key: operator_public_key.0.to_compressed().into(), + operator_public_key, voting_key_hash: *voting_address .payload() .as_pubkey_hash() diff --git a/key-wallet/src/wallet/root_extended_keys.rs b/key-wallet/src/wallet/root_extended_keys.rs index 4571bef5f..7668a4ff0 100644 --- a/key-wallet/src/wallet/root_extended_keys.rs +++ b/key-wallet/src/wallet/root_extended_keys.rs @@ -8,7 +8,7 @@ use crate::{Error, Network, Wallet}; #[cfg(feature = "bincode")] use bincode::{BorrowDecode, Decode, Encode}; #[cfg(feature = "bls")] -use dashcore::blsful::Bls12381G2Impl; +use dashcore::bls_sig_utils::BlsSkBytes; use dashcore_hashes::{sha512, Hash, HashEngine, Hmac, HmacEngine}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -101,28 +101,21 @@ impl RootExtendedPrivKey { #[cfg(feature = "bls")] pub fn to_bls_extended_priv_key(&self, network: Network) -> Result { // Convert secp256k1 private key bytes to BLS private key - // Using from_le_bytes for little-endian byte order - // Note: from_le_bytes returns a CtOption (constant-time option) for security - let bls_private_key_option = dashcore::blsful::SecretKey::::from_le_bytes( - &self.root_private_key.to_secret_bytes(), - ); - - // Convert CtOption to Result - let bls_private_key = if bls_private_key_option.is_some().into() { - bls_private_key_option.unwrap() - } else { - return Err(Error::InvalidParameter( - "Failed to convert to BLS key: invalid key bytes".to_string(), - )); - }; - - Ok(ExtendedBLSPrivKey { + // The scalar is read little-endian from the secp secret, the bag holds + // big-endian, so the bytes are reversed going in. + let mut scalar_bytes = self.root_private_key.to_secret_bytes(); + scalar_bytes.reverse(); + + ExtendedBLSPrivKey::from_parts( network, - depth: 0, - parent_fingerprint: Default::default(), - child_number: ChildNumber::from(0), - private_key: bls_private_key, - chain_code: self.root_chain_code, + 0, + Default::default(), + ChildNumber::from(0), + BlsSkBytes::from_bytes(scalar_bytes), + self.root_chain_code, + ) + .map_err(|_| { + Error::InvalidParameter("Failed to convert to BLS key: invalid key bytes".to_string()) }) }