From 76471eeb968c0c4f8f853c20b9ece0317ac3c733 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:32:38 +0530 Subject: [PATCH 1/6] types%refac: isolate codec-independent macros to shared macros module `git diff --color-moved=dimmed-zebra --color-moved-ws=ignore-all-space` --- pkgs/types/src/entity.rs | 136 --------------------- pkgs/types/src/lib.rs | 3 +- pkgs/types/src/macros.rs | 251 +++++++++++++++++++++++++++++++++++++++ pkgs/types/src/secret.rs | 114 +----------------- 4 files changed, 254 insertions(+), 250 deletions(-) diff --git a/pkgs/types/src/entity.rs b/pkgs/types/src/entity.rs index c91741b9..4a7151ac 100644 --- a/pkgs/types/src/entity.rs +++ b/pkgs/types/src/entity.rs @@ -212,142 +212,6 @@ macro_rules! impl_bytes { }; } -/// The standard trait set for a fixed-size byte newtype, expressed only -/// through `from_bytes` / `as_bytes`. -/// -/// Emits `Clone`, `Copy`, `Default`, `Eq`, `PartialEq`, `Ord`, `PartialOrd`, -/// `Hash`, `is_null`, `AsRef<[u8]>`, `AsRef<[u8; N]>`, `From for -/// [u8; N]`, a hex `Debug`/`Display`, and the hex `serde` pair. -/// -/// A trailing `rev` renders the hex in reverse storage order, the default `fwd` -/// renders storage order. -/// -/// For a newtype holding secrets use [`derive_sbytes!`](crate::derive_sbytes), -/// which withholds everything that would read or copy out the plaintext. -#[macro_export] -macro_rules! derive_bytes { - (@parse [$($g:tt)*] $ty:ty, $n:expr, $rev:expr) => { - impl<$($g)*> ::core::clone::Clone for $ty { - fn clone(&self) -> Self { *self } - } - - impl<$($g)*> ::core::marker::Copy for $ty {} - - impl<$($g)*> ::core::default::Default for $ty { - fn default() -> Self { Self::from_bytes([0u8; $n]) } - } - - impl<$($g)*> ::core::cmp::Eq for $ty {} - - impl<$($g)*> ::core::cmp::PartialEq for $ty { - fn eq(&self, other: &Self) -> bool { self.as_bytes() == other.as_bytes() } - } - - impl<$($g)*> ::core::cmp::Ord for $ty { - fn cmp(&self, other: &Self) -> ::core::cmp::Ordering { - self.as_bytes().cmp(other.as_bytes()) - } - } - - impl<$($g)*> ::core::cmp::PartialOrd for $ty { - fn partial_cmp(&self, other: &Self) -> ::core::option::Option<::core::cmp::Ordering> { - ::core::option::Option::Some(::core::cmp::Ord::cmp(self, other)) - } - } - - impl<$($g)*> ::core::hash::Hash for $ty { - fn hash(&self, state: &mut H) { - ::core::hash::Hash::hash(self.as_bytes(), state); - } - } - - impl<$($g)*> ::core::convert::AsRef<[u8]> for $ty { - fn as_ref(&self) -> &[u8] { self.as_bytes() } - } - - impl<$($g)*> ::core::convert::AsRef<[u8; $n]> for $ty { - fn as_ref(&self) -> &[u8; $n] { self.as_bytes() } - } - - impl<$($g)*> ::core::convert::From<$ty> for [u8; $n] { - fn from(val: $ty) -> Self { *val.as_bytes() } - } - - impl<$($g)*> $ty { - /// Returns `true` when every byte is zero. - pub fn is_null(&self) -> bool { self.as_bytes().iter().all(|&b| b == 0) } - } - - impl<$($g)*> ::core::fmt::Debug for $ty { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - $crate::qtypestr(f, ::core::any::type_name::())?; - f.write_str("(")?; - ::core::fmt::Display::fmt(self, f)?; - f.write_str(")") - } - } - - $crate::derive_bytes!(@hex [$($g)*] $ty, $n, $rev); - }; - (@order [$($g:tt)*] $ty:ty, $n:expr, fwd) => { - $crate::derive_bytes!(@parse [$($g)*] $ty, $n, false); - }; - (@order [$($g:tt)*] $ty:ty, $n:expr, rev) => { - $crate::derive_bytes!(@parse [$($g)*] $ty, $n, true); - }; - (@hex [$($g:tt)*] $ty:ty, $n:expr, $rev:expr) => { - impl<$($g)*> ::core::fmt::Display for $ty { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - let bytes = self.as_bytes(); - for i in 0..$n { - let byte = if $rev { bytes[$n - 1 - i] } else { bytes[i] }; - ::core::write!(f, "{byte:02x}")?; - } - ::core::result::Result::Ok(()) - } - } - - $crate::cfg_serde! { - impl<$($g)*> $crate::__private::serde::Serialize for $ty { - fn serialize(&self, serializer: Z) -> Result - where - Z: $crate::__private::serde::Serializer, - { - serializer.serialize_str(&::alloc::format!("{self}")) - } - } - - impl<'de, $($g)*> $crate::__private::serde::Deserialize<'de> for $ty { - fn deserialize(deserializer: D) -> Result - where - D: $crate::__private::serde::Deserializer<'de>, - { - use $crate::__private::serde::de::Error as _; - let s = <::alloc::string::String as $crate::__private::serde::Deserialize>::deserialize(deserializer)?; - let mut bytes = <[u8; $n] as $crate::__private::hex_conservative::FromHex>::from_hex(&s) - .map_err(D::Error::custom)?; - if $rev { - bytes.reverse(); - } - ::core::result::Result::Ok(Self::from_bytes(bytes)) - } - } - } - }; - (for[$($generic:tt)*] $ty:ty, $n:expr, $order:tt) => { - $crate::derive_bytes!(@order [$($generic)*] $ty, $n, $order); - }; - (for[$($generic:tt)*] $ty:ty, $n:expr) => { - $crate::derive_bytes!(@order [$($generic)*] $ty, $n, fwd); - }; - ($ty:ty, $n:expr, $order:tt) => { - $crate::derive_bytes!(@order [] $ty, $n, $order); - }; - ($ty:ty, $n:expr) => { - $crate::derive_bytes!(@order [] $ty, $n, fwd); - }; -} - /// Declares a fixed-size byte newtype over `[u8; N]` with the `from_bytes` / /// `to_bytes` / `as_bytes` accessors. /// diff --git a/pkgs/types/src/lib.rs b/pkgs/types/src/lib.rs index e1a02d86..48e7b187 100644 --- a/pkgs/types/src/lib.rs +++ b/pkgs/types/src/lib.rs @@ -30,7 +30,8 @@ pub mod type_id; pub use compact::CompactSize; pub use entity::{VecDecoder, VecEncoder, MAX_SER_SIZE}; -pub use secret::{qtypestr, ArrDecoder, ArrEncoder, ArrayBuf, MAX_ARR_SIZE}; +pub use macros::qtypestr; +pub use secret::{ArrDecoder, ArrEncoder, ArrayBuf, MAX_ARR_SIZE}; #[doc(hidden)] pub mod __private { diff --git a/pkgs/types/src/macros.rs b/pkgs/types/src/macros.rs index 583c81b1..d549ce2f 100644 --- a/pkgs/types/src/macros.rs +++ b/pkgs/types/src/macros.rs @@ -6,6 +6,8 @@ //! Shared macro definitions. +use core::fmt; + /// Emits its body only when *this* crate has the `serde` feature. /// /// `#[cfg(feature = "serde")]` written inside an exported macro resolves @@ -29,6 +31,37 @@ macro_rules! cfg_serde { ($($item:tt)*) => {}; } +/// Writes [`type_name`](core::any::type_name) output to `f` with its module +/// qualifiers dropped. +pub fn qtypestr(f: &mut fmt::Formatter<'_>, path: &str) -> fmt::Result { + let bytes = path.as_bytes(); + let (mut seg, mut i) = (0, 0); + while i < bytes.len() { + match bytes[i] { + // A qualifier: discard everything emitted since the last segment. + b':' if bytes.get(i + 1) == Some(&b':') => { + i += 2; + seg = i; + } + delim @ (b'<' | b'>' | b',') => { + f.write_str(&path[seg..i])?; + f.write_str(match delim { + b'<' => "<", + b'>' => ">", + _ => ", ", + })?; + i += 1; + while bytes.get(i) == Some(&b' ') { + i += 1; + } + seg = i; + } + _ => i += 1, + } + } + f.write_str(&path[seg..]) +} + /// Maps enum variants to integer constants and display strings. /// /// Generates the enum definition, integer mapping (via `NumCodec` or inherent @@ -277,6 +310,202 @@ macro_rules! enum_map { }; } +/// The standard trait set for a fixed-size byte newtype, expressed only +/// through `from_bytes` / `as_bytes`. +/// +/// Emits `Clone`, `Copy`, `Default`, `Eq`, `PartialEq`, `Ord`, `PartialOrd`, +/// `Hash`, `is_null`, `AsRef<[u8]>`, `AsRef<[u8; N]>`, `From for +/// [u8; N]`, a hex `Debug`/`Display`, and the hex `serde` pair. +/// +/// A trailing `rev` renders the hex in reverse storage order, the default `fwd` +/// renders storage order. +/// +/// For a newtype holding secrets use [`derive_sbytes!`](crate::derive_sbytes), +/// which withholds everything that would read or copy out the plaintext. +#[macro_export] +macro_rules! derive_bytes { + (@parse [$($g:tt)*] $ty:ty, $n:expr, $rev:expr) => { + impl<$($g)*> ::core::clone::Clone for $ty { + fn clone(&self) -> Self { *self } + } + + impl<$($g)*> ::core::marker::Copy for $ty {} + + impl<$($g)*> ::core::default::Default for $ty { + fn default() -> Self { Self::from_bytes([0u8; $n]) } + } + + impl<$($g)*> ::core::cmp::Eq for $ty {} + + impl<$($g)*> ::core::cmp::PartialEq for $ty { + fn eq(&self, other: &Self) -> bool { self.as_bytes() == other.as_bytes() } + } + + impl<$($g)*> ::core::cmp::Ord for $ty { + fn cmp(&self, other: &Self) -> ::core::cmp::Ordering { + self.as_bytes().cmp(other.as_bytes()) + } + } + + impl<$($g)*> ::core::cmp::PartialOrd for $ty { + fn partial_cmp(&self, other: &Self) -> ::core::option::Option<::core::cmp::Ordering> { + ::core::option::Option::Some(::core::cmp::Ord::cmp(self, other)) + } + } + + impl<$($g)*> ::core::hash::Hash for $ty { + fn hash(&self, state: &mut H) { + ::core::hash::Hash::hash(self.as_bytes(), state); + } + } + + impl<$($g)*> ::core::convert::AsRef<[u8]> for $ty { + fn as_ref(&self) -> &[u8] { self.as_bytes() } + } + + impl<$($g)*> ::core::convert::AsRef<[u8; $n]> for $ty { + fn as_ref(&self) -> &[u8; $n] { self.as_bytes() } + } + + impl<$($g)*> ::core::convert::From<$ty> for [u8; $n] { + fn from(val: $ty) -> Self { *val.as_bytes() } + } + + impl<$($g)*> $ty { + /// Returns `true` when every byte is zero. + pub fn is_null(&self) -> bool { self.as_bytes().iter().all(|&b| b == 0) } + } + + impl<$($g)*> ::core::fmt::Debug for $ty { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + $crate::qtypestr(f, ::core::any::type_name::())?; + f.write_str("(")?; + ::core::fmt::Display::fmt(self, f)?; + f.write_str(")") + } + } + + $crate::derive_bytes!(@hex [$($g)*] $ty, $n, $rev); + }; + (@order [$($g:tt)*] $ty:ty, $n:expr, fwd) => { + $crate::derive_bytes!(@parse [$($g)*] $ty, $n, false); + }; + (@order [$($g:tt)*] $ty:ty, $n:expr, rev) => { + $crate::derive_bytes!(@parse [$($g)*] $ty, $n, true); + }; + (@hex [$($g:tt)*] $ty:ty, $n:expr, $rev:expr) => { + impl<$($g)*> ::core::fmt::Display for $ty { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + let bytes = self.as_bytes(); + for i in 0..$n { + let byte = if $rev { bytes[$n - 1 - i] } else { bytes[i] }; + ::core::write!(f, "{byte:02x}")?; + } + ::core::result::Result::Ok(()) + } + } + + $crate::cfg_serde! { + impl<$($g)*> $crate::__private::serde::Serialize for $ty { + fn serialize(&self, serializer: Z) -> Result + where + Z: $crate::__private::serde::Serializer, + { + serializer.serialize_str(&::alloc::format!("{self}")) + } + } + + impl<'de, $($g)*> $crate::__private::serde::Deserialize<'de> for $ty { + fn deserialize(deserializer: D) -> Result + where + D: $crate::__private::serde::Deserializer<'de>, + { + use $crate::__private::serde::de::Error as _; + let s = <::alloc::string::String as $crate::__private::serde::Deserialize>::deserialize(deserializer)?; + let mut bytes = <[u8; $n] as $crate::__private::hex_conservative::FromHex>::from_hex(&s) + .map_err(D::Error::custom)?; + if $rev { + bytes.reverse(); + } + ::core::result::Result::Ok(Self::from_bytes(bytes)) + } + } + } + }; + (for[$($generic:tt)*] $ty:ty, $n:expr, $order:tt) => { + $crate::derive_bytes!(@order [$($generic)*] $ty, $n, $order); + }; + (for[$($generic:tt)*] $ty:ty, $n:expr) => { + $crate::derive_bytes!(@order [$($generic)*] $ty, $n, fwd); + }; + ($ty:ty, $n:expr, $order:tt) => { + $crate::derive_bytes!(@order [] $ty, $n, $order); + }; + ($ty:ty, $n:expr) => { + $crate::derive_bytes!(@order [] $ty, $n, fwd); + }; +} + +/// The secret counterpart to [`derive_bytes!`](crate::derive_bytes), for a +/// fixed-size byte newtype holding key material. +/// +/// Emits `Drop`, `ZeroizeOnDrop`, `is_null`, the `AsRef` pair, and a redacting +/// `Debug`/`Display`. `Zeroize`, `Clone` and `Eq`/`PartialEq` are left to the +/// type: only it knows which fields are secret, and equality must be +/// constant-time. +/// +/// Withholds `Copy`, `Default`, `Ord`/`PartialOrd`/`Hash`, `From for +/// [u8; N]` and the hex `serde` pair, each because it either escapes the wipe +/// or reads the plaintext. Do *not* implement them. +#[macro_export] +macro_rules! derive_sbytes { + (@parse [$($g:tt)*] $ty:ty, $n:expr) => { + impl<$($g)*> ::core::ops::Drop for $ty { + fn drop(&mut self) { + ::zeroize(self); + } + } + + impl<$($g)*> $crate::__private::zeroize::ZeroizeOnDrop for $ty {} + + impl<$($g)*> $ty { + /// Returns `true` when every byte is zero. + pub fn is_null(&self) -> bool { + use $crate::__private::subtle::ConstantTimeEq as _; + self.as_bytes().ct_eq(&[0u8; $n]).into() + } + } + + impl<$($g)*> ::core::fmt::Debug for $ty { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + // `type_name` rather than `stringify!`, which cannot see the generics + $crate::qtypestr(f, ::core::any::type_name::())?; + f.write_str("(..)") + } + } + + impl<$($g)*> ::core::fmt::Display for $ty { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + ::core::fmt::Debug::fmt(self, f) + } + } + + impl<$($g)*> ::core::convert::AsRef<[u8]> for $ty { + fn as_ref(&self) -> &[u8] { self.as_bytes() } + } + + impl<$($g)*> ::core::convert::AsRef<[u8; $n]> for $ty { + fn as_ref(&self) -> &[u8; $n] { self.as_bytes() } + } + }; + (for[$($generic:tt)*] $($args:tt)*) => { + $crate::derive_sbytes!(@parse [$($generic)*] $($args)*); + }; + ($($args:tt)*) => { + $crate::derive_sbytes!(@parse [] $($args)*); + }; +} + /// Generates `From` + `From<&T>` (or `TryFrom` equivalents). The closure /// body receives `&$src`; the owned impl delegates. #[macro_export] @@ -333,11 +562,14 @@ macro_rules! type_cvrt { #[cfg(test)] mod tests { + use super::qtypestr; use crate::codec::NumCodec; use crate::prelude::*; use rstest::*; + use core::fmt; + enum_map! { /// Open enum: unrecognized codes survive a round trip. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -426,4 +658,23 @@ mod tests { fn closed_display() { assert_eq!(Closed::Lo.to_string(), "Lo"); } + + struct Qtype<'a>(&'a str); + + impl fmt::Display for Qtype<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + qtypestr(f, self.0) + } + } + + #[rstest] + #[case::plain("a::b::Foo", "Foo")] + #[case::unqualified("Foo", "Foo")] + #[case::one_arg("a::Foo", "Foo")] + #[case::two_args("a::Foo", "Foo")] + #[case::nested("a::Foo>", "Foo>")] + #[case::nested_pair("a::Foo, d::Qux>", "Foo, Qux>")] + fn qtypestr_drops_module_paths(#[case] path: &str, #[case] expect: &str) { + assert_eq!(Qtype(path).to_string(), expect); + } } diff --git a/pkgs/types/src/secret.rs b/pkgs/types/src/secret.rs index 0d38b9dc..55ae7ea6 100644 --- a/pkgs/types/src/secret.rs +++ b/pkgs/types/src/secret.rs @@ -17,37 +17,6 @@ use core::fmt; /// Widest buffer [`ArrEncoder`] and [`ArrDecoder`] will wipe. pub const MAX_ARR_SIZE: usize = 512; -/// Writes [`type_name`](core::any::type_name) output to `f` with its module -/// qualifiers dropped. -pub fn qtypestr(f: &mut fmt::Formatter<'_>, path: &str) -> fmt::Result { - let bytes = path.as_bytes(); - let (mut seg, mut i) = (0, 0); - while i < bytes.len() { - match bytes[i] { - // A qualifier: discard everything emitted since the last segment. - b':' if bytes.get(i + 1) == Some(&b':') => { - i += 2; - seg = i; - } - delim @ (b'<' | b'>' | b',') => { - f.write_str(&path[seg..i])?; - f.write_str(match delim { - b'<' => "<", - b'>' => ">", - _ => ", ", - })?; - i += 1; - while bytes.get(i) == Some(&b' ') { - i += 1; - } - seg = i; - } - _ => i += 1, - } - } - f.write_str(&path[seg..]) -} - /// Fixed-size encode buffer backed by `[u8; N]`. /// /// Implements [`Zeroize`] but has no `Drop`, so it does *not* wipe itself when @@ -319,66 +288,6 @@ macro_rules! impl_sbytes { }; } -/// The secret counterpart to [`derive_bytes!`](crate::derive_bytes), for a -/// fixed-size byte newtype holding key material. -/// -/// Emits `Drop`, `ZeroizeOnDrop`, `is_null`, the `AsRef` pair, and a redacting -/// `Debug`/`Display`. `Zeroize`, `Clone` and `Eq`/`PartialEq` are left to the -/// type: only it knows which fields are secret, and equality must be -/// constant-time. -/// -/// Withholds `Copy`, `Default`, `Ord`/`PartialOrd`/`Hash`, `From for -/// [u8; N]` and the hex `serde` pair, each because it either escapes the wipe -/// or reads the plaintext. Do *not* implement them. -#[macro_export] -macro_rules! derive_sbytes { - (@parse [$($g:tt)*] $ty:ty, $n:expr) => { - impl<$($g)*> ::core::ops::Drop for $ty { - fn drop(&mut self) { - ::zeroize(self); - } - } - - impl<$($g)*> $crate::__private::zeroize::ZeroizeOnDrop for $ty {} - - impl<$($g)*> $ty { - /// Returns `true` when every byte is zero. - pub fn is_null(&self) -> bool { - use $crate::__private::subtle::ConstantTimeEq as _; - self.as_bytes().ct_eq(&[0u8; $n]).into() - } - } - - impl<$($g)*> ::core::fmt::Debug for $ty { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - // `type_name` rather than `stringify!`, which cannot see the generics - $crate::qtypestr(f, ::core::any::type_name::())?; - f.write_str("(..)") - } - } - - impl<$($g)*> ::core::fmt::Display for $ty { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - ::core::fmt::Debug::fmt(self, f) - } - } - - impl<$($g)*> ::core::convert::AsRef<[u8]> for $ty { - fn as_ref(&self) -> &[u8] { self.as_bytes() } - } - - impl<$($g)*> ::core::convert::AsRef<[u8; $n]> for $ty { - fn as_ref(&self) -> &[u8; $n] { self.as_bytes() } - } - }; - (for[$($generic:tt)*] $($args:tt)*) => { - $crate::derive_sbytes!(@parse [$($generic)*] $($args)*); - }; - ($($args:tt)*) => { - $crate::derive_sbytes!(@parse [] $($args)*); - }; -} - /// The secret counterpart to [`dlgt_codec!`](crate::dlgt_codec), for an /// operational type whose wire image is key material. /// @@ -406,7 +315,7 @@ macro_rules! dlgt_scodec { #[cfg(test)] mod tests { - use super::{qtypestr, ArrDecoder, ArrEncoder, ArrayBuf, MAX_ARR_SIZE}; + use super::{ArrDecoder, ArrEncoder, ArrayBuf, MAX_ARR_SIZE}; use crate::codec::{DecodeError, EncodeBuf}; use crate::prelude::*; @@ -414,8 +323,6 @@ mod tests { use rstest::*; use zeroize::Zeroize; - use core::fmt; - fn filled(fill: u8, len: usize) -> ArrayBuf { let mut b = ArrayBuf::::new(); b.extend_from_slice(&vec![fill; len]); @@ -494,23 +401,4 @@ mod tests { let adec = ArrDecoder::, 16>::new(take_all); assert!(format!("{adec:?}").contains("limit: 16")); } - - struct Qtype<'a>(&'a str); - - impl fmt::Display for Qtype<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - qtypestr(f, self.0) - } - } - - #[rstest] - #[case::plain("a::b::Foo", "Foo")] - #[case::unqualified("Foo", "Foo")] - #[case::one_arg("a::Foo", "Foo")] - #[case::two_args("a::Foo", "Foo")] - #[case::nested("a::Foo>", "Foo>")] - #[case::nested_pair("a::Foo, d::Qux>", "Foo, Qux>")] - fn qtypestr_drops_module_paths(#[case] path: &str, #[case] expect: &str) { - assert_eq!(Qtype(path).to_string(), expect); - } } From d1e9219591f700afdef9557b5cd9062d521f7f43 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:48:44 +0530 Subject: [PATCH 2/6] types%refac(codec): gate behind feature `codec` --- pkgs/dev/Cargo.toml | 8 +- pkgs/num/Cargo.toml | 4 +- pkgs/p2p_core/Cargo.toml | 1 + pkgs/p2p_core/src/msg/mn_list.rs | 2 +- pkgs/params/Cargo.toml | 4 +- pkgs/pkc/Cargo.toml | 4 +- pkgs/primitives/Cargo.toml | 1 + pkgs/primitives/src/payload/proregtx.rs | 2 +- pkgs/primitives/src/payload/proupservtx.rs | 2 +- pkgs/primitives/src/payload/quorum.rs | 2 +- pkgs/primitives/src/transaction.rs | 2 +- pkgs/primitives/src/types/addrv2.rs | 2 +- pkgs/script/Cargo.toml | 1 + pkgs/script/src/addrs.rs | 2 +- pkgs/script/src/opcode.rs | 4 +- pkgs/script/src/sigops.rs | 2 - pkgs/types/Cargo.toml | 12 +-- pkgs/types/src/lib.rs | 24 ++++-- pkgs/types/src/macros.rs | 88 +++++++++++++++++----- 19 files changed, 121 insertions(+), 46 deletions(-) diff --git a/pkgs/dev/Cargo.toml b/pkgs/dev/Cargo.toml index 95fdb3c0..7ad57123 100644 --- a/pkgs/dev/Cargo.toml +++ b/pkgs/dev/Cargo.toml @@ -38,7 +38,9 @@ bin = [ [build-dependencies] built = { version = "0.7", features = ["dependency-tree", "git2"] } -dash-types = { version = "0.0.0", path = "../types", default-features = false } +dash-types = { version = "0.0.0", path = "../types", default-features = false, features = [ + "codec", +] } proc-macro2 = "1" syn = { version = "2", features = ["full", "visit"] } xxhash-rust = { version = "0.8", features = ["xxh32"] } @@ -50,7 +52,9 @@ dash-num = { version = "0.0.0", path = "../num", optional = true } dash-params = { version = "0.0.0", path = "../params", optional = true } dash-pow = { version = "0.0.0", path = "../pow", optional = true } dash-primitives = { version = "0.0.0", path = "../primitives" } -dash-types = { version = "0.0.0", path = "../types", default-features = false } +dash-types = { version = "0.0.0", path = "../types", default-features = false, features = [ + "codec", +] } hex-conservative = { version = "0.3", default-features = false, features = [ "alloc", ] } diff --git a/pkgs/num/Cargo.toml b/pkgs/num/Cargo.toml index 0a0f0420..7e2eade4 100644 --- a/pkgs/num/Cargo.toml +++ b/pkgs/num/Cargo.toml @@ -12,7 +12,9 @@ serde = ["dep:serde", "dash-types/serde"] [dependencies] bitcoin-consensus-encoding = { workspace = true } -dash-types = { version = "0.0.0", path = "../types", default-features = false } +dash-types = { version = "0.0.0", path = "../types", default-features = false, features = [ + "codec", +] } serde = { version = "1", default-features = false, features = [ "derive", "alloc", diff --git a/pkgs/p2p_core/Cargo.toml b/pkgs/p2p_core/Cargo.toml index b919ef22..c5ace1e0 100644 --- a/pkgs/p2p_core/Cargo.toml +++ b/pkgs/p2p_core/Cargo.toml @@ -39,6 +39,7 @@ dash-params = { version = "0.0.0", path = "../params" } dash-primitives = { version = "0.0.0", path = "../primitives" } dash-types = { version = "0.0.0", path = "../types", default-features = false, features = [ "bitcoin-p2p-messages", + "codec", ] } serde = { version = "1", default-features = false, features = [ "alloc", diff --git a/pkgs/p2p_core/src/msg/mn_list.rs b/pkgs/p2p_core/src/msg/mn_list.rs index 784cc247..ca08dbe7 100644 --- a/pkgs/p2p_core/src/msg/mn_list.rs +++ b/pkgs/p2p_core/src/msg/mn_list.rs @@ -14,7 +14,7 @@ use dash_primitives::{ hash_impl, BlockHash, Commitment, LlmqType, MnType, PlatformNodeId, ServiceV1, Transaction, TxHash, }; use dash_script::PubKeyHash; -use dash_types::codec::{BaseCodec, DecodeError, EncodeBuf, NumCodec}; +use dash_types::codec::{BaseCodec, DecodeError, EncodeBuf}; use dash_types::type_id::TypeId; use core::fmt; diff --git a/pkgs/params/Cargo.toml b/pkgs/params/Cargo.toml index e52fc74f..cf49c829 100644 --- a/pkgs/params/Cargo.toml +++ b/pkgs/params/Cargo.toml @@ -19,7 +19,9 @@ hex-literal = "0.4" [dev-dependencies] bitcoin-consensus-encoding = { workspace = true, features = ["alloc"] } -dash-types = { version = "0.0.0", path = "../types", default-features = false } +dash-types = { version = "0.0.0", path = "../types", default-features = false, features = [ + "codec", +] } hex-literal = "0.4" rstest = "0.25" diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index d81caacd..c95b7cf5 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -12,7 +12,9 @@ ff = { version = "0.14", default-features = false, optional = true } group = { version = "0.14", default-features = false, optional = true } cfg-if = "1" dash-num = { version = "0.0.0", path = "../num" } -dash-types = { version = "0.0.0", path = "../types", default-features = false } +dash-types = { version = "0.0.0", path = "../types", default-features = false, features = [ + "codec", +] } hex-conservative = { version = "0.3", default-features = false, features = [ "alloc", ] } diff --git a/pkgs/primitives/Cargo.toml b/pkgs/primitives/Cargo.toml index 4a11fc8e..2b4de875 100644 --- a/pkgs/primitives/Cargo.toml +++ b/pkgs/primitives/Cargo.toml @@ -41,6 +41,7 @@ dash-pow = { version = "0.0.0", path = "../pow" } dash-script = { version = "0.0.0", path = "../script" } dash-types = { version = "0.0.0", path = "../types", default-features = false, features = [ "bitcoin-primitives", + "codec", ] } cfg-if = "1" hex-conservative = { version = "0.3", default-features = false, features = ["alloc"] } diff --git a/pkgs/primitives/src/payload/proregtx.rs b/pkgs/primitives/src/payload/proregtx.rs index ce124e70..e8c8ccf8 100644 --- a/pkgs/primitives/src/payload/proregtx.rs +++ b/pkgs/primitives/src/payload/proregtx.rs @@ -18,7 +18,7 @@ use crate::{hash_impl, TxHash}; use bitcoin_primitives::script::ScriptPubKeyBuf; use dash_pkc::bls::{BlsPkBytes, BlsScIetf}; use dash_script::{PubKeyHash, Recipient}; -use dash_types::codec::{BaseCodec, Checkable, DecodeError, EncodeBuf, NumCodec}; +use dash_types::codec::{BaseCodec, Checkable, DecodeError, EncodeBuf}; use dash_types::make_bytes; use dash_types::type_id::TypeId; diff --git a/pkgs/primitives/src/payload/proupservtx.rs b/pkgs/primitives/src/payload/proupservtx.rs index e0947622..57cd12fc 100644 --- a/pkgs/primitives/src/payload/proupservtx.rs +++ b/pkgs/primitives/src/payload/proupservtx.rs @@ -14,7 +14,7 @@ use crate::{hash_impl, TxHash}; use bitcoin_primitives::script::ScriptPubKeyBuf; use dash_pkc::bls::{BlsScIetf, BlsSigBytes}; -use dash_types::codec::{BaseCodec, Checkable, DecodeError, EncodeBuf, NumCodec}; +use dash_types::codec::{BaseCodec, Checkable, DecodeError, EncodeBuf}; use dash_types::type_id::TypeId; use core::fmt; diff --git a/pkgs/primitives/src/payload/quorum.rs b/pkgs/primitives/src/payload/quorum.rs index 9be2d988..370d6768 100644 --- a/pkgs/primitives/src/payload/quorum.rs +++ b/pkgs/primitives/src/payload/quorum.rs @@ -13,7 +13,7 @@ use crate::support::{DynBitset, LlmqType}; use dash_num::{make_hash, Hash256}; use dash_pkc::bls::{BlsPkBytes, BlsScIetf, BlsSigBytes}; -use dash_types::codec::{BaseCodec, Checkable, DecodeError, EncodeBuf, NumCodec}; +use dash_types::codec::{BaseCodec, Checkable, DecodeError, EncodeBuf}; use dash_types::type_id::{TypeId, Unencodable}; use core::fmt; diff --git a/pkgs/primitives/src/transaction.rs b/pkgs/primitives/src/transaction.rs index ef01165b..764a1a9b 100644 --- a/pkgs/primitives/src/transaction.rs +++ b/pkgs/primitives/src/transaction.rs @@ -15,7 +15,7 @@ use bitcoin_hashes::sha256d; use bitcoin_primitives::script::{ScriptPubKeyBuf, ScriptSigBuf}; use bitcoin_units::Amount; use dash_num::{make_hash, Hash256}; -use dash_types::codec::{self, BaseCodec, Checkable, DecodeError, EncodeBuf, Hashable, NumCodec}; +use dash_types::codec::{self, BaseCodec, Checkable, DecodeError, EncodeBuf, Hashable}; use dash_types::type_id::{TypeId, Unencodable}; use dash_types::{impl_type, CompactSize}; diff --git a/pkgs/primitives/src/types/addrv2.rs b/pkgs/primitives/src/types/addrv2.rs index 628f2c62..a03ce87c 100644 --- a/pkgs/primitives/src/types/addrv2.rs +++ b/pkgs/primitives/src/types/addrv2.rs @@ -13,7 +13,7 @@ use crate::hash_impl; use crate::prelude::*; use bitcoin_hashes::sha3_256; -use dash_types::codec::{self, BaseCodec, Checkable, DecodeError, EncodeBuf, NumCodec}; +use dash_types::codec::{self, BaseCodec, Checkable, DecodeError, EncodeBuf}; use dash_types::type_id::TypeId; use dash_types::{impl_type, type_cvrt, CompactSize}; diff --git a/pkgs/script/Cargo.toml b/pkgs/script/Cargo.toml index 37e550b1..c799a5af 100644 --- a/pkgs/script/Cargo.toml +++ b/pkgs/script/Cargo.toml @@ -22,6 +22,7 @@ dash-num = { version = "0.0.0", path = "../num", default-features = false } dash-pkc = { version = "0.0.0", path = "../pkc", default-features = false } dash-types = { version = "0.0.0", path = "../types", default-features = false, features = [ "bitcoin-primitives", + "codec", ] } serde = { version = "1", default-features = false, features = [ "derive", diff --git a/pkgs/script/src/addrs.rs b/pkgs/script/src/addrs.rs index cd7da3fd..de879f60 100644 --- a/pkgs/script/src/addrs.rs +++ b/pkgs/script/src/addrs.rs @@ -12,7 +12,7 @@ use crate::{opcode::Opcode, PubKeyHash, ScriptHash}; use base58ck::decode_check; use dash_num::Hash160; use dash_pkc::ecdsa::EcdsaPkBytes; -use dash_types::codec::{BaseCodec, EncodeBuf, Hashable, NumCodec}; +use dash_types::codec::{BaseCodec, EncodeBuf, Hashable}; use dash_types::type_cvrt; use dash_types::type_id::Unencodable; diff --git a/pkgs/script/src/opcode.rs b/pkgs/script/src/opcode.rs index b22d5df4..c3b647ed 100644 --- a/pkgs/script/src/opcode.rs +++ b/pkgs/script/src/opcode.rs @@ -6,7 +6,7 @@ //! Script opcodes as defined by the consensus rules. -use dash_types::{codec::NumCodec, enum_map}; +use dash_types::enum_map; use core::fmt; @@ -299,7 +299,7 @@ impl fmt::Debug for Opcode { #[cfg(test)] mod tests { - use super::{NumCodec, Opcode}; + use super::Opcode; use crate::prelude::*; use rstest::*; diff --git a/pkgs/script/src/sigops.rs b/pkgs/script/src/sigops.rs index 28c01453..e57c2445 100644 --- a/pkgs/script/src/sigops.rs +++ b/pkgs/script/src/sigops.rs @@ -8,8 +8,6 @@ use crate::opcode::Opcode; -use dash_types::codec::NumCodec; - const MAX_PUBKEYS: usize = 20; /// Count legacy signature operations in a script. diff --git a/pkgs/types/Cargo.toml b/pkgs/types/Cargo.toml index 19431f00..05ead6c1 100644 --- a/pkgs/types/Cargo.toml +++ b/pkgs/types/Cargo.toml @@ -7,14 +7,16 @@ license = "MIT" [features] default = [] std = [ - "bitcoin-consensus-encoding/std", + "bitcoin-consensus-encoding?/std", "bitcoin-p2p-messages?/std", "bitcoin-primitives?/std", "hex-conservative?/std", ] -full = ["std", "serde", "bitcoin-p2p-messages", "bitcoin-primitives"] -bitcoin-p2p-messages = ["dep:bitcoin-p2p-messages"] +full = ["std", "codec", "serde", "bitcoin-p2p-messages", "bitcoin-primitives"] +codec = ["dep:bitcoin-consensus-encoding", "dep:dash-types-marker"] +bitcoin-p2p-messages = ["codec", "dep:bitcoin-p2p-messages"] bitcoin-primitives = [ + "codec", "dep:base58ck", "dep:bitcoin-primitives", "dep:bitcoin_hashes", @@ -23,12 +25,12 @@ serde = ["dep:serde", "dep:hex-conservative"] [dependencies] base58ck = { workspace = true, optional = true, features = ["alloc"] } -bitcoin-consensus-encoding = { workspace = true } +bitcoin-consensus-encoding = { workspace = true, optional = true } bitcoin-p2p-messages = { workspace = true, optional = true } bitcoin_hashes = { workspace = true, optional = true, features = ["alloc"] } bitcoin-primitives = { workspace = true, optional = true, features = ["alloc"] } cfg-if = "1" -dash-types-marker = { version = "0.0.0", path = "marker" } +dash-types-marker = { version = "0.0.0", path = "marker", optional = true } hex-conservative = { version = "0.3", default-features = false, features = [ "alloc", ], optional = true } diff --git a/pkgs/types/src/lib.rs b/pkgs/types/src/lib.rs index 48e7b187..fee945cc 100644 --- a/pkgs/types/src/lib.rs +++ b/pkgs/types/src/lib.rs @@ -14,30 +14,42 @@ extern crate self as dash_types; extern crate std; #[allow(unused_macros, reason = "used by feature-gated submodules")] +#[cfg(feature = "codec")] mod adapters; -mod compact; +#[cfg(feature = "codec")] mod entity; mod macros; #[allow(unused_imports, reason = "ergonomic shim, exports may be unused")] mod prelude; +#[cfg(feature = "codec")] mod secret; +#[cfg(feature = "codec")] mod uint; -pub mod codec; #[cfg(feature = "serde")] pub mod serialize; -pub mod type_id; -pub use compact::CompactSize; -pub use entity::{VecDecoder, VecEncoder, MAX_SER_SIZE}; pub use macros::qtypestr; -pub use secret::{ArrDecoder, ArrEncoder, ArrayBuf, MAX_ARR_SIZE}; + +cfg_if::cfg_if! { + if #[cfg(feature = "codec")] { + mod compact; + + pub mod codec; + pub mod type_id; + + pub use compact::CompactSize; + pub use entity::{VecDecoder, VecEncoder, MAX_SER_SIZE}; + pub use secret::{ArrDecoder, ArrEncoder, ArrayBuf, MAX_ARR_SIZE}; + } +} #[doc(hidden)] pub mod __private { #[cfg(feature = "bitcoin-primitives")] pub use crate::adapters::bitcoin_primitives::ScriptHash as __ScriptHash; + #[cfg(feature = "codec")] pub use bitcoin_consensus_encoding; #[cfg(feature = "serde")] pub use hex_conservative; diff --git a/pkgs/types/src/macros.rs b/pkgs/types/src/macros.rs index d549ce2f..b9501855 100644 --- a/pkgs/types/src/macros.rs +++ b/pkgs/types/src/macros.rs @@ -8,15 +8,32 @@ use core::fmt; -/// Emits its body only when *this* crate has the `serde` feature. +/// Emits its body only when *this* crate has the `codec` feature. /// -/// `#[cfg(feature = "serde")]` written inside an exported macro resolves -/// against the invoking crate, which doesn't need have a `serde` feature at +/// `#[cfg(feature = "codec")]` written inside an exported macro resolves +/// against the invoking crate, which doesn't need have a `codec` feature at /// all. This marker is compiled here, so it tracks `dash-types` instead. /// /// The two arms must stay plain `#[cfg]` items. Wrapping them in `cfg_if!` /// makes the definition macro-expanded, and a macro-expanded `#[macro_export]` /// macro cannot be reached by `$crate::` from its own crate (rust#52234). +#[cfg(feature = "codec")] +#[doc(hidden)] +#[macro_export] +macro_rules! cfg_codec { + ($($item:tt)*) => { $($item)* }; +} + +#[cfg(not(feature = "codec"))] +#[doc(hidden)] +#[macro_export] +macro_rules! cfg_codec { + ($($item:tt)*) => {}; +} + +/// Emits its body only when *this* crate has the `serde` feature. +/// +/// Identical rationale to [`cfg_codec!`](crate::cfg_codec). #[cfg(feature = "serde")] #[doc(hidden)] #[macro_export] @@ -62,10 +79,28 @@ pub fn qtypestr(f: &mut fmt::Formatter<'_>, path: &str) -> fmt::Result { f.write_str(&path[seg..]) } +/// Generates `NumCodec<$base>` for an enum that already carries the inherent +/// `fn {from,to}_base` pair. +#[cfg(feature = "codec")] +#[macro_export] +macro_rules! impl_enum { + ($enum:ident, $base:ty) => { + impl $crate::codec::NumCodec<$base> for $enum { + fn from_base(val: $base) -> Self { + $enum::from_base(val) + } + + fn to_base(&self) -> $base { + $enum::to_base(*self) + } + } + }; +} + /// Maps enum variants to integer constants and display strings. /// -/// Generates the enum definition, integer mapping (via `NumCodec` or inherent -/// `const fn`), and `impl Display` from a single table. +/// Generates the enum definition, inherent `const fn` integer mapping, and +/// `impl Display` from a single table. /// /// # Syntax /// @@ -78,10 +113,12 @@ pub fn qtypestr(f: &mut fmt::Formatter<'_>, path: &str) -> fmt::Result { /// /// ## Infallible /// -/// Generates the enum with a catch-all variant, `impl NumCodec`, `new`, -/// `is_canonical`, `variants`, and `impl Display`. The catch-all displays as -/// `unknown({v})`; build values with `new` so it never shadows a named -/// variant. +/// Generates the enum with a catch-all variant, inherent `fn {to,from}_base` +/// methods and the [`impl_enum!`](crate::impl_enum) impl over them, `new`, +/// `is_canonical`, `variants`, and `impl Display`. +/// +/// The catch-all displays as `unknown({v})`; build values with `new` so it +/// never shadows a named variant. /// /// ```ignore /// enum_map! { @@ -210,30 +247,30 @@ macro_rules! enum_map { (@infallible $enum:ident, $base:ty, $catch_all:ident { $($variant:ident = $value:literal),+ }) => { - impl $crate::codec::NumCodec<$base> for $enum { - fn from_base(val: $base) -> Self { + impl $enum { + /// Constructs from the base integer value. + pub const fn from_base(val: $base) -> Self { match val { $($value => Self::$variant,)+ other => Self::$catch_all(other), } } - fn to_base(&self) -> $base { + /// Returns the base integer value. + pub const fn to_base(self) -> $base { match self { $(Self::$variant => $value,)+ - Self::$catch_all(v) => *v, + Self::$catch_all(v) => v, } } - } - impl $enum { /// Canonical constructor. /// /// Routes through `from_base`, so a value a named variant covers yields /// that variant instead of a catch-all holding the same number. Decoded /// values already take this path. - pub fn new(val: $base) -> Self { - >::from_base(val) + pub const fn new(val: $base) -> Self { + Self::from_base(val) } /// Whether this value is in canonical form. @@ -242,7 +279,7 @@ macro_rules! enum_map { /// already covers. pub fn is_canonical(&self) -> bool { !matches!(self, Self::$catch_all(v) if matches!( - >::from_base(*v), + Self::from_base(*v), $(Self::$variant)|+ )) } @@ -252,6 +289,10 @@ macro_rules! enum_map { &[$(Self::$variant),+] } } + + $crate::cfg_codec! { + $crate::impl_enum!($enum, $base); + } }; (@fallible $enum:ident, $base:ty { @@ -563,7 +604,6 @@ macro_rules! type_cvrt { #[cfg(test)] mod tests { use super::qtypestr; - use crate::codec::NumCodec; use crate::prelude::*; use rstest::*; @@ -632,6 +672,7 @@ mod tests { #[rstest] fn auto_stringize_uses_the_variant_name() { assert_eq!(Auto::Alpha.to_string(), "Alpha"); + assert_eq!(Auto::Alpha.to_base(), 7); assert_eq!(Auto::Other(3).to_string(), "unknown(3)"); assert_eq!(Auto::variants(), &[Auto::Alpha]); assert_eq!(Auto::new(7), Auto::Alpha); @@ -659,6 +700,15 @@ mod tests { assert_eq!(Closed::Lo.to_string(), "Lo"); } + #[cfg(feature = "codec")] + #[rstest] + fn open_maps_through_the_codec_trait() { + use crate::codec::NumCodec; + + assert_eq!(>::from_base(1), Open::One); + assert_eq!(NumCodec::::to_base(&Open::Two), 2); + } + struct Qtype<'a>(&'a str); impl fmt::Display for Qtype<'_> { From 48d152963d688b55650c08fa3e42570146590b2e Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 24 Aug 2026 23:50:27 +0530 Subject: [PATCH 3/6] pkc%refac(codec): make `codec` optional for `bls`-only builds --- pkgs/p2p_core/Cargo.toml | 2 +- pkgs/pkc/Cargo.toml | 19 +++++++++---------- pkgs/pkc/src/bls/group.rs | 16 +++++++++++----- pkgs/pkc/src/bls/public_bytes.rs | 12 ++++++++++-- pkgs/pkc/src/bls/public_ops.rs | 13 ++++++++++--- pkgs/pkc/src/bls/scalar.rs | 7 +++++-- pkgs/pkc/src/bls/schemes.rs | 12 ++++++++++-- pkgs/pkc/src/bls/secret_bytes.rs | 12 ++++++++++-- pkgs/pkc/src/bls/secret_ops.rs | 15 ++++++++++++--- pkgs/pkc/src/bls/share_id.rs | 3 ++- pkgs/pkc/src/bls/share_ops.rs | 5 +++-- pkgs/pkc/src/bls/sig_basic.rs | 13 ++++++++++--- pkgs/pkc/src/bls/sig_bytes.rs | 12 ++++++++++-- pkgs/pkc/src/bls/sig_id.rs | 4 +++- pkgs/pkc/src/lib.rs | 13 +++++++++---- pkgs/primitives/Cargo.toml | 4 +++- pkgs/script/Cargo.toml | 4 +++- 17 files changed, 121 insertions(+), 45 deletions(-) diff --git a/pkgs/p2p_core/Cargo.toml b/pkgs/p2p_core/Cargo.toml index c5ace1e0..4eda80a8 100644 --- a/pkgs/p2p_core/Cargo.toml +++ b/pkgs/p2p_core/Cargo.toml @@ -33,7 +33,7 @@ bitcoin-primitives = { workspace = true } bitcoin-units = { workspace = true, features = ["alloc"] } cfg-if = "1" dash-num = { version = "0.0.0", path = "../num" } -dash-pkc = { version = "0.0.0", path = "../pkc" } +dash-pkc = { version = "0.0.0", path = "../pkc", features = ["codec"] } dash-script = { version = "0.0.0", path = "../script" } dash-params = { version = "0.0.0", path = "../params" } dash-primitives = { version = "0.0.0", path = "../primitives" } diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index c95b7cf5..4680891e 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -5,16 +5,14 @@ edition = "2021" license = "MIT" [dependencies] -base58ck = { workspace = true, features = ["alloc"] } -bitcoin_hashes = { workspace = true, features = ["alloc"] } +base58ck = { workspace = true, optional = true, features = ["alloc"] } +bitcoin_hashes = { workspace = true, optional = true, features = ["alloc"] } blst = { version = "0.3", default-features = false, optional = true } ff = { version = "0.14", default-features = false, optional = true } group = { version = "0.14", default-features = false, optional = true } cfg-if = "1" -dash-num = { version = "0.0.0", path = "../num" } -dash-types = { version = "0.0.0", path = "../types", default-features = false, features = [ - "codec", -] } +dash-num = { version = "0.0.0", path = "../num", optional = true } +dash-types = { version = "0.0.0", path = "../types", default-features = false } hex-conservative = { version = "0.3", default-features = false, features = [ "alloc", ] } @@ -48,11 +46,12 @@ serde = { version = "1", features = ["derive"] } [features] default = [] -std = ["base58ck/std", "bitcoin_hashes/std", "dash-types/std"] +std = ["base58ck?/std", "bitcoin_hashes?/std", "dash-types/std"] bls = ["dep:blst", "dep:ff", "dep:group", "dep:rand_core", "dep:sha2"] -ecdsa = ["dep:k256", "dep:rand_core"] -serde = ["dep:serde", "dash-num/serde", "dash-types/serde"] -full = ["ecdsa", "bls", "serde", "std", "tests"] +codec = ["dep:base58ck", "dep:bitcoin_hashes", "dep:dash-num", "dash-types/codec"] +ecdsa = ["codec", "dep:k256", "dep:rand_core"] +serde = ["dep:serde", "dash-num?/serde", "dash-types/serde"] +full = ["bls", "codec", "ecdsa", "serde", "std", "tests"] tests = ["std", "dep:rstest"] [lints] diff --git a/pkgs/pkc/src/bls/group.rs b/pkgs/pkc/src/bls/group.rs index 069dbf32..7c39a923 100644 --- a/pkgs/pkc/src/bls/group.rs +++ b/pkgs/pkc/src/bls/group.rs @@ -11,6 +11,7 @@ use super::scalar::{Fp2, Fr, FR_BITS}; use blst::{blst_p1, blst_p1_affine, blst_p2, blst_p2_affine}; use dash_types::type_cvrt; +#[cfg(feature = "codec")] use dash_types::type_id::Unencodable; use ff::{Field, PrimeField}; use group::{Group, GroupEncoding}; @@ -34,7 +35,8 @@ pub(crate) trait Point: Copy + Default + Add { } /// The compressed encoding of a group element, `N` bytes wide. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Unencodable)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "codec", derive(Unencodable))] pub struct BlsPointRepr([u8; N]); impl BlsPointRepr { @@ -69,7 +71,8 @@ impl From<[u8; N]> for BlsPointRepr { } /// A point of the G1 group (over `Fp`) in projective coordinates. -#[derive(Clone, Copy, Default, Unencodable)] +#[derive(Clone, Copy, Default)] +#[cfg_attr(feature = "codec", derive(Unencodable))] pub struct G1(pub(super) blst_p1); type_cvrt!(From for blst_p1, |g| g.0); @@ -77,7 +80,8 @@ type_cvrt!(From for blst_p1, |g| g.0); type_cvrt!(From for G1, |raw| Self(*raw)); /// A point of the G1 group in affine coordinates. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[cfg_attr(feature = "codec", derive(Unencodable))] pub struct G1Affine(pub(super) blst_p1_affine); impl_group!(G1, G1Affine, 48); @@ -87,7 +91,8 @@ type_cvrt!(From for blst_p1_affine, |a| a.0); type_cvrt!(From for G1Affine, |raw| Self(*raw)); /// A point of the G2 group (over `Fp2`) in projective coordinates. -#[derive(Clone, Copy, Default, Unencodable)] +#[derive(Clone, Copy, Default)] +#[cfg_attr(feature = "codec", derive(Unencodable))] pub struct G2(pub(super) blst_p2); type_cvrt!(From for G2, |raw| Self(*raw)); @@ -95,7 +100,8 @@ type_cvrt!(From for G2, |raw| Self(*raw)); type_cvrt!(From for blst_p2, |g| g.0); /// A point of the G2 group in affine coordinates. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[cfg_attr(feature = "codec", derive(Unencodable))] pub struct G2Affine(pub(super) blst_p2_affine); impl G2Affine { diff --git a/pkgs/pkc/src/bls/public_bytes.rs b/pkgs/pkc/src/bls/public_bytes.rs index 60238f0c..751900eb 100644 --- a/pkgs/pkc/src/bls/public_bytes.rs +++ b/pkgs/pkc/src/bls/public_bytes.rs @@ -8,11 +8,17 @@ use crate::bls::BlsSchemeId; +#[cfg(feature = "codec")] use bitcoin_hashes::sha256d::Hash as Sha256d; +#[cfg(feature = "codec")] use dash_num::Hash256; +#[cfg(feature = "codec")] use dash_types::codec::Hashable; +use dash_types::derive_bytes; +#[cfg(feature = "codec")] +use dash_types::impl_bytes; +#[cfg(feature = "codec")] use dash_types::type_id::TypeId; -use dash_types::{derive_bytes, impl_bytes}; use core::marker::PhantomData; @@ -20,14 +26,16 @@ use core::marker::PhantomData; pub const BLS_PK_LEN: usize = 48; /// Scheme-tagged BLS public key bytes (48 bytes, unvalidated). -#[derive(TypeId)] +#[cfg_attr(feature = "codec", derive(TypeId))] pub struct BlsPkBytes { inner: [u8; BLS_PK_LEN], _scheme: PhantomData, } +#[cfg(feature = "codec")] impl_bytes!(for[S: BlsSchemeId] BlsPkBytes, BLS_PK_LEN); +#[cfg(feature = "codec")] impl Hashable for BlsPkBytes { type Hash = Hash256; diff --git a/pkgs/pkc/src/bls/public_ops.rs b/pkgs/pkc/src/bls/public_ops.rs index 7da12595..ead44184 100644 --- a/pkgs/pkc/src/bls/public_ops.rs +++ b/pkgs/pkc/src/bls/public_ops.rs @@ -9,12 +9,18 @@ use super::error::BlsError; use super::group::G1; use super::scheme_ops::BlsScheme; -use super::{BlsPkBytes, BLS_PK_LEN}; +use super::BlsPkBytes; +#[cfg(feature = "codec")] +use super::BLS_PK_LEN; use crate::prelude::*; +#[cfg(feature = "codec")] use dash_num::Hash256; +#[cfg(feature = "codec")] +use dash_types::dlgt_codec; +#[cfg(feature = "codec")] use dash_types::type_id::TypeId; -use dash_types::{dlgt_codec, qtypestr, type_cvrt}; +use dash_types::{qtypestr, type_cvrt}; use hex_conservative::DisplayHex; use core::any::type_name; @@ -22,12 +28,13 @@ use core::fmt::{Debug, Formatter, Result as FmtResult}; use core::hash::{Hash, Hasher}; /// A BLS public key (48-byte compressed G1 point) +#[cfg_attr(feature = "codec", derive(TypeId))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(into = "BlsPkBytes", try_from = "BlsPkBytes",))] #[cfg_attr(feature = "serde", serde(bound(serialize = "", deserialize = "")))] -#[derive(TypeId)] pub struct BlsPublicKey(pub(crate) S::InnerPk); +#[cfg(feature = "codec")] dlgt_codec!(for[S: BlsScheme] BlsPublicKey => BlsPkBytes, Hash256, BlsError, BLS_PK_LEN); impl BlsPublicKey { diff --git a/pkgs/pkc/src/bls/scalar.rs b/pkgs/pkc/src/bls/scalar.rs index 9dc13f95..4bbbe118 100644 --- a/pkgs/pkc/src/bls/scalar.rs +++ b/pkgs/pkc/src/bls/scalar.rs @@ -12,6 +12,7 @@ use super::share_id::BlsShareId; use blst::{blst_fp, blst_fp2, blst_fr}; use dash_types::type_cvrt; +#[cfg(feature = "codec")] use dash_types::type_id::Unencodable; use ff::helpers::{sqrt_ratio_generic, sqrt_tonelli_shanks}; use ff::{Field, PrimeField}; @@ -274,7 +275,8 @@ impl Zeroize for Fr { /// An element of the BLS12-381 base field, i.e. an integer reduced modulo the /// field prime `p`. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[cfg_attr(feature = "codec", derive(Unencodable))] pub(crate) struct Fp(pub(super) blst_fp); impl Fp { @@ -292,7 +294,8 @@ type_cvrt!(From for Fp, |raw| Self(*raw)); /// An element of the quadratic extension field `Fp2 = Fp[u]/(u^2 + 1)`, written /// `c0 + c1*u`. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[cfg_attr(feature = "codec", derive(Unencodable))] pub(crate) struct Fp2(pub(super) blst_fp2); impl Fp2 { diff --git a/pkgs/pkc/src/bls/schemes.rs b/pkgs/pkc/src/bls/schemes.rs index 2c7c711d..e6498bf5 100644 --- a/pkgs/pkc/src/bls/schemes.rs +++ b/pkgs/pkc/src/bls/schemes.rs @@ -6,19 +6,27 @@ //! BLS scheme trait and marker types. +#[cfg(feature = "codec")] use dash_types::type_id::{TypeId, Unencodable}; /// BLS scheme discriminator. +#[cfg(feature = "codec")] pub trait BlsSchemeId: TypeId + 'static {} +/// BLS scheme discriminator. +#[cfg(not(feature = "codec"))] +pub trait BlsSchemeId: 'static {} + /// Legacy (Chia) BLS scheme marker. -#[derive(Clone, Debug, Eq, Hash, PartialEq, TypeId, Unencodable)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "codec", derive(TypeId, Unencodable))] pub enum BlsScChia {} impl BlsSchemeId for BlsScChia {} /// IETF-standard BLS scheme marker. -#[derive(Clone, Debug, Eq, Hash, PartialEq, TypeId, Unencodable)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "codec", derive(TypeId, Unencodable))] pub enum BlsScIetf {} impl BlsSchemeId for BlsScIetf {} diff --git a/pkgs/pkc/src/bls/secret_bytes.rs b/pkgs/pkc/src/bls/secret_bytes.rs index 15b97385..55b13429 100644 --- a/pkgs/pkc/src/bls/secret_bytes.rs +++ b/pkgs/pkc/src/bls/secret_bytes.rs @@ -8,11 +8,17 @@ use crate::bls::BlsSchemeId; +#[cfg(feature = "codec")] use bitcoin_hashes::sha256d::Hash as Sha256d; +#[cfg(feature = "codec")] use dash_num::Hash256; +#[cfg(feature = "codec")] use dash_types::codec::Hashable; +use dash_types::derive_sbytes; +#[cfg(feature = "codec")] +use dash_types::impl_sbytes; +#[cfg(feature = "codec")] use dash_types::type_id::TypeId; -use dash_types::{derive_sbytes, impl_sbytes}; use subtle::ConstantTimeEq; use zeroize::{Zeroize, Zeroizing}; @@ -22,14 +28,16 @@ use core::marker::PhantomData; pub const BLS_SK_LEN: usize = 32; /// Scheme-tagged BLS secret key bytes (32 bytes, zeroized on drop). -#[derive(TypeId)] +#[cfg_attr(feature = "codec", derive(TypeId))] pub struct BlsSkBytes { inner: [u8; BLS_SK_LEN], _scheme: PhantomData, } +#[cfg(feature = "codec")] impl_sbytes!(for[S: BlsSchemeId] BlsSkBytes, BLS_SK_LEN); +#[cfg(feature = "codec")] impl Hashable for BlsSkBytes { type Hash = Hash256; diff --git a/pkgs/pkc/src/bls/secret_ops.rs b/pkgs/pkc/src/bls/secret_ops.rs index c8850f76..77c106c2 100644 --- a/pkgs/pkc/src/bls/secret_ops.rs +++ b/pkgs/pkc/src/bls/secret_ops.rs @@ -12,20 +12,27 @@ use super::public_ops::BlsPublicKey; use super::scalar::Fr; use super::scheme_ops::BlsScheme; use super::sig_basic::BlsSignature; -use super::{BlsScIetf, BlsSigId, BlsSkBytes, BLS_SK_LEN}; +#[cfg(feature = "codec")] +use super::BLS_SK_LEN; +use super::{BlsScIetf, BlsSigId, BlsSkBytes}; use crate::prelude::*; +#[cfg(feature = "codec")] use dash_num::Hash256; +#[cfg(feature = "codec")] +use dash_types::dlgt_scodec; +#[cfg(feature = "codec")] use dash_types::type_id::TypeId; -use dash_types::{dlgt_scodec, qtypestr, type_cvrt}; +use dash_types::{qtypestr, type_cvrt}; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use core::fmt::{Debug, Formatter, Result as FmtResult}; /// A BLS secret key (32-byte scalar). -#[derive(TypeId)] +#[cfg_attr(feature = "codec", derive(TypeId))] pub struct BlsSecretKey(pub(crate) S::InnerSk); +#[cfg(feature = "codec")] dlgt_scodec!(for[S: BlsScheme] BlsSecretKey => BlsSkBytes, Hash256, BlsError, BLS_SK_LEN); impl BlsSecretKey { @@ -271,6 +278,7 @@ mod tests { assert_ne!(chia.public_key().to_bytes(), ietf.public_key().to_bytes()); } + #[cfg(feature = "codec")] fn assert_codec_roundtrip() { use dash_types::codec::BaseCodec; @@ -285,6 +293,7 @@ mod tests { assert!(slice.is_empty()); } + #[cfg(feature = "codec")] #[rstest] #[case::chia(assert_codec_roundtrip::)] #[case::ietf(assert_codec_roundtrip::)] diff --git a/pkgs/pkc/src/bls/share_id.rs b/pkgs/pkc/src/bls/share_id.rs index 718e968a..a395c614 100644 --- a/pkgs/pkc/src/bls/share_id.rs +++ b/pkgs/pkc/src/bls/share_id.rs @@ -7,13 +7,14 @@ //! Threshold participant identifier. use dash_types::derive_bytes; +#[cfg(feature = "codec")] use dash_types::type_id::Unencodable; /// Threshold participant identifier length. pub const BLS_ID_LEN: usize = 32; /// Threshold participant identifier. -#[derive(Unencodable)] +#[cfg_attr(feature = "codec", derive(Unencodable))] pub struct BlsShareId { inner: [u8; BLS_ID_LEN], } diff --git a/pkgs/pkc/src/bls/share_ops.rs b/pkgs/pkc/src/bls/share_ops.rs index a2b597b3..6db12196 100644 --- a/pkgs/pkc/src/bls/share_ops.rs +++ b/pkgs/pkc/src/bls/share_ops.rs @@ -15,6 +15,7 @@ use super::BlsShareId; use crate::prelude::*; use dash_types::qtypestr; +#[cfg(feature = "codec")] use dash_types::type_id::Unencodable; use rand_core::CryptoRng; @@ -22,7 +23,7 @@ use core::fmt::{Debug, Formatter, Result as FmtResult}; use core::hash::{Hash, Hasher}; /// Secret key share for threshold signing. -#[derive(Unencodable)] +#[cfg_attr(feature = "codec", derive(Unencodable))] pub struct BlsSkShare { id: BlsShareId, sk: BlsSecretKey, @@ -70,7 +71,7 @@ impl Debug for BlsSkShare { } /// Signature share from a threshold participant. -#[derive(Unencodable)] +#[cfg_attr(feature = "codec", derive(Unencodable))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound(serialize = "", deserialize = "")))] pub struct BlsSigShare { diff --git a/pkgs/pkc/src/bls/sig_basic.rs b/pkgs/pkc/src/bls/sig_basic.rs index 16dd2fa3..2161254d 100644 --- a/pkgs/pkc/src/bls/sig_basic.rs +++ b/pkgs/pkc/src/bls/sig_basic.rs @@ -10,23 +10,30 @@ use super::error::BlsError; use super::group::G2; use super::public_ops::BlsPublicKey; use super::scheme_ops::BlsScheme; -use super::{BlsScIetf, BlsSigBytes, BlsSigId, BLS_SIG_LEN}; +#[cfg(feature = "codec")] +use super::BLS_SIG_LEN; +use super::{BlsScIetf, BlsSigBytes, BlsSigId}; +#[cfg(feature = "codec")] use dash_num::Hash256; +#[cfg(feature = "codec")] +use dash_types::dlgt_codec; +#[cfg(feature = "codec")] use dash_types::type_id::TypeId; -use dash_types::{dlgt_codec, qtypestr, type_cvrt}; +use dash_types::{qtypestr, type_cvrt}; use hex_conservative::DisplayHex; use core::fmt::{Debug, Formatter, Result as FmtResult}; use core::hash::{Hash, Hasher}; /// A BLS signature (96-byte compressed G2 point) +#[cfg_attr(feature = "codec", derive(TypeId))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(into = "BlsSigBytes", try_from = "BlsSigBytes"))] #[cfg_attr(feature = "serde", serde(bound(serialize = "", deserialize = "")))] -#[derive(TypeId)] pub struct BlsSignature(pub(crate) S::InnerSig); +#[cfg(feature = "codec")] dlgt_codec!(for[S: BlsScheme] BlsSignature => BlsSigBytes, Hash256, BlsError, BLS_SIG_LEN); impl BlsSignature { diff --git a/pkgs/pkc/src/bls/sig_bytes.rs b/pkgs/pkc/src/bls/sig_bytes.rs index 112348df..201149fb 100644 --- a/pkgs/pkc/src/bls/sig_bytes.rs +++ b/pkgs/pkc/src/bls/sig_bytes.rs @@ -8,11 +8,17 @@ use crate::bls::BlsSchemeId; +#[cfg(feature = "codec")] use bitcoin_hashes::sha256d::Hash as Sha256d; +#[cfg(feature = "codec")] use dash_num::Hash256; +#[cfg(feature = "codec")] use dash_types::codec::Hashable; +use dash_types::derive_bytes; +#[cfg(feature = "codec")] +use dash_types::impl_bytes; +#[cfg(feature = "codec")] use dash_types::type_id::TypeId; -use dash_types::{derive_bytes, impl_bytes}; use core::marker::PhantomData; @@ -20,14 +26,16 @@ use core::marker::PhantomData; pub const BLS_SIG_LEN: usize = 96; /// Scheme-tagged BLS signature bytes (96 bytes, unvalidated). -#[derive(TypeId)] +#[cfg_attr(feature = "codec", derive(TypeId))] pub struct BlsSigBytes { inner: [u8; BLS_SIG_LEN], _scheme: PhantomData, } +#[cfg(feature = "codec")] impl_bytes!(for[S: BlsSchemeId] BlsSigBytes, BLS_SIG_LEN); +#[cfg(feature = "codec")] impl Hashable for BlsSigBytes { type Hash = Hash256; diff --git a/pkgs/pkc/src/bls/sig_id.rs b/pkgs/pkc/src/bls/sig_id.rs index 0172553a..fa45e17a 100644 --- a/pkgs/pkc/src/bls/sig_id.rs +++ b/pkgs/pkc/src/bls/sig_id.rs @@ -6,10 +6,12 @@ //! Signature types. +#[cfg(feature = "codec")] use dash_types::type_id::Unencodable; /// BLS signature variant. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Unencodable)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "codec", derive(Unencodable))] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub enum BlsSigId { /// Basic scheme (NUL augmentation). diff --git a/pkgs/pkc/src/lib.rs b/pkgs/pkc/src/lib.rs index ac201ec1..1758c974 100644 --- a/pkgs/pkc/src/lib.rs +++ b/pkgs/pkc/src/lib.rs @@ -16,9 +16,14 @@ extern crate std; mod prelude; pub mod bls; -pub mod ecdsa; -#[doc(hidden)] -pub mod __private { - pub use crate::ecdsa::PubKeyHash as __PubKeyHash; +cfg_if::cfg_if! { + if #[cfg(feature = "codec")] { + pub mod ecdsa; + + #[doc(hidden)] + pub mod __private { + pub use crate::ecdsa::PubKeyHash as __PubKeyHash; + } + } } diff --git a/pkgs/primitives/Cargo.toml b/pkgs/primitives/Cargo.toml index 2b4de875..2748f6d1 100644 --- a/pkgs/primitives/Cargo.toml +++ b/pkgs/primitives/Cargo.toml @@ -36,7 +36,9 @@ bitcoin-primitives = { workspace = true, features = ["alloc"] } bitcoin_hashes = { workspace = true, features = ["alloc"] } bitcoin-units = { workspace = true, features = ["alloc"] } dash-num = { version = "0.0.0", path = "../num" } -dash-pkc = { version = "0.0.0", path = "../pkc", default-features = false } +dash-pkc = { version = "0.0.0", path = "../pkc", default-features = false, features = [ + "codec", +] } dash-pow = { version = "0.0.0", path = "../pow" } dash-script = { version = "0.0.0", path = "../script" } dash-types = { version = "0.0.0", path = "../types", default-features = false, features = [ diff --git a/pkgs/script/Cargo.toml b/pkgs/script/Cargo.toml index c799a5af..cfde4c56 100644 --- a/pkgs/script/Cargo.toml +++ b/pkgs/script/Cargo.toml @@ -19,7 +19,9 @@ serde = ["dep:serde", "dash-pkc/serde", "dash-types/serde"] base58ck = { workspace = true, features = ["alloc"] } bitcoin-consensus-encoding = { workspace = true, features = ["alloc"] } dash-num = { version = "0.0.0", path = "../num", default-features = false } -dash-pkc = { version = "0.0.0", path = "../pkc", default-features = false } +dash-pkc = { version = "0.0.0", path = "../pkc", default-features = false, features = [ + "codec", +] } dash-types = { version = "0.0.0", path = "../types", default-features = false, features = [ "bitcoin-primitives", "codec", From 060e9c1cc0b23bbef0fd0cc1eab0b68e16a72162 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:12:38 +0530 Subject: [PATCH 4/6] pkc%feat(bls): implement BLS-IES KDF and AES-CBC routines --- Cargo.lock | 32 ++++++++++ pkgs/pkc/Cargo.toml | 5 +- pkgs/pkc/src/aes_cbc.rs | 109 +++++++++++++++++++++++++++++++++ pkgs/pkc/src/bls/error.rs | 6 ++ pkgs/pkc/src/bls/scheme_ops.rs | 60 +++++++++++++++++- pkgs/pkc/src/bls/secret_ops.rs | 3 +- pkgs/pkc/src/lib.rs | 2 + 7 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 pkgs/pkc/src/aes_cbc.rs diff --git a/Cargo.lock b/Cargo.lock index 6c5b0099..7ecbd362 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", + "zeroize", +] + [[package]] name = "aho-corasick" version = "1.1.5" @@ -335,6 +347,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.6", + "inout", +] + [[package]] name = "clap" version = "4.6.5" @@ -591,6 +613,7 @@ dependencies = [ name = "dash-pkc" version = "0.0.0" dependencies = [ + "aes", "base58ck", "bitcoin-consensus-encoding", "bitcoin_hashes", @@ -1175,6 +1198,15 @@ dependencies = [ "web-time", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "itoa" version = "1.0.18" diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index 4680891e..f75b812b 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" license = "MIT" [dependencies] +aes = { version = "0.8", default-features = false, features = [ + "zeroize", +], optional = true } base58ck = { workspace = true, optional = true, features = ["alloc"] } bitcoin_hashes = { workspace = true, optional = true, features = ["alloc"] } blst = { version = "0.3", default-features = false, optional = true } @@ -47,7 +50,7 @@ serde = { version = "1", features = ["derive"] } [features] default = [] std = ["base58ck?/std", "bitcoin_hashes?/std", "dash-types/std"] -bls = ["dep:blst", "dep:ff", "dep:group", "dep:rand_core", "dep:sha2"] +bls = ["dep:aes", "dep:blst", "dep:ff", "dep:group", "dep:rand_core", "dep:sha2"] codec = ["dep:base58ck", "dep:bitcoin_hashes", "dep:dash-num", "dash-types/codec"] ecdsa = ["codec", "dep:k256", "dep:rand_core"] serde = ["dep:serde", "dash-num?/serde", "dash-types/serde"] diff --git a/pkgs/pkc/src/aes_cbc.rs b/pkgs/pkc/src/aes_cbc.rs new file mode 100644 index 00000000..28e0977c --- /dev/null +++ b/pkgs/pkc/src/aes_cbc.rs @@ -0,0 +1,109 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Unpadded AES-256-CBC. + +use crate::prelude::*; + +use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit}; +use aes::Aes256; +use zeroize::{Zeroize, Zeroizing}; + +/// AES block length in bytes. +pub(crate) const AES_BLOCK_LEN: usize = 16; + +/// AES-256 key length in bytes. +pub(crate) const AES_KEY_LEN: usize = 32; + +/// Encrypts under unpadded AES-256-CBC, or `None` when the input is not a +/// whole number of blocks. +pub(crate) fn encrypt(key: &[u8; AES_KEY_LEN], iv: &[u8; AES_BLOCK_LEN], plaintext: &[u8]) -> Option> { + if plaintext.len() % AES_BLOCK_LEN != 0 { + return None; + } + + let cipher = Aes256::new(key.into()); + let mut output = Vec::with_capacity(plaintext.len()); + let mut chain = *iv; + + for plain in plaintext.chunks_exact(AES_BLOCK_LEN) { + let mut block = aes::Block::default(); + for (out, (p, c)) in block.iter_mut().zip(plain.iter().zip(&chain)) { + *out = p ^ c; + } + cipher.encrypt_block(&mut block); + chain.copy_from_slice(&block); + output.extend_from_slice(&block); // nosemgrep: codec-no-raw-extend + } + + Some(output) +} + +/// Decrypts under unpadded AES-256-CBC, or `None` when the input is not a +/// whole number of blocks. +pub(crate) fn decrypt( + key: &[u8; AES_KEY_LEN], + iv: &[u8; AES_BLOCK_LEN], + ciphertext: &[u8], +) -> Option>> { + if ciphertext.len() % AES_BLOCK_LEN != 0 { + return None; + } + + let cipher = Aes256::new(key.into()); + let mut output = Zeroizing::new(Vec::with_capacity(ciphertext.len())); + let mut chain = *iv; + + for cipher_text in ciphertext.chunks_exact(AES_BLOCK_LEN) { + let mut block = aes::Block::default(); + block.copy_from_slice(cipher_text); + cipher.decrypt_block(&mut block); + for (out, c) in block.iter().zip(&chain) { + output.push(out ^ c); + } + chain.copy_from_slice(cipher_text); + block[..].zeroize(); + } + + Some(output) +} + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + + use rstest::rstest; + + const KEY: [u8; AES_KEY_LEN] = [0x5a; AES_KEY_LEN]; + const IV: [u8; AES_BLOCK_LEN] = [0xcd; AES_BLOCK_LEN]; + + #[rstest] + #[case::empty(vec![])] + #[case::one_block(vec![0x11; 16])] + #[case::many_blocks(vec![0x22; 64])] + fn cbc_roundtrips(#[case] plaintext: Vec) { + let ciphertext = encrypt(&KEY, &IV, &plaintext).unwrap(); + assert_eq!(*decrypt(&KEY, &IV, &ciphertext).unwrap(), plaintext); + } + + /// Identical plaintext blocks must not encrypt alike. + #[rstest] + fn cbc_chains_ciphertext_blocks() { + let ciphertext = encrypt(&KEY, &IV, &[0x11u8; 32]).unwrap(); + assert_ne!(ciphertext[..AES_BLOCK_LEN], ciphertext[AES_BLOCK_LEN..]); + } + + /// Neither direction may silently truncate a partial trailing block. + #[rstest] + #[case::one_over(17)] + #[case::under_a_block(15)] + fn cbc_rejects_a_partial_trailing_block(#[case] len: usize) { + let input = vec![0x11u8; len]; + assert!(encrypt(&KEY, &IV, &input).is_none()); + assert!(decrypt(&KEY, &IV, &input).is_none()); + } +} diff --git a/pkgs/pkc/src/bls/error.rs b/pkgs/pkc/src/bls/error.rs index 7df10b23..78d90304 100644 --- a/pkgs/pkc/src/bls/error.rs +++ b/pkgs/pkc/src/bls/error.rs @@ -21,8 +21,12 @@ pub enum BlsError { EmptyAggregation, /// not enough shares to recover InsufficientShares, + /// ciphertext is not a whole number of cipher blocks + InvalidCiphertextLength, /// input keying material is too short (need >= 32 bytes) InvalidKeyMaterial, + /// plaintext is not a whole number of cipher blocks + InvalidPlaintextLength, /// public key bytes are not a valid G1 point InvalidPublicKey, /// secret key bytes are not a valid scalar @@ -47,7 +51,9 @@ impl fmt::Display for BlsError { Self::DuplicateShareId => write!(f, "duplicate share id in recovery set"), Self::EmptyAggregation => write!(f, "no items provided for aggregation"), Self::InsufficientShares => write!(f, "not enough shares to recover"), + Self::InvalidCiphertextLength => write!(f, "ciphertext is not a whole number of cipher blocks"), Self::InvalidKeyMaterial => write!(f, "input keying material too short"), + Self::InvalidPlaintextLength => write!(f, "plaintext is not a whole number of cipher blocks"), Self::InvalidPublicKey => write!(f, "invalid public key bytes"), Self::InvalidSecretKey => write!(f, "invalid secret key bytes"), Self::InvalidShareId => write!(f, "share id reduces to zero in the scalar field"), diff --git a/pkgs/pkc/src/bls/scheme_ops.rs b/pkgs/pkc/src/bls/scheme_ops.rs index c9b2ed99..7bfa63a0 100644 --- a/pkgs/pkc/src/bls/scheme_ops.rs +++ b/pkgs/pkc/src/bls/scheme_ops.rs @@ -11,7 +11,8 @@ use super::error::BlsError; use super::group::{Point, G1, G2}; use super::scalar::{Fr, FR_BITS}; use super::schemes::BlsSchemeId; -use super::BlsShareId; +use super::{BlsDhBytes, BlsShareId}; +use crate::aes_cbc::{self, AES_BLOCK_LEN, AES_KEY_LEN}; use crate::prelude::*; use blst::BLST_ERROR; @@ -37,7 +38,7 @@ pub(crate) fn verify_ok(result: BLST_ERROR) -> Result<(), BlsError> { } /// BLS operations tied to a specific scheme. -pub trait BlsScheme: BlsSchemeId { +pub trait BlsScheme: BlsSchemeId + Sized { /// Inner secret key representation. type InnerSk: Clone + Send + Sync; /// Inner public key representation. @@ -148,6 +149,61 @@ pub trait BlsScheme: BlsSchemeId { Self::g1_to_pk(product) } + /// Serialize a Diffie-Hellman product. + /// + /// # Errors + /// + /// Returns `InvalidPublicKey` when the shared secret cannot be derived. + fn dh_bytes(sk: &Self::InnerSk, peer_pk: &Self::InnerPk) -> Result, BlsError> { + let shared = Self::dh_exchange(sk, peer_pk)?; + Ok(BlsDhBytes::from_bytes(Self::pk_to_bytes(&shared))) + } + + /// Derive the BLS-IES symmetric key from a Diffie-Hellman shared key. + /// + /// The key is the leading 32 bytes of the shared point's serialization, + /// rendering it sensitive to scheme selection. All operations are therefore + /// effectively restricted to the initial scheme choice. + fn ies_key(shared: &BlsDhBytes) -> Zeroizing<[u8; AES_KEY_LEN]> { + let mut key = Zeroizing::new([0u8; AES_KEY_LEN]); + key.copy_from_slice(&shared.as_bytes()[..AES_KEY_LEN]); + key + } + + /// Seal one plaintext to a recipient under an ephemeral key and IV. + /// + /// # Errors + /// + /// Returns `InvalidPlaintextLength` when the plaintext is not a whole + /// number of 16-byte blocks, or `InvalidPublicKey` when the shared secret + /// cannot be derived. + fn ies_seal( + eph_sk: &Self::InnerSk, + recipient: &Self::InnerPk, + iv: &[u8; AES_BLOCK_LEN], + plaintext: &[u8], + ) -> Result, BlsError> { + let key = Self::ies_key(&Self::dh_bytes(eph_sk, recipient)?); + aes_cbc::encrypt(&key, iv, plaintext).ok_or(BlsError::InvalidPlaintextLength) + } + + /// Open one ciphertext sealed to this key under an ephemeral key and IV. + /// + /// # Errors + /// + /// Returns `InvalidCiphertextLength` when the ciphertext is not a whole + /// number of 16-byte blocks, or `InvalidPublicKey` when the shared secret + /// cannot be derived. + fn ies_open( + sk: &Self::InnerSk, + eph_pk: &Self::InnerPk, + iv: &[u8; AES_BLOCK_LEN], + ciphertext: &[u8], + ) -> Result>, BlsError> { + let key = Self::ies_key(&Self::dh_bytes(sk, eph_pk)?); + aes_cbc::decrypt(&key, iv, ciphertext).ok_or(BlsError::InvalidCiphertextLength) + } + /// Aggregate public keys into one. /// /// # Errors diff --git a/pkgs/pkc/src/bls/secret_ops.rs b/pkgs/pkc/src/bls/secret_ops.rs index 77c106c2..df538054 100644 --- a/pkgs/pkc/src/bls/secret_ops.rs +++ b/pkgs/pkc/src/bls/secret_ops.rs @@ -90,8 +90,7 @@ impl BlsSecretKey { /// Returns `InvalidPublicKey` when the peer key or the product point /// is invalid. pub fn dh_exchange(&self, peer_pk: &BlsPublicKey) -> Result, BlsError> { - let shared = S::dh_exchange(&self.0, &peer_pk.0)?; - Ok(BlsDhBytes::from_bytes(S::pk_to_bytes(&shared))) + S::dh_bytes(&self.0, &peer_pk.0) } /// Sum multiple secret keys (mod group order). diff --git a/pkgs/pkc/src/lib.rs b/pkgs/pkc/src/lib.rs index 1758c974..e929d2d8 100644 --- a/pkgs/pkc/src/lib.rs +++ b/pkgs/pkc/src/lib.rs @@ -12,6 +12,8 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; +#[cfg(feature = "bls")] +mod aes_cbc; #[allow(unused_imports, reason = "ergonomic shim, exports may be unused")] mod prelude; From f58d67d7b8ff541b590210ced19d01ce3ad7cc4c Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:05:20 +0530 Subject: [PATCH 5/6] pkc%feat(bls): implement byte bags for BLS-IES encrypted blobs --- pkgs/pkc/Cargo.toml | 2 +- pkgs/pkc/corpus/bls_ies.json5 | 94 ++++++ pkgs/pkc/src/bls/error.rs | 3 + pkgs/pkc/src/bls/ies_bytes.rs | 525 ++++++++++++++++++++++++++++++++++ pkgs/pkc/src/bls/mod.rs | 2 + 5 files changed, 625 insertions(+), 1 deletion(-) create mode 100644 pkgs/pkc/corpus/bls_ies.json5 create mode 100644 pkgs/pkc/src/bls/ies_bytes.rs diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index f75b812b..cca4f7fa 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -53,7 +53,7 @@ std = ["base58ck?/std", "bitcoin_hashes?/std", "dash-types/std"] bls = ["dep:aes", "dep:blst", "dep:ff", "dep:group", "dep:rand_core", "dep:sha2"] codec = ["dep:base58ck", "dep:bitcoin_hashes", "dep:dash-num", "dash-types/codec"] ecdsa = ["codec", "dep:k256", "dep:rand_core"] -serde = ["dep:serde", "dash-num?/serde", "dash-types/serde"] +serde = ["codec", "dep:serde", "dash-num/serde", "dash-types/serde"] full = ["bls", "codec", "ecdsa", "serde", "std", "tests"] tests = ["std", "dep:rstest"] diff --git a/pkgs/pkc/corpus/bls_ies.json5 b/pkgs/pkc/corpus/bls_ies.json5 new file mode 100644 index 00000000..f5b071b3 --- /dev/null +++ b/pkgs/pkc/corpus/bls_ies.json5 @@ -0,0 +1,94 @@ +{ + "chia": { + "blob": [ + { + "data_len": 0, + "image": "8efe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb010101010101010101010101010101010101010101010101010101010101010100" + }, + { + "data_len": 16, + "image": "8efe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb01010101010101010101010101010101010101010101010101010101010101011011111111111111111111111111111111" + }, + { + "data_len": 32, + "image": "8efe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb0101010101010101010101010101010101010101010101010101010101010101201111111111111111111111111111111111111111111111111111111111111111" + }, + { + "data_len": 252, + "image": "8efe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb0101010101010101010101010101010101010101010101010101010101010101fc111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + }, + { + "data_len": 253, + "image": "8efe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb0101010101010101010101010101010101010101010101010101010101010101fdfd0011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + } + ], + "multi": [ + { + "blob_lens": [], + "image": "8efe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb010101010101010101010101010101010101010101010101010101010101010100" + }, + { + "blob_lens": [0], + "image": "8efe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb01010101010101010101010101010101010101010101010101010101010101010100" + }, + { + "blob_lens": [16], + "image": "8efe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb01010101010101010101010101010101010101010101010101010101010101010110a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0" + }, + { + "blob_lens": [16, 0, 32], + "image": "8efe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb01010101010101010101010101010101010101010101010101010101010101010310a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a00020a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2" + }, + { + "blob_lens": [252, 253], + "image": "8efe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb010101010101010101010101010101010101010101010101010101010101010102fca0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0fdfd00a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + } + ] + }, + "ietf": { + "blob": [ + { + "data_len": 0, + "image": "aefe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb010101010101010101010101010101010101010101010101010101010101010100" + }, + { + "data_len": 16, + "image": "aefe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb01010101010101010101010101010101010101010101010101010101010101011011111111111111111111111111111111" + }, + { + "data_len": 32, + "image": "aefe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb0101010101010101010101010101010101010101010101010101010101010101201111111111111111111111111111111111111111111111111111111111111111" + }, + { + "data_len": 252, + "image": "aefe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb0101010101010101010101010101010101010101010101010101010101010101fc111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + }, + { + "data_len": 253, + "image": "aefe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb0101010101010101010101010101010101010101010101010101010101010101fdfd0011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + } + ], + "multi": [ + { + "blob_lens": [], + "image": "aefe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb010101010101010101010101010101010101010101010101010101010101010100" + }, + { + "blob_lens": [0], + "image": "aefe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb01010101010101010101010101010101010101010101010101010101010101010100" + }, + { + "blob_lens": [16], + "image": "aefe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb01010101010101010101010101010101010101010101010101010101010101010110a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0" + }, + { + "blob_lens": [16, 0, 32], + "image": "aefe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb01010101010101010101010101010101010101010101010101010101010101010310a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a00020a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2" + }, + { + "blob_lens": [252, 253], + "image": "aefe1789d6476f60439e1168f588ea16652dc321279f05a805fbc63933e88ae9c175d6c6ab182e54af562e1a0dce41bb010101010101010101010101010101010101010101010101010101010101010102fca0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0fdfd00a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + } + ] + } +} diff --git a/pkgs/pkc/src/bls/error.rs b/pkgs/pkc/src/bls/error.rs index 78d90304..559b9d7b 100644 --- a/pkgs/pkc/src/bls/error.rs +++ b/pkgs/pkc/src/bls/error.rs @@ -23,6 +23,8 @@ pub enum BlsError { InsufficientShares, /// ciphertext is not a whole number of cipher blocks InvalidCiphertextLength, + /// initialisation vector seed is all zeroes + InvalidIvSeed, /// input keying material is too short (need >= 32 bytes) InvalidKeyMaterial, /// plaintext is not a whole number of cipher blocks @@ -52,6 +54,7 @@ impl fmt::Display for BlsError { Self::EmptyAggregation => write!(f, "no items provided for aggregation"), Self::InsufficientShares => write!(f, "not enough shares to recover"), Self::InvalidCiphertextLength => write!(f, "ciphertext is not a whole number of cipher blocks"), + Self::InvalidIvSeed => write!(f, "initialisation vector seed is all zeroes"), Self::InvalidKeyMaterial => write!(f, "input keying material too short"), Self::InvalidPlaintextLength => write!(f, "plaintext is not a whole number of cipher blocks"), Self::InvalidPublicKey => write!(f, "invalid public key bytes"), diff --git a/pkgs/pkc/src/bls/ies_bytes.rs b/pkgs/pkc/src/bls/ies_bytes.rs new file mode 100644 index 00000000..cea8c408 --- /dev/null +++ b/pkgs/pkc/src/bls/ies_bytes.rs @@ -0,0 +1,525 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Byte bags for BLS-IES encrypted blobs. + +#[cfg(feature = "codec")] +use crate::bls::BlsError; +use crate::bls::{BlsPkBytes, BlsSchemeId}; +use crate::prelude::*; + +#[cfg(feature = "codec")] +use bitcoin_hashes::sha256d::Hash as Sha256d; +use cfg_if::cfg_if; +#[cfg(feature = "codec")] +use dash_num::Hash256; +#[cfg(feature = "codec")] +use dash_types::codec::{read_bytes, BaseCodec, Checkable, DecodeError, EncodeBuf, Hashable}; +#[cfg(feature = "codec")] +use dash_types::type_id::TypeId; +#[cfg(feature = "codec")] +use dash_types::{impl_type, CompactSize}; +use hex_conservative::DisplayHex; + +use core::fmt; +use core::hash::{Hash, Hasher}; + +/// Byte length of the seed a BLS-IES message derives its IVs from. +pub const IV_SEED_LEN: usize = 32; + +/// Largest recipient count (and index), an IV is derived for. +pub const MAX_IES_RECIPIENTS: usize = 2048; + +/// A BLS-IES encrypted blob under an ephemeral key. +#[cfg_attr(feature = "codec", derive(TypeId))] +pub struct BlsIesBlobBytes { + ephemeral_pk: BlsPkBytes, + iv_seed: [u8; IV_SEED_LEN], + data: Vec, +} + +#[cfg(feature = "codec")] +impl BaseCodec for BlsIesBlobBytes { + fn decode(data: &mut &[u8]) -> Result { + let ephemeral_pk = BlsPkBytes::::decode(data)?; + let iv_seed = <[u8; IV_SEED_LEN]>::decode(data)?; + let len = CompactSize::decode(data)?.into_len(data.len())?; + Ok(Self { + ephemeral_pk, + iv_seed, + data: read_bytes(data, len)?.to_vec(), + }) + } + + fn encode(&self, buf: &mut impl EncodeBuf) { + self.ephemeral_pk.encode(buf); + self.iv_seed.encode(buf); + CompactSize::from(self.data.len()).encode(buf); + buf.extend_from_slice(&self.data); // nosemgrep: codec-no-raw-extend + } +} + +#[cfg(feature = "codec")] +impl_type!(for[S: BlsSchemeId] BlsIesBlobBytes); + +#[cfg(feature = "codec")] +impl Checkable for BlsIesBlobBytes { + type Error = BlsError; + + fn check(&self) -> Option { + if self.ephemeral_pk.is_null() { + return Some(BlsError::InvalidPublicKey); + } + if self.data.is_empty() { + return Some(BlsError::InvalidCiphertextLength); + } + if self.iv_seed.iter().all(|&b| b == 0) { + return Some(BlsError::InvalidIvSeed); + } + None + } +} + +#[cfg(feature = "codec")] +impl Hashable for BlsIesBlobBytes { + type Hash = Hash256; + + fn hash(&self) -> Self::Hash { + Hash256::from_bytes(Sha256d::hash(&self.to_bytes()).to_byte_array()) + } +} + +impl BlsIesBlobBytes { + /// Constructs from raw components. + pub fn new(ephemeral_pk: BlsPkBytes, iv_seed: [u8; IV_SEED_LEN], data: Vec) -> Self { + Self { + ephemeral_pk, + iv_seed, + data, + } + } + + /// Borrows the ciphertext. + pub fn data(&self) -> &[u8] { + &self.data + } + + /// The ephemeral public key the sender encrypted under. + pub fn ephemeral_pk(&self) -> &BlsPkBytes { + &self.ephemeral_pk + } + + /// The seed the recipient's IV is derived from. + pub fn iv_seed(&self) -> &[u8; IV_SEED_LEN] { + &self.iv_seed + } +} + +#[cfg(feature = "codec")] +impl BlsIesBlobBytes { + /// The full wire image. + pub fn to_bytes(&self) -> Vec { + let mut buf = Vec::new(); + self.encode(&mut buf); + buf + } +} + +impl Clone for BlsIesBlobBytes { + fn clone(&self) -> Self { + Self { + ephemeral_pk: self.ephemeral_pk, + iv_seed: self.iv_seed, + data: self.data.clone(), + } + } +} + +impl fmt::Debug for BlsIesBlobBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BlsIesBlobBytes") + .field("ephemeral_pk", &self.ephemeral_pk) + .field("iv_seed", &self.iv_seed.as_hex()) + .field("data_len", &self.data.len()) + .finish() + } +} + +impl Eq for BlsIesBlobBytes {} + +impl Hash for BlsIesBlobBytes { + fn hash(&self, state: &mut H) { + self.ephemeral_pk.as_bytes().hash(state); + self.iv_seed.hash(state); + self.data.hash(state); + } +} + +impl PartialEq for BlsIesBlobBytes { + fn eq(&self, other: &Self) -> bool { + self.ephemeral_pk == other.ephemeral_pk && self.iv_seed == other.iv_seed && self.data == other.data + } +} + +/// One ciphertext per recipient under an ephemeral key. +#[cfg_attr(feature = "codec", derive(TypeId))] +pub struct BlsIesMultiBytes { + ephemeral_pk: BlsPkBytes, + iv_seed: [u8; IV_SEED_LEN], + blobs: Vec>, +} + +#[cfg(feature = "codec")] +impl BaseCodec for BlsIesMultiBytes { + fn decode(data: &mut &[u8]) -> Result { + let ephemeral_pk = BlsPkBytes::::decode(data)?; + let iv_seed = <[u8; IV_SEED_LEN]>::decode(data)?; + + // Each element is a `Vec` far wider than the length prefix that admitted it + // so capacity is grown against content, not reserved. + let count = CompactSize::decode(data)?.into_len(data.len())?; + let mut blobs = Vec::new(); + for _ in 0..count { + let len = CompactSize::decode(data)?.into_len(data.len())?; + blobs.push(read_bytes(data, len)?.to_vec()); + } + + Ok(Self { + ephemeral_pk, + iv_seed, + blobs, + }) + } + + fn encode(&self, buf: &mut impl EncodeBuf) { + self.ephemeral_pk.encode(buf); + self.iv_seed.encode(buf); + CompactSize::from(self.blobs.len()).encode(buf); + for blob in &self.blobs { + CompactSize::from(blob.len()).encode(buf); + buf.extend_from_slice(blob); // nosemgrep: codec-no-raw-extend + } + } +} + +#[cfg(feature = "codec")] +impl_type!(for[S: BlsSchemeId] BlsIesMultiBytes); + +#[cfg(feature = "codec")] +impl Hashable for BlsIesMultiBytes { + type Hash = Hash256; + + fn hash(&self) -> Self::Hash { + Hash256::from_bytes(Sha256d::hash(&self.to_bytes()).to_byte_array()) + } +} + +impl BlsIesMultiBytes { + /// Constructs from raw components. + pub fn new(ephemeral_pk: BlsPkBytes, iv_seed: [u8; IV_SEED_LEN], blobs: Vec>) -> Self { + Self { + ephemeral_pk, + iv_seed, + blobs, + } + } + + /// Borrows the per-recipient ciphertexts. + pub fn blobs(&self) -> &[Vec] { + &self.blobs + } + + /// The ephemeral public key the sender encrypted under. + pub fn ephemeral_pk(&self) -> &BlsPkBytes { + &self.ephemeral_pk + } + + /// The seed every recipient's IV is derived from. + pub fn iv_seed(&self) -> &[u8; IV_SEED_LEN] { + &self.iv_seed + } +} + +#[cfg(feature = "codec")] +impl BlsIesMultiBytes { + /// The full wire image. + pub fn to_bytes(&self) -> Vec { + let mut buf = Vec::new(); + self.encode(&mut buf); + buf + } +} + +impl Clone for BlsIesMultiBytes { + fn clone(&self) -> Self { + Self { + ephemeral_pk: self.ephemeral_pk, + iv_seed: self.iv_seed, + blobs: self.blobs.clone(), + } + } +} + +impl fmt::Debug for BlsIesMultiBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BlsIesMultiBytes") + .field("ephemeral_pk", &self.ephemeral_pk) + .field("iv_seed", &self.iv_seed.as_hex()) + .field("blob_count", &self.blobs.len()) + .finish() + } +} + +impl Eq for BlsIesMultiBytes {} + +impl Hash for BlsIesMultiBytes { + fn hash(&self, state: &mut H) { + self.ephemeral_pk.as_bytes().hash(state); + self.iv_seed.hash(state); + self.blobs.hash(state); + } +} + +impl PartialEq for BlsIesMultiBytes { + fn eq(&self, other: &Self) -> bool { + self.ephemeral_pk == other.ephemeral_pk && self.iv_seed == other.iv_seed && self.blobs == other.blobs + } +} + +cfg_if! { + if #[cfg(feature = "codec")] { + cfg_if! { + if #[cfg(feature = "serde")] { + use dash_types::serialize::hex as serde_hex; + use serde::de::Error as DeError; + use serde::{Deserializer, Serializer}; + + /// Decodes a whole hex image, rejecting any unconsumed tail. + fn from_image<'de, T: BaseCodec, D: Deserializer<'de>>(deserializer: D) -> Result { + let bytes = serde_hex::deserialize(deserializer)?; + let mut cursor = bytes.as_slice(); + let value = T::decode(&mut cursor).map_err(DeError::custom)?; + + if !cursor.is_empty() { + return Err(DeError::custom("trailing bytes after encoded value")); + } + + Ok(value) + } + + impl ::serde::Serialize for BlsIesBlobBytes { + fn serialize(&self, serializer: T) -> Result { + serde_hex::serialize(&self.to_bytes(), serializer) + } + } + + impl<'de, S: BlsSchemeId> ::serde::Deserialize<'de> for BlsIesBlobBytes { + fn deserialize>(deserializer: D) -> Result { + from_image(deserializer) + } + } + + impl ::serde::Serialize for BlsIesMultiBytes { + fn serialize(&self, serializer: T) -> Result { + serde_hex::serialize(&self.to_bytes(), serializer) + } + } + + impl<'de, S: BlsSchemeId> ::serde::Deserialize<'de> for BlsIesMultiBytes { + fn deserialize>(deserializer: D) -> Result { + from_image(deserializer) + } + } + } + } + } +} + +#[cfg(all(test, feature = "codec"))] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::bls::BlsScIetf; + + use rstest::rstest; + + fn blob(data: Vec) -> BlsIesBlobBytes { + BlsIesBlobBytes::new(BlsPkBytes::from_bytes([0xab; 48]), [0xcd; IV_SEED_LEN], data) + } + + fn multi(blobs: Vec>) -> BlsIesMultiBytes { + BlsIesMultiBytes::new(BlsPkBytes::from_bytes([0xab; 48]), [0xcd; IV_SEED_LEN], blobs) + } + + #[rstest] + fn layout_matches_reference() { + let encoded = blob(vec![0x11; 3]).to_bytes(); + + assert_eq!(encoded[..48], [0xab; 48]); + assert_eq!(encoded[48..80], [0xcd; 32]); + assert_eq!(encoded[80], 3); + assert_eq!(encoded.len(), 84); + + let encoded = multi(vec![vec![0x11; 2], vec![0x22; 1]]).to_bytes(); + assert_eq!(encoded[80], 2, "blob count precedes the blobs"); + assert_eq!(encoded[81], 2); + assert_eq!(encoded[84], 1); + assert_eq!(encoded.len(), 86); + } + + #[rstest] + #[case::empty(vec![])] + #[case::one_block(vec![0x11; 16])] + fn blob_codec_roundtrips(#[case] data: Vec) { + let bag = blob(data); + let encoded = bag.to_bytes(); + assert_eq!(BlsIesBlobBytes::decode(&mut encoded.as_slice()).unwrap(), bag); + } + + #[rstest] + #[case::none(vec![])] + #[case::ragged(vec![vec![0x11; 16], vec![], vec![0x22; 32]])] + fn multi_codec_roundtrips(#[case] blobs: Vec>) { + let bag = multi(blobs); + let encoded = bag.to_bytes(); + assert_eq!(BlsIesMultiBytes::decode(&mut encoded.as_slice()).unwrap(), bag); + } + + #[rstest] + fn decode_leaves_trailing_input_for_the_next_reader() { + let bag = blob(vec![0x11; 16]); + let image: Vec = bag.to_bytes().into_iter().chain([0xff; 4]).collect(); + + let mut cursor = image.as_slice(); + assert_eq!(BlsIesBlobBytes::::decode(&mut cursor).unwrap(), bag); + assert_eq!(cursor, [0xff; 4], "the suffix is the caller's to read"); + } + + /// A length claiming more than the input holds must fail rather than + /// allocate for it. + #[rstest] + fn decode_rejects_overlong_lengths() { + let mut encoded = blob(vec![0x11; 3]).to_bytes(); + encoded[80] = 0xfe; + assert!(BlsIesBlobBytes::::decode(&mut encoded.as_slice()).is_err()); + + let mut encoded = multi(vec![vec![0x11; 3]]).to_bytes(); + encoded[80] = 0xfe; + assert!(BlsIesMultiBytes::::decode(&mut encoded.as_slice()).is_err()); + } + + cfg_if! { + if #[cfg(feature = "bls")] { + use crate::bls::tests::RSEED; + use crate::bls::{BlsScChia, BlsScheme, BlsSecretKey}; + + use dash_dev::{vec_from_hex, Corpus}; + use serde::Deserialize; + + #[derive(Deserialize)] + struct BlobVec { + data_len: usize, + image: String, + } + + #[derive(Deserialize)] + struct MultiVec { + blob_lens: Vec, + image: String, + } + + fn assert_codec_vectors(scheme: &str) { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ies").scope(scheme); + let eph_pk = BlsSecretKey::::generate(&RSEED[1]).unwrap().public_key().to_bytes(); + + for v in corpus.vectors::("blob") { + let image = vec_from_hex(&v.image); + let bag = BlsIesBlobBytes::::decode(&mut image.as_slice()).unwrap(); + + assert_eq!(bag.ephemeral_pk().as_bytes(), &eph_pk); + assert_eq!(bag.iv_seed(), &RSEED[1]); + assert_eq!(bag.data().len(), v.data_len); + assert_eq!(bag.to_bytes(), image); + } + + for v in corpus.vectors::("multi") { + let image = vec_from_hex(&v.image); + let bag = BlsIesMultiBytes::::decode(&mut image.as_slice()).unwrap(); + + assert_eq!(bag.ephemeral_pk().as_bytes(), &eph_pk); + assert_eq!(bag.iv_seed(), &RSEED[1]); + assert_eq!(bag.blobs().iter().map(Vec::len).collect::>(), v.blob_lens); + assert_eq!(bag.to_bytes(), image); + } + } + + #[rstest] + #[case::chia(assert_codec_vectors::, "chia")] + #[case::ietf(assert_codec_vectors::, "ietf")] + fn codec_matches_the_reference(#[case] assertion: fn(&str), #[case] scheme: &str) { + assertion(scheme); + } + } + } + + /// Null key, empty ciphertext, null seed, and nothing besides. + /// + /// A fault the reference does not name is not ours to add, which is also + /// why the multi-recipient bag has no `Checkable` at all. + #[rstest] + fn check_mirrors_core_is_valid() { + assert_eq!(blob(vec![0x11; 16]).check(), None); + assert_eq!(blob(vec![]).check(), Some(BlsError::InvalidCiphertextLength)); + + let null_seed = + BlsIesBlobBytes::::new(BlsPkBytes::from_bytes([0xab; 48]), [0; IV_SEED_LEN], vec![0x11; 16]); + assert_eq!(null_seed.check(), Some(BlsError::InvalidIvSeed)); + + let null_pk = + BlsIesBlobBytes::::new(BlsPkBytes::from_bytes([0; 48]), [0xcd; IV_SEED_LEN], vec![0x11; 16]); + assert_eq!(null_pk.check(), Some(BlsError::InvalidPublicKey)); + + // A ragged blob clears the gate and is left to fail at the cipher. + assert_eq!(blob(vec![0x11; 17]).check(), None); + } + + cfg_if! { + if #[cfg(feature = "serde")] { + use dash_dev::assert_json_rt; + + #[rstest] + fn serde_roundtrips() { + assert_json_rt(&blob(vec![0x11; 16])); + assert_json_rt(&multi(vec![vec![0x11; 16], vec![0x22; 16]])); + } + + /// A suffix the decoder never reaches would re-serialize shortened, so + /// the image has to be consumed whole. + #[rstest] + fn serde_rejects_trailing_bytes() { + use dash_dev::json_rejects; + + fn quoted(bytes: &[u8]) -> String { + alloc::format!("\"{}\"", bytes.as_hex()) + } + + fn with_suffix(bytes: &[u8]) -> Vec { + let mut padded = bytes.to_vec(); + padded.push(0xff); + padded + } + + let image = blob(vec![0x11; 16]).to_bytes(); + assert!(!json_rejects::>("ed(&image))); + assert!(json_rejects::>("ed(&with_suffix(&image)))); + + let image = multi(vec![vec![0x11; 16]]).to_bytes(); + assert!(!json_rejects::>("ed(&image))); + assert!(json_rejects::>("ed(&with_suffix(&image)))); + } + } + } +} diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index b5ec6fd1..f1eec48b 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -8,6 +8,7 @@ mod dh_bytes; mod error; +mod ies_bytes; mod public_bytes; mod schemes; mod secret_bytes; @@ -17,6 +18,7 @@ mod sig_id; pub use dh_bytes::{BlsDhBytes, BLS_DH_LEN}; pub use error::BlsError; +pub use ies_bytes::{BlsIesBlobBytes, BlsIesMultiBytes, IV_SEED_LEN, MAX_IES_RECIPIENTS}; pub use public_bytes::{BlsPkBytes, BLS_PK_LEN}; pub use schemes::{BlsScChia, BlsScIetf, BlsSchemeId}; pub use secret_bytes::{BlsSkBytes, BLS_SK_LEN}; From f568d49e7350ecb37db407a431542051a127630a Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:38:14 +0530 Subject: [PATCH 6/6] pkc%feat(bls): implement BLS-IES --- pkgs/dev/src/corpus.rs | 14 + pkgs/pkc/bench/bls.rs | 38 ++ pkgs/pkc/corpus/bls_dh.json5 | 62 +++ pkgs/pkc/src/bls/error.rs | 6 + pkgs/pkc/src/bls/ies_ops.rs | 900 +++++++++++++++++++++++++++++++++++ pkgs/pkc/src/bls/mod.rs | 2 + 6 files changed, 1022 insertions(+) create mode 100644 pkgs/pkc/src/bls/ies_ops.rs diff --git a/pkgs/dev/src/corpus.rs b/pkgs/dev/src/corpus.rs index e1b9d8ba..2028b446 100644 --- a/pkgs/dev/src/corpus.rs +++ b/pkgs/dev/src/corpus.rs @@ -149,6 +149,20 @@ impl Corpus { } } + /// Returns a named section as one typed value: `{ section: T }`, for + /// sections that hold a single object rather than an array. + /// + /// # Panics + /// + /// Panics if the section is missing or does not deserialize as `T`. + pub fn value(&self, section: &str) -> T { + let val = self + .root + .get(section) + .unwrap_or_else(|| panic!("{}: missing section '{section}'", self.name)); + serde_json::from_value(val.clone()).unwrap_or_else(|e| panic!("{}: section '{section}': {e}", self.name)) + } + /// Returns a named array section as typed vectors: `{ section: [T, ...] }`. /// /// # Panics diff --git a/pkgs/pkc/bench/bls.rs b/pkgs/pkc/bench/bls.rs index d5bf202d..2839ac8a 100644 --- a/pkgs/pkc/bench/bls.rs +++ b/pkgs/pkc/bench/bls.rs @@ -221,6 +221,44 @@ fn derive_share_n(bencher: Bencher, n: usize) { .bench(|| BlsSecretKey::::derive_share(&master_refs, &id)); } +/// Sealing one blob to one recipient, over a plaintext of `n` blocks. +#[divan::bench(types = [BlsScChia, BlsScIetf], args = [1, 2, 16])] +fn ies_encrypt_n(bencher: Bencher, n: usize) { + let pk = BlsSecretKey::::generate(&test_ikm(1)).unwrap().public_key(); + let plaintext = vec![0x42u8; n * 16]; + let mut rng = UnwrapErr(SysRng); + + bencher + .counter(ItemsCount::new(n)) + .bench_local(|| pk.ies_encrypt(&plaintext, &mut rng)); +} + +/// Opening one blob, paying for a DH exchange. +#[divan::bench(types = [BlsScChia, BlsScIetf], args = [1, 2, 16])] +fn ies_decrypt_n(bencher: Bencher, n: usize) { + let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); + let plaintext = vec![0x42u8; n * 16]; + let blob = sk.public_key().ies_encrypt(&plaintext, &mut UnwrapErr(SysRng)).unwrap(); + + bencher.counter(ItemsCount::new(n)).bench(|| sk.ies_decrypt(&blob, 0)); +} + +/// Sealing one 32-byte share to each of `n` recipients. +#[divan::bench(types = [BlsScChia, BlsScIetf], args = [2, 10, 100])] +fn ies_encrypt_multi_n(bencher: Bencher, n: usize) { + let pks: Vec<_> = (0..n) + .map(|i| BlsSecretKey::::generate(&test_ikm(i)).unwrap().public_key()) + .collect(); + let pk_refs: Vec<&BlsPublicKey> = pks.iter().collect(); + let plaintexts = vec![[0x42u8; 32]; n]; + let pt_refs: Vec<&[u8]> = plaintexts.iter().map(|p| p.as_slice()).collect(); + let mut rng = UnwrapErr(SysRng); + + bencher + .counter(ItemsCount::new(n)) + .bench_local(|| BlsPublicKey::::ies_encrypt_multi(&pk_refs, &pt_refs, &mut rng)); +} + /// IETF-only BLS operations. mod ietf { use super::*; diff --git a/pkgs/pkc/corpus/bls_dh.json5 b/pkgs/pkc/corpus/bls_dh.json5 index 142fb347..c67ccd19 100644 --- a/pkgs/pkc/corpus/bls_dh.json5 +++ b/pkgs/pkc/corpus/bls_dh.json5 @@ -12,6 +12,37 @@ "shared": "93033f1b14f964edc629de1508d4d5709bacefc6a9856cdc47aee4bb76ae825225442857cc158d8b3c49e898bb55d600" } ], + "ies": { + "eph_sk": "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", + "eph_pk": "0193db75369ee1f1b3d8828adbc05c0dc6bd38ef5dab2528174eace2140da80cd8f253ce43748a53b316fdc263234af4", + "iv_seed": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "recipients": [ + { + "sk": "0101010101010101010101010101010101010101010101010101010101010101", + "pk": "8a1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1", + "shared": "84dbc64593c984618257694cad51ea02a3c6aafcfd60f6133c1908c263d562ce4e54390f4b2665635c7cd6689d8a7dd4", + "iv": "000102030405060708090a0b0c0d0e0f", + "plaintext": "a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0", + "ciphertext": "1a242c09c5cfb0133dee47b07c911bfb2191e15927afea241b9420dec130d6de", + }, + { + "sk": "0202020202020202020202020202020202020202020202020202020202020202", + "pk": "0004066a1a5cb9cdf244e45f0a59cf579a78d90ac0bc24663565264601c1c9251c0aa3dfb9835b520e0ba0f211a6696c", + "shared": "8fa4f45683aefd530efa5ee1e7c0ad7163c27db90ce53b115ab6b6ac01e1e71cfdfacf650a4a592ba7993de6c89522a6", + "iv": "2f287b4d3d4910f6cada9e1bd1b46480", + "plaintext": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "ciphertext": "06042f900b0b3b2feb4863f6bf2e3dd905fda77888dbfdcfa23ee01b863b8ac11780aa0e5f66ea87788ad3fa6d0156d2", + }, + { + "sk": "0303030303030303030303030303030303030303030303030303030303030303", + "pk": "8355519968b7db86b1ceb2261e179f6cde1a6010b8588e4a1a59eae804c9eed5f3e3d433a69dabb1eb7403c9c2721116", + "shared": "028b7c71eedeb59c96c5d7d44b60ee9badfdcf782f393f3f7663e4a301f3a28c1c711f7b4c26851d3600af792cfaf837", + "iv": "cefc1232dee44cc53fccf8cc078f657f", + "plaintext": "a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2", + "ciphertext": "518fe9f6001c86bfc36e5a6e06025353f2c5a097bdce288104067e6ef1305777d81aad3f29c3fd5f9565a65096cd711a502f769f6d234e5c31c8b905fddbb624", + } + ] + } }, "ietf": { "dh": [ @@ -26,5 +57,36 @@ "shared": "b3033f1b14f964edc629de1508d4d5709bacefc6a9856cdc47aee4bb76ae825225442857cc158d8b3c49e898bb55d600" } ], + "ies": { + "eph_sk": "2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a", + "eph_pk": "8193db75369ee1f1b3d8828adbc05c0dc6bd38ef5dab2528174eace2140da80cd8f253ce43748a53b316fdc263234af4", + "iv_seed": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "recipients": [ + { + "sk": "0101010101010101010101010101010101010101010101010101010101010101", + "pk": "aa1a1c26055a329817a5759d877a2795f9499b97d6056edde0eea39512f24e8bc874b4471f0501127abb1ea0d9f68ac1", + "shared": "a4dbc64593c984618257694cad51ea02a3c6aafcfd60f6133c1908c263d562ce4e54390f4b2665635c7cd6689d8a7dd4", + "iv": "000102030405060708090a0b0c0d0e0f", + "plaintext": "a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0", + "ciphertext": "f9eaf2e1f1d86f2d4477ce89ad038524fc7067fa828209cf09113ac48fa232e4", + }, + { + "sk": "0202020202020202020202020202020202020202020202020202020202020202", + "pk": "8004066a1a5cb9cdf244e45f0a59cf579a78d90ac0bc24663565264601c1c9251c0aa3dfb9835b520e0ba0f211a6696c", + "shared": "afa4f45683aefd530efa5ee1e7c0ad7163c27db90ce53b115ab6b6ac01e1e71cfdfacf650a4a592ba7993de6c89522a6", + "iv": "2f287b4d3d4910f6cada9e1bd1b46480", + "plaintext": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "ciphertext": "7dfda8d00665983a943d9a1485206d93dbf08d3b46ad2c560e92d338d3d7e8f52d369c1a0ed32a00d2689197f61e7d96", + }, + { + "sk": "0303030303030303030303030303030303030303030303030303030303030303", + "pk": "a355519968b7db86b1ceb2261e179f6cde1a6010b8588e4a1a59eae804c9eed5f3e3d433a69dabb1eb7403c9c2721116", + "shared": "828b7c71eedeb59c96c5d7d44b60ee9badfdcf782f393f3f7663e4a301f3a28c1c711f7b4c26851d3600af792cfaf837", + "iv": "cefc1232dee44cc53fccf8cc078f657f", + "plaintext": "a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2", + "ciphertext": "4d9bc3cf27cfa3a1da757379231685c5a9368a141c7cb990c8ea5f0afbc7c6882a394581116e63e74c4be50051b642bd4152dd5f6ccbda8883e86e41dead5f24", + } + ] + } } } diff --git a/pkgs/pkc/src/bls/error.rs b/pkgs/pkc/src/bls/error.rs index 559b9d7b..cc1a6c81 100644 --- a/pkgs/pkc/src/bls/error.rs +++ b/pkgs/pkc/src/bls/error.rs @@ -19,6 +19,10 @@ pub enum BlsError { DuplicateShareId, /// no items provided for aggregation EmptyAggregation, + /// recipient index past the end of the message + IndexOutOfRange, + /// recipient index above the supported maximum + IndexTooLarge, /// not enough shares to recover InsufficientShares, /// ciphertext is not a whole number of cipher blocks @@ -52,6 +56,8 @@ impl fmt::Display for BlsError { Self::DuplicateMessage => write!(f, "repeated message in a distinct-message aggregate"), Self::DuplicateShareId => write!(f, "duplicate share id in recovery set"), Self::EmptyAggregation => write!(f, "no items provided for aggregation"), + Self::IndexOutOfRange => write!(f, "recipient index past the end of the message"), + Self::IndexTooLarge => write!(f, "recipient index above the supported maximum"), Self::InsufficientShares => write!(f, "not enough shares to recover"), Self::InvalidCiphertextLength => write!(f, "ciphertext is not a whole number of cipher blocks"), Self::InvalidIvSeed => write!(f, "initialisation vector seed is all zeroes"), diff --git a/pkgs/pkc/src/bls/ies_ops.rs b/pkgs/pkc/src/bls/ies_ops.rs new file mode 100644 index 00000000..c86fcd2d --- /dev/null +++ b/pkgs/pkc/src/bls/ies_ops.rs @@ -0,0 +1,900 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! BLS integrated encryption scheme. + +use super::error::BlsError; +use super::ies_bytes::{BlsIesBlobBytes, BlsIesMultiBytes}; +use super::ies_bytes::{IV_SEED_LEN, MAX_IES_RECIPIENTS}; +use super::public_ops::BlsPublicKey; +use super::scheme_ops::BlsScheme; +use super::secret_ops::BlsSecretKey; +use super::BlsPkBytes; +use crate::aes_cbc::AES_BLOCK_LEN; +use crate::prelude::*; + +#[cfg(feature = "codec")] +use dash_num::Hash256; +#[cfg(feature = "codec")] +use dash_types::type_id::TypeId; +#[cfg(feature = "codec")] +use dash_types::{dlgt_codec, MAX_SER_SIZE}; +use dash_types::{qtypestr, type_cvrt}; +use hex_conservative::DisplayHex; +use rand_core::CryptoRng; +use sha2::{Digest, Sha256}; +use zeroize::Zeroizing; + +use core::any::type_name; +use core::fmt::{Debug, Formatter, Result as FmtResult}; +use core::hash::{Hash, Hasher}; + +/// Computes `SHA256(SHA256(input))`. +fn sha256d(input: &[u8]) -> [u8; IV_SEED_LEN] { + let first = Sha256::digest(input); + Sha256::digest(first).into() +} + +/// Advances the IV chain to a recipient index. The seed is hashed once per +/// index, so index 0 is the seed itself. +fn iv_chain(iv_seed: &[u8; IV_SEED_LEN], index: usize) -> [u8; IV_SEED_LEN] { + let mut chain = *iv_seed; + for _ in 0..index { + chain = sha256d(&chain); + } + chain +} + +/// The leading block of a chain value, which is all the cipher takes. +fn iv_of(chain: &[u8; IV_SEED_LEN]) -> [u8; AES_BLOCK_LEN] { + let mut iv = [0u8; AES_BLOCK_LEN]; + iv.copy_from_slice(&chain[..AES_BLOCK_LEN]); + iv +} + +/// The initialisation vector recipient `index` decrypts under. +/// +/// # Errors +/// +/// Returns `IndexTooLarge` above [`MAX_IES_RECIPIENTS`]. +fn iv_at_index(iv_seed: &[u8; IV_SEED_LEN], index: usize) -> Result<[u8; AES_BLOCK_LEN], BlsError> { + if index > MAX_IES_RECIPIENTS { + return Err(BlsError::IndexTooLarge); + } + Ok(iv_of(&iv_chain(iv_seed, index))) +} + +/// Draws a fresh ephemeral key and IV seed. +fn ephemeral(rng: &mut impl CryptoRng) -> Result<(BlsSecretKey, [u8; IV_SEED_LEN]), BlsError> { + let mut ikm = Zeroizing::new([0u8; 32]); + rng.fill_bytes(ikm.as_mut()); + let eph_sk = BlsSecretKey::::generate(ikm.as_ref())?; + + let mut iv_seed = [0u8; IV_SEED_LEN]; + rng.fill_bytes(&mut iv_seed); + Ok((eph_sk, iv_seed)) +} + +/// A BLS-IES encrypted blob under an ephemeral key. +#[cfg_attr( + feature = "serde", + derive(::serde::Serialize, ::serde::Deserialize), + serde(into = "BlsIesBlobBytes", try_from = "BlsIesBlobBytes"), + serde(bound(serialize = "", deserialize = "")) +)] +#[cfg_attr(feature = "codec", derive(TypeId))] +pub struct BlsIesBlob { + ephemeral_pk: BlsPublicKey, + iv_seed: [u8; IV_SEED_LEN], + data: Vec, +} + +#[cfg(feature = "codec")] +dlgt_codec!(for[S: BlsScheme] BlsIesBlob => BlsIesBlobBytes, Hash256, BlsError, MAX_SER_SIZE); + +impl BlsIesBlob { + /// Constructs from an ephemeral key, an IV seed and a ciphertext. + pub fn new(ephemeral_pk: BlsPublicKey, iv_seed: [u8; IV_SEED_LEN], data: Vec) -> Self { + Self { + ephemeral_pk, + iv_seed, + data, + } + } + + /// Borrows the ciphertext. + pub fn data(&self) -> &[u8] { + &self.data + } + + /// The ephemeral public key the sender encrypted under. + pub fn ephemeral_pk(&self) -> &BlsPublicKey { + &self.ephemeral_pk + } + + /// The seed the recipient's IV is derived from. + pub fn iv_seed(&self) -> &[u8; IV_SEED_LEN] { + &self.iv_seed + } + + /// Re-encode the ephemeral key under another scheme. + /// + /// Only the ephemeral key's encoding moves; the ciphertext and the key it + /// was written under are untouched. The tag records how the sender wrote + /// that key, which need not match the scheme the KDF read. + /// + /// # Errors + /// + /// Returns `InvalidPublicKey` when the target scheme refuses the ephemeral + /// key. + pub fn to_scheme(&self) -> Result, BlsError> { + Ok(BlsIesBlob::new( + self.ephemeral_pk.to_scheme::()?, + self.iv_seed, + self.data.clone(), + )) + } +} + +impl Clone for BlsIesBlob { + fn clone(&self) -> Self { + Self { + ephemeral_pk: self.ephemeral_pk.clone(), + iv_seed: self.iv_seed, + data: self.data.clone(), + } + } +} + +impl Debug for BlsIesBlob { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + qtypestr(f, type_name::())?; + write!(f, "(iv_seed={}, data_len={})", self.iv_seed.as_hex(), self.data.len()) + } +} + +impl Eq for BlsIesBlob {} + +impl Hash for BlsIesBlob { + fn hash(&self, state: &mut H) { + self.ephemeral_pk.hash(state); + self.iv_seed.hash(state); + self.data.hash(state); + } +} + +impl PartialEq for BlsIesBlob { + fn eq(&self, other: &Self) -> bool { + self.ephemeral_pk == other.ephemeral_pk && self.iv_seed == other.iv_seed && self.data == other.data + } +} + +type_cvrt!(for[S: BlsScheme] From> for BlsIesBlobBytes, |blob| { + Self::new(BlsPkBytes::from(&blob.ephemeral_pk), blob.iv_seed, blob.data.clone()) +}); + +type_cvrt!(for[S: BlsScheme] TryFrom> for BlsIesBlob, BlsError, |bytes| { + Ok(Self::new( + BlsPublicKey::from_bytes(bytes.ephemeral_pk().as_bytes())?, + *bytes.iv_seed(), + bytes.data().to_vec(), + )) +}); + +/// One ciphertext per recipient under an ephemeral key. +#[cfg_attr( + feature = "serde", + derive(::serde::Serialize, ::serde::Deserialize), + serde(into = "BlsIesMultiBytes", try_from = "BlsIesMultiBytes"), + serde(bound(serialize = "", deserialize = "")) +)] +#[cfg_attr(feature = "codec", derive(TypeId))] +pub struct BlsIesMulti { + ephemeral_pk: BlsPublicKey, + iv_seed: [u8; IV_SEED_LEN], + blobs: Vec>, +} + +#[cfg(feature = "codec")] +dlgt_codec!(for[S: BlsScheme] BlsIesMulti => BlsIesMultiBytes, Hash256, BlsError, MAX_SER_SIZE); + +impl BlsIesMulti { + /// Constructs from an ephemeral key, an IV seed and one ciphertext per + /// recipient. + /// + /// # Errors + /// + /// Returns `IndexTooLarge` when there are more recipients than + /// [`MAX_IES_RECIPIENTS`]. + pub fn new(ephemeral_pk: BlsPublicKey, iv_seed: [u8; IV_SEED_LEN], blobs: Vec>) -> Result { + if blobs.len() > MAX_IES_RECIPIENTS { + return Err(BlsError::IndexTooLarge); + } + + Ok(Self { + ephemeral_pk, + iv_seed, + blobs, + }) + } + + /// Borrows the per-recipient ciphertexts. + pub fn blobs(&self) -> &[Vec] { + &self.blobs + } + + /// The ephemeral public key the sender encrypted under. + pub fn ephemeral_pk(&self) -> &BlsPublicKey { + &self.ephemeral_pk + } + + /// The seed every recipient's IV is derived from. + pub fn iv_seed(&self) -> &[u8; IV_SEED_LEN] { + &self.iv_seed + } + + /// Lift recipient `index`'s ciphertext out as a standalone blob. + /// + /// The seed travels with the blob, so decryption still takes the original + /// recipient index. + pub fn to_blob(&self, index: usize) -> Option> { + Some(BlsIesBlob::new( + self.ephemeral_pk.clone(), + self.iv_seed, + self.blobs.get(index)?.clone(), + )) + } + + /// Re-encode the ephemeral key under another scheme. + /// + /// # Errors + /// + /// Returns `InvalidPublicKey` when the target scheme refuses the ephemeral + /// key. + pub fn to_scheme(&self) -> Result, BlsError> { + BlsIesMulti::new(self.ephemeral_pk.to_scheme::()?, self.iv_seed, self.blobs.clone()) + } +} + +impl Clone for BlsIesMulti { + fn clone(&self) -> Self { + Self { + ephemeral_pk: self.ephemeral_pk.clone(), + iv_seed: self.iv_seed, + blobs: self.blobs.clone(), + } + } +} + +impl Debug for BlsIesMulti { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + qtypestr(f, type_name::())?; + write!( + f, + "(iv_seed={}, blob_count={})", + self.iv_seed.as_hex(), + self.blobs.len() + ) + } +} + +impl Eq for BlsIesMulti {} + +impl Hash for BlsIesMulti { + fn hash(&self, state: &mut H) { + self.ephemeral_pk.hash(state); + self.iv_seed.hash(state); + self.blobs.hash(state); + } +} + +impl PartialEq for BlsIesMulti { + fn eq(&self, other: &Self) -> bool { + self.ephemeral_pk == other.ephemeral_pk && self.iv_seed == other.iv_seed && self.blobs == other.blobs + } +} + +type_cvrt!(for[S: BlsScheme] From> for BlsIesMultiBytes, |multi| { + Self::new(BlsPkBytes::from(&multi.ephemeral_pk), multi.iv_seed, multi.blobs.clone()) +}); + +type_cvrt!(for[S: BlsScheme] TryFrom> for BlsIesMulti, BlsError, |bytes| { + Self::new( + BlsPublicKey::from_bytes(bytes.ephemeral_pk().as_bytes())?, + *bytes.iv_seed(), + bytes.blobs().to_vec(), + ) +}); + +impl BlsPublicKey { + /// Encrypt one blob for this recipient. + /// + /// The blob decrypts at recipient index 0. The scheme reaches the ciphertext + /// through the symmetric key, so a blob is readable only by the same scheme + /// that wrote it. + /// + /// # Errors + /// + /// Returns `InvalidPlaintextLength` when the plaintext is not a whole + /// number of 16-byte blocks, or `InvalidPublicKey` when the shared secret + /// cannot be derived. + pub fn ies_encrypt(&self, plaintext: &[u8], rng: &mut impl CryptoRng) -> Result, BlsError> { + let (eph_sk, iv_seed) = ephemeral(rng)?; + self.ies_encrypt_with(&eph_sk, &iv_seed, plaintext) + } + + /// Encrypt one plaintext per recipient under a single ephemeral key. + /// + /// Recipient `i`'s blob takes the IV at index `i` of the seed's chain. The + /// plaintexts differ per recipient because in a DKG each member receives + /// its own secret share. + /// + /// # Errors + /// + /// Returns `CountMismatch` on differing plaintext and recipient counts, + /// `IndexTooLarge` above [`MAX_IES_RECIPIENTS`] recipients, + /// `InvalidPlaintextLength` on a misaligned plaintext, and + /// `InvalidPublicKey` when a shared secret cannot be derived. + pub fn ies_encrypt_multi( + recipients: &[&Self], + plaintexts: &[&[u8]], + rng: &mut impl CryptoRng, + ) -> Result, BlsError> { + let (eph_sk, iv_seed) = ephemeral(rng)?; + Self::ies_encrypt_multi_with(&eph_sk, &iv_seed, recipients, plaintexts) + } + + /// [`ies_encrypt`](Self::ies_encrypt) over a caller-chosen ephemeral key. + pub(crate) fn ies_encrypt_with( + &self, + eph_sk: &BlsSecretKey, + iv_seed: &[u8; IV_SEED_LEN], + plaintext: &[u8], + ) -> Result, BlsError> { + let ciphertext = S::ies_seal(&eph_sk.0, &self.0, &iv_of(iv_seed), plaintext)?; + Ok(BlsIesBlob::new(eph_sk.public_key(), *iv_seed, ciphertext)) + } + + /// [`ies_encrypt_multi`](Self::ies_encrypt_multi) over a caller-chosen + /// ephemeral key. + pub(crate) fn ies_encrypt_multi_with( + eph_sk: &BlsSecretKey, + iv_seed: &[u8; IV_SEED_LEN], + recipients: &[&Self], + plaintexts: &[&[u8]], + ) -> Result, BlsError> { + if plaintexts.len() != recipients.len() { + return Err(BlsError::CountMismatch); + } + if recipients.len() > MAX_IES_RECIPIENTS { + return Err(BlsError::IndexTooLarge); + } + + let mut chain = *iv_seed; + let mut blobs = Vec::with_capacity(recipients.len()); + for (recipient, plaintext) in recipients.iter().zip(plaintexts) { + blobs.push(S::ies_seal(&eph_sk.0, &recipient.0, &iv_of(&chain), plaintext)?); + chain = sha256d(&chain); + } + + BlsIesMulti::new(eph_sk.public_key(), *iv_seed, blobs) + } +} + +impl BlsSecretKey { + /// Decrypt one BLS-IES blob. + /// + /// `index` selects the IV in the seed's chain: 0 for a standalone blob, or + /// the original recipient index for one lifted out of a multi-recipient + /// message. + /// + /// # Errors + /// + /// Returns `IndexTooLarge` above [`MAX_IES_RECIPIENTS`], + /// `InvalidCiphertextLength` on a misaligned ciphertext, or + /// `InvalidPublicKey` when the shared secret cannot be derived. + pub fn ies_decrypt(&self, blob: &BlsIesBlob, index: usize) -> Result>, BlsError> { + S::ies_open( + &self.0, + &blob.ephemeral_pk().0, + &iv_at_index(blob.iv_seed(), index)?, + blob.data(), + ) + } + + /// Decrypt one recipient's blob out of a multi-recipient message. + /// + /// # Errors + /// + /// Returns `IndexOutOfRange` when the message holds no blob at `index`, + /// and otherwise as [`ies_decrypt`](Self::ies_decrypt). + pub fn ies_decrypt_multi(&self, multi: &BlsIesMulti, index: usize) -> Result>, BlsError> { + let ciphertext = multi.blobs().get(index).ok_or(BlsError::IndexOutOfRange)?; + + S::ies_open( + &self.0, + &multi.ephemeral_pk().0, + &iv_at_index(multi.iv_seed(), index)?, + ciphertext, + ) + } +} + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::bls::tests::RSEED; + use crate::bls::{BlsScChia, BlsScIetf}; + + use cfg_if::cfg_if; + use dash_dev::{arr_from_hex, vec_from_hex, Corpus}; + use getrandom::SysRng; + use hex_conservative::DisplayHex; + use rand_core::UnwrapErr; + use rstest::rstest; + use serde::Deserialize; + + #[derive(Deserialize)] + struct IesVec { + eph_sk: String, + eph_pk: String, + iv_seed: String, + recipients: Vec, + } + + #[derive(Deserialize)] + struct RecipientVec { + sk: String, + pk: String, + shared: String, + iv: String, + plaintext: String, + ciphertext: String, + } + + struct Kat { + eph_sk: BlsSecretKey, + eph_pk: BlsPublicKey, + iv_seed: [u8; IV_SEED_LEN], + recipients: Vec, + } + + fn load_kat(scheme: &str) -> Kat { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_dh").scope(scheme); + let v: IesVec = corpus.value("ies"); + + Kat { + eph_sk: BlsSecretKey::from_bytes(&arr_from_hex(&v.eph_sk)).unwrap(), + eph_pk: BlsPublicKey::from_bytes(&arr_from_hex(&v.eph_pk)).unwrap(), + iv_seed: arr_from_hex(&v.iv_seed), + recipients: v.recipients, + } + } + + impl Kat { + /// The multi-recipient message the vectors record. + fn message(&self) -> BlsIesMulti { + BlsIesMulti::new( + self.eph_pk.clone(), + self.iv_seed, + self.recipients.iter().map(|r| vec_from_hex(&r.ciphertext)).collect(), + ) + .unwrap() + } + } + + impl RecipientVec { + fn public_key(&self) -> BlsPublicKey { + BlsPublicKey::from_bytes(&arr_from_hex(&self.pk)).unwrap() + } + + fn secret_key(&self) -> BlsSecretKey { + BlsSecretKey::from_bytes(&arr_from_hex(&self.sk)).unwrap() + } + } + + fn make_sk(seed: usize) -> BlsSecretKey { + BlsSecretKey::generate(&RSEED[seed]).unwrap() + } + + /// The shared secret and the IV are the two inputs the ciphertext hangs on, + /// so they are pinned on their own: a mismatch in either would otherwise + /// surface only as an unexplained ciphertext difference. + fn assert_kat_key_material(scheme: &str) { + let kat = load_kat::(scheme); + + for (i, r) in kat.recipients.iter().enumerate() { + let shared = kat.eph_sk.dh_exchange(&r.public_key::()).unwrap(); + assert_eq!(shared.as_bytes().to_lower_hex_string(), r.shared); + assert_eq!(r.secret_key::().dh_exchange(&kat.eph_pk).unwrap(), shared); + assert_eq!(iv_at_index(&kat.iv_seed, i).unwrap().to_lower_hex_string(), r.iv); + } + } + + #[rstest] + #[case::chia(assert_kat_key_material::, "chia")] + #[case::ietf(assert_kat_key_material::, "ietf")] + fn kat_key_material_matches_the_reference(#[case] assertion: fn(&str), #[case] scheme: &str) { + assertion(scheme); + } + + /// The scheme decides how the shared point is written and the key is that + /// encoding truncated, so the arms part company at the flag byte and + /// nowhere else. + #[rstest] + fn kdf_reads_the_scheme_own_encoding() { + let legacy_kat = load_kat::("chia"); + let basic_kat = load_kat::("ietf"); + let legacy_r = &legacy_kat.recipients[0]; + let basic_r = &basic_kat.recipients[0]; + let plaintext = vec_from_hex(&legacy_r.plaintext); + + let legacy = legacy_kat.eph_sk.dh_exchange(&legacy_r.public_key()).unwrap(); + let basic = basic_kat.eph_sk.dh_exchange(&basic_r.public_key()).unwrap(); + + assert_ne!(legacy.as_bytes()[0], basic.as_bytes()[0]); + assert_eq!( + legacy.as_bytes()[1..], + basic.as_bytes()[1..], + "one point, two encodings" + ); + + let blob = legacy_r + .public_key::() + .ies_encrypt_with(&legacy_kat.eph_sk, &legacy_kat.iv_seed, &plaintext) + .unwrap(); + assert_eq!(blob.data(), vec_from_hex(&legacy_r.ciphertext)); + assert_ne!( + vec_from_hex(&legacy_r.ciphertext), + vec_from_hex(&basic_r.ciphertext), + "the arms must not coincide" + ); + } + + fn assert_kat_encrypt(scheme: &str) { + let kat = load_kat::(scheme); + let pks: Vec> = kat.recipients.iter().map(RecipientVec::public_key::).collect(); + let pk_refs: Vec<&BlsPublicKey> = pks.iter().collect(); + let plaintexts: Vec> = kat.recipients.iter().map(|r| vec_from_hex(&r.plaintext)).collect(); + let pt_refs: Vec<&[u8]> = plaintexts.iter().map(Vec::as_slice).collect(); + + let multi = BlsPublicKey::ies_encrypt_multi_with(&kat.eph_sk, &kat.iv_seed, &pk_refs, &pt_refs).unwrap(); + assert_eq!(multi, kat.message()); + + // The single-recipient path is the same construction at index 0. + let blob = pks[0] + .ies_encrypt_with(&kat.eph_sk, &kat.iv_seed, &plaintexts[0]) + .unwrap(); + assert_eq!(blob, multi.to_blob(0).unwrap()); + } + + #[rstest] + #[case::chia(assert_kat_encrypt::, "chia")] + #[case::ietf(assert_kat_encrypt::, "ietf")] + fn kat_encrypt_matches_the_reference(#[case] assertion: fn(&str), #[case] scheme: &str) { + assertion(scheme); + } + + fn assert_kat_decrypt(scheme: &str) { + let kat = load_kat::(scheme); + let multi = kat.message(); + + for (i, r) in kat.recipients.iter().enumerate() { + let sk = r.secret_key::(); + let plaintext = vec_from_hex(&r.plaintext); + assert_eq!(*sk.ies_decrypt_multi(&multi, i).unwrap(), plaintext); + + // A lifted blob keeps the shared seed, so it decrypts at the index it + // was encrypted at and at no other. + let blob = multi.to_blob(i).unwrap(); + assert_eq!(*sk.ies_decrypt(&blob, i).unwrap(), plaintext); + } + } + + #[rstest] + #[case::chia(assert_kat_decrypt::, "chia")] + #[case::ietf(assert_kat_decrypt::, "ietf")] + fn kat_decrypt_matches_the_reference(#[case] assertion: fn(&str), #[case] scheme: &str) { + assertion(scheme); + } + + fn assert_encrypt_decrypt_roundtrip() { + let sk = make_sk::(0); + let plaintext = [0x42u8; 32]; + let mut rng = UnwrapErr(SysRng); + + let blob = sk.public_key().ies_encrypt(&plaintext, &mut rng).unwrap(); + assert_eq!(*sk.ies_decrypt(&blob, 0).unwrap(), plaintext); + } + + #[rstest] + #[case::chia(assert_encrypt_decrypt_roundtrip::)] + #[case::ietf(assert_encrypt_decrypt_roundtrip::)] + fn encrypt_decrypt_roundtrip(#[case] assertion: fn()) { + assertion(); + } + + fn assert_empty_plaintext_roundtrip() { + let sk = make_sk::(0); + let mut rng = UnwrapErr(SysRng); + + let blob = sk.public_key().ies_encrypt(&[], &mut rng).unwrap(); + assert!(sk.ies_decrypt(&blob, 0).unwrap().is_empty()); + } + + #[rstest] + #[case::chia(assert_empty_plaintext_roundtrip::)] + #[case::ietf(assert_empty_plaintext_roundtrip::)] + fn empty_plaintext_roundtrip(#[case] assertion: fn()) { + assertion(); + } + + fn assert_multi_encrypt_decrypt_roundtrip() { + let sks: Vec> = (0..3).map(make_sk::).collect(); + let pks: Vec> = sks.iter().map(BlsSecretKey::public_key).collect(); + let pk_refs: Vec<&BlsPublicKey> = pks.iter().collect(); + let plaintexts: Vec> = (0..3u8).map(|i| vec![i; 48]).collect(); + let pt_refs: Vec<&[u8]> = plaintexts.iter().map(Vec::as_slice).collect(); + let mut rng = UnwrapErr(SysRng); + + let multi = BlsPublicKey::ies_encrypt_multi(&pk_refs, &pt_refs, &mut rng).unwrap(); + for (i, sk) in sks.iter().enumerate() { + assert_eq!(*sk.ies_decrypt_multi(&multi, i).unwrap(), plaintexts[i]); + assert_eq!(*sk.ies_decrypt(&multi.to_blob(i).unwrap(), i).unwrap(), plaintexts[i]); + } + } + + #[rstest] + #[case::chia(assert_multi_encrypt_decrypt_roundtrip::)] + #[case::ietf(assert_multi_encrypt_decrypt_roundtrip::)] + fn multi_encrypt_decrypt_roundtrip(#[case] assertion: fn()) { + assertion(); + } + + /// Two recipients holding the same key still get distinct ciphertexts, + /// because the IV advances per index rather than per key. + #[rstest] + fn multi_ciphertexts_differ_by_iv() { + let sk = make_sk::(0); + let pk = sk.public_key(); + let plaintext = [0x77u8; 16]; + let mut rng = UnwrapErr(SysRng); + + let multi = BlsPublicKey::ies_encrypt_multi(&[&pk, &pk], &[&plaintext, &plaintext], &mut rng).unwrap(); + assert_ne!(multi.blobs()[0], multi.blobs()[1]); + } + + #[rstest] + fn decrypting_with_the_wrong_key_yields_junk() { + let sk = make_sk::(0); + let plaintext = [0xddu8; 32]; + let mut rng = UnwrapErr(SysRng); + + let blob = sk.public_key().ies_encrypt(&plaintext, &mut rng).unwrap(); + assert_ne!(*make_sk::(1).ies_decrypt(&blob, 0).unwrap(), plaintext); + } + + /// Decrypting at the wrong index picks the wrong IV, which corrupts the + /// first block and leaves the rest intact, so the check has to be on the + /// whole plaintext. + #[rstest] + fn decrypting_at_the_wrong_index_yields_junk() { + let kat = load_kat::("ietf"); + let multi = kat.message(); + let sk = kat.recipients[1].secret_key::(); + + assert_ne!( + *sk.ies_decrypt(&multi.to_blob(1).unwrap(), 0).unwrap(), + vec_from_hex(&kat.recipients[1].plaintext) + ); + } + + #[rstest] + #[case::single(1)] + #[case::block_and_a_half(24)] + fn rejects_unaligned_plaintext(#[case] len: usize) { + let sk = make_sk::(0); + let pk = sk.public_key(); + let plaintext = vec![0xffu8; len]; + let mut rng = UnwrapErr(SysRng); + + assert_eq!( + pk.ies_encrypt(&plaintext, &mut rng).unwrap_err(), + BlsError::InvalidPlaintextLength + ); + assert_eq!( + BlsPublicKey::ies_encrypt_multi(&[&pk], &[plaintext.as_slice()], &mut rng).unwrap_err(), + BlsError::InvalidPlaintextLength + ); + } + + #[rstest] + fn a_misaligned_ciphertext_survives_construction_and_decode() { + let kat = load_kat::("ietf"); + let ragged = vec![0u8; 17]; + + let blob = BlsIesBlob::new(kat.eph_pk.clone(), kat.iv_seed, ragged.clone()); + assert_eq!(blob.data(), ragged); + assert!(BlsIesMulti::new(kat.eph_pk.clone(), kat.iv_seed, vec![ragged.clone()]).is_ok()); + + let bag = BlsIesBlobBytes::new(BlsPkBytes::from(&kat.eph_pk), kat.iv_seed, ragged.clone()); + assert_eq!(BlsIesBlob::::try_from(&bag).unwrap().data(), ragged); + assert!(BlsIesBlob::::decode(&mut bag.to_bytes().as_slice()).is_ok()); + + assert_eq!( + kat.recipients[0] + .secret_key::() + .ies_decrypt(&blob, 0) + .unwrap_err(), + BlsError::InvalidCiphertextLength + ); + } + + /// A bound this crate adds, so it must sit far above any real quorum. + #[rstest] + fn bounds_the_iv_walk_above_any_real_quorum() { + let kat = load_kat::("ietf"); + let sk = kat.recipients[0].secret_key::(); + let blob = kat.message().to_blob(0).unwrap(); + + assert!(sk.ies_decrypt(&blob, MAX_IES_RECIPIENTS).is_ok()); + assert_eq!( + sk.ies_decrypt(&blob, MAX_IES_RECIPIENTS + 1).unwrap_err(), + BlsError::IndexTooLarge + ); + } + + /// The chain advances once per index, so index 0 is the seed itself and + /// every step after it is one double SHA256 further along. + #[rstest] + fn iv_chain_advances_once_per_index() { + let seed = [0xcd; IV_SEED_LEN]; + + assert_eq!(iv_chain(&seed, 0), seed); + assert_eq!(iv_chain(&seed, 1), sha256d(&seed)); + assert_eq!(iv_chain(&seed, 2), sha256d(&iv_chain(&seed, 1))); + } + + /// The cipher sees the leading block of the chain value and no more. + #[rstest] + fn iv_is_the_leading_block_of_the_chain() { + let seed = [0xcd; IV_SEED_LEN]; + + for index in 0..3 { + assert_eq!( + iv_at_index(&seed, index).unwrap(), + iv_chain(&seed, index)[..AES_BLOCK_LEN] + ); + } + } + + /// Carrying the chain forward per recipient must land on the same IVs as + /// walking to each index from the seed, or the two paths would disagree. + #[rstest] + fn advancing_the_chain_matches_indexed_lookup() { + let seed = [0xcd; IV_SEED_LEN]; + let mut chain = seed; + + for index in 0..5 { + assert_eq!(iv_of(&chain), iv_at_index(&seed, index).unwrap()); + chain = sha256d(&chain); + } + } + + #[rstest] + fn rejects_mismatched_recipient_count() { + let pk = make_sk::(0).public_key(); + let plaintext = [0u8; 16]; + let mut rng = UnwrapErr(SysRng); + + assert_eq!( + BlsPublicKey::ies_encrypt_multi(&[&pk, &pk], &[&plaintext[..]], &mut rng).unwrap_err(), + BlsError::CountMismatch + ); + } + + #[rstest] + fn rejects_index_past_the_last_recipient() { + let sk = make_sk::(0); + let plaintext = [0u8; 16]; + let mut rng = UnwrapErr(SysRng); + + let multi = BlsPublicKey::ies_encrypt_multi(&[&sk.public_key()], &[&plaintext[..]], &mut rng).unwrap(); + assert_eq!(sk.ies_decrypt_multi(&multi, 1).unwrap_err(), BlsError::IndexOutOfRange); + assert!(multi.to_blob(1).is_none()); + } + + cfg_if! { + if #[cfg(feature = "codec")] { + use dash_types::codec::BaseCodec; + + /// The wire image belongs to the bag, so the operational type reaches it by + /// conversion and comes back out of the bytes it wrote. + fn assert_codec_delegates_to_the_bag(scheme: &str) { + let multi = load_kat::(scheme).message(); + let blob = multi.to_blob(0).unwrap(); + + let mut encoded = Vec::new(); + blob.encode(&mut encoded); + assert_eq!(encoded, BlsIesBlobBytes::from(&blob).to_bytes()); + assert_eq!(BlsIesBlob::::decode(&mut encoded.as_slice()).unwrap(), blob); + + let mut encoded = Vec::new(); + multi.encode(&mut encoded); + assert_eq!(encoded, BlsIesMultiBytes::from(&multi).to_bytes()); + assert_eq!(BlsIesMulti::::decode(&mut encoded.as_slice()).unwrap(), multi); + } + + #[rstest] + #[case::chia(assert_codec_delegates_to_the_bag::, "chia")] + #[case::ietf(assert_codec_delegates_to_the_bag::, "ietf")] + fn codec_delegates_to_the_bag(#[case] assertion: fn(&str), #[case] scheme: &str) { + assertion(scheme); + } + + /// A bag carrying an ephemeral key no point decodes to has no operational + /// counterpart, so the conversion is where that is caught. + #[rstest] + fn decoding_rejects_an_invalid_ephemeral_key() { + let bag = + BlsIesBlobBytes::::new(BlsPkBytes::from_bytes([0xab; 48]), [0xcd; IV_SEED_LEN], vec![0x11; 16]); + + assert_eq!( + BlsIesBlob::::try_from(&bag).unwrap_err(), + BlsError::InvalidPublicKey + ); + assert!(BlsIesBlob::::decode(&mut bag.to_bytes().as_slice()).is_err()); + } + + /// The bag's tag and the cipher's scheme are separate. + /// + /// A sender may write the ephemeral key in the legacy encoding while + /// deriving the symmetric key from the basic one, so its message is + /// carried across by converting it and read by the basic arm. + #[rstest] + fn legacy_tagged_blob_converts_and_decrypts() { + let kat = load_kat::("ietf"); + let multi = kat.message(); + + let legacy = multi.to_scheme::().unwrap(); + assert_ne!( + BlsIesMultiBytes::from(&legacy).to_bytes(), + BlsIesMultiBytes::from(&multi).to_bytes(), + "the encoding moved" + ); + assert_eq!(legacy.to_scheme::().unwrap(), multi); + + // The recipient's own key is typed to the scheme it was read under, and + // reaches the cipher the same way. + let sk = kat.recipients[0] + .secret_key::() + .to_scheme::() + .unwrap(); + let recovered = sk + .to_scheme::() + .unwrap() + .ies_decrypt_multi(&legacy.to_scheme::().unwrap(), 0) + .unwrap(); + + assert_eq!(*recovered, vec_from_hex(&kat.recipients[0].plaintext)); + } + + cfg_if! { + if #[cfg(feature = "serde")] { + use dash_dev::assert_json_rt; + + #[rstest] + fn serde_roundtrips() { + let multi = load_kat::("ietf").message(); + assert_json_rt(&multi.to_blob(0).unwrap()); + assert_json_rt(&multi); + } + } + } + } + } +} diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index f1eec48b..15331814 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -29,6 +29,7 @@ pub use sig_id::BlsSigId; cfg_if::cfg_if! { if #[cfg(feature = "bls")] { mod curve_consts; + mod ies_ops; mod macros; mod public_ops; mod scheme_chia; @@ -51,6 +52,7 @@ cfg_if::cfg_if! { #[expect(clippy::unwrap_used, reason = "test support code")] pub mod tests; + pub use ies_ops::{BlsIesBlob, BlsIesMulti}; pub use public_ops::BlsPublicKey; pub use group::{BlsPointRepr, G1Affine, G2Affine, G1, G2}; pub use scalar::Fr;