diff --git a/Cargo.lock b/Cargo.lock index 69cc034c..c169462b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -526,6 +526,7 @@ dependencies = [ "hex-conservative 0.3.2", "rstest", "serde", + "subtle", "zeroize", ] diff --git a/contrib/codeql/zeroize.ql b/contrib/codeql/zeroize.ql index e086e629..cea1256d 100644 --- a/contrib/codeql/zeroize.ql +++ b/contrib/codeql/zeroize.ql @@ -143,7 +143,7 @@ predicate zeroizeSatisfied(TypeItem t) { /** * Holds if `t` reaches the wire through the wiping encoder pair. * - * `impl_stype!`/`impl_sbyte!` emit `type Encoder = ArrEncoder`; the plain + * `impl_stype!`/`impl_sbytes!` emit `type Encoder = ArrEncoder`; the plain * `impl_type!`/`impl_bytes!` emit `type Encoder = VecEncoder`. */ predicate usesSecretBridge(TypeItem t) { diff --git a/contrib/semgrep/types.yml b/contrib/semgrep/types.yml index 35c01619..de12fe8d 100644 --- a/contrib/semgrep/types.yml +++ b/contrib/semgrep/types.yml @@ -5,5 +5,31 @@ rules: languages: [rust] paths: include: [/pkgs/types/src/**/*.rs] - exclude: [/pkgs/types/src/hex.rs, /pkgs/types/src/uint.rs] - pattern-regex: '\b(?:make|impl)_(?:bytes|num|type)!\s*\{' + exclude: + - /pkgs/types/src/entity.rs + - /pkgs/types/src/macros.rs + - /pkgs/types/src/secret.rs + - /pkgs/types/src/uint.rs + pattern-regex: '\b(?:make|impl)_(?:bytes|num|type)!\s*[({]' + + - id: types-macro-generics-bracketed + message: "forward macro generics bare: `@parse [$($g)*]`, not `@parse [<$($g)*>]`" + severity: ERROR + languages: [rust] + paths: + include: [/pkgs/types/src/**/*.rs] + pattern-regex: |- + (?x) + @ (?: parse | delegate | codec ) # an internal forwarding arm + \s* \[ \s* < # ... whose payload opens with < + + - id: types-macro-impl-unbracketed + message: "bracket generics in a macro impl template: `impl<$($g)*>`, not `impl $($g)*`" + severity: ERROR + languages: [rust] + paths: + include: [/pkgs/types/src/**/*.rs] + pattern-regex: |- + (?xm) + ^ [ \t]* impl [ \t]+ # `impl` then a space rather than < + \$ \( # ... then a macro repetition: the generics diff --git a/contrib/semgrep/workspace.yml b/contrib/semgrep/workspace.yml index 86264ef3..fbfadaa1 100644 --- a/contrib/semgrep/workspace.yml +++ b/contrib/semgrep/workspace.yml @@ -100,6 +100,23 @@ rules: include: [/pkgs/**/*.rs, /contrib/samples/**/*.rs] pattern-regex: '\bimpl\b[^{]*\bInto\s*<.*>\s+for\b' + - id: macro-export-no-serde-cfg + message: "use `$crate::cfg_serde!` and `$crate::__private::serde` to root dependency and cfg flag to defining macro." + severity: ERROR + languages: [rust] + paths: + include: + - /pkgs/**/*.rs + exclude: + - /pkgs/types/marker/** + pattern-regex: |- + (?sxm) + ^([ \t]*) \#\[macro_export\] + (?: \n\1 \#\[[^\]]*\] )* + \n\1 macro_rules!\s*\w+\s*\{ + (?: (?!^\1\}) . )*? + (?: feature \s*=\s* "serde" | (? rooted foreign-crate paths (::crate::) in macros must route @@ -116,6 +133,21 @@ rules: - pattern-not-regex: "::(?:core|alloc|std|serde)::" - pattern-not-regex: '\$crate::__private::' + - id: macro-no-bare-std-path + message: "root std-prefix paths (::core::, ::alloc::, ::std::) in macros" + severity: ERROR + languages: [rust] + paths: + include: + - /pkgs/**/*.rs + exclude: + - /pkgs/types/marker/** + pattern-regex: |- + (?sxm) + ^([ \t]*) macro_rules!\s*\w+\s*\{ + (?: (?!^\1\}) . )*? + (? core::cmp::Ordering { + fn cmp(&self, other: &Self) -> ::core::cmp::Ordering { // Lexicographic on raw bytes (consensus ordering). self.0.cmp(&other.0) } } impl PartialOrd for $name { - fn partial_cmp(&self, other: &Self) -> Option { + fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> { Some(self.cmp(other)) } } diff --git a/pkgs/num/src/lib.rs b/pkgs/num/src/lib.rs index aff68d06..07ba1052 100644 --- a/pkgs/num/src/lib.rs +++ b/pkgs/num/src/lib.rs @@ -29,6 +29,8 @@ pub mod util; pub mod __private { pub use bitcoin_consensus_encoding; pub use dash_types; + #[cfg(feature = "serde")] + pub use serde; } pub use arith::ArithInt; diff --git a/pkgs/num/src/util.rs b/pkgs/num/src/util.rs index 0f0d4c48..1fbbe065 100644 --- a/pkgs/num/src/util.rs +++ b/pkgs/num/src/util.rs @@ -6,6 +6,22 @@ //! Hash newtype macros. +/// dash-num's [`cfg_serde!`](dash_types::cfg_serde), keyed to `dash-num/serde` +/// (this crate) rather than `dash-types/serde`. +#[cfg(feature = "serde")] +#[doc(hidden)] +#[macro_export] +macro_rules! cfg_serde { + ($($item:tt)*) => { $($item)* }; +} + +#[cfg(not(feature = "serde"))] +#[doc(hidden)] +#[macro_export] +macro_rules! cfg_serde { + ($($item:tt)*) => {}; +} + /// Generates `BaseCodec` + `Encodable` + `Decodable` for hash newtypes. #[macro_export] macro_rules! impl_hash { @@ -38,10 +54,26 @@ macro_rules! make_hash { ) => { $(#[$attr])* #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, $crate::__private::dash_types::TypeId)] - #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] - #[cfg_attr(feature = "serde", serde(transparent))] pub struct $name($base); + $crate::cfg_serde! { + impl $crate::__private::serde::Serialize for $name { + fn serialize( + &self, serializer: S, + ) -> Result { + $crate::__private::serde::Serialize::serialize(&self.0, serializer) + } + } + + impl<'de> $crate::__private::serde::Deserialize<'de> for $name { + fn deserialize>( + deserializer: D, + ) -> Result { + <$base as $crate::__private::serde::Deserialize>::deserialize(deserializer).map(Self) + } + } + } + impl $name { /// The all-zeros (null) hash. pub const ZERO: Self = Self(<$base>::ZERO); @@ -88,19 +120,19 @@ macro_rules! make_hash { fn default() -> Self { Self::ZERO } } - impl core::fmt::Display for $name { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - core::fmt::Display::fmt(&self.0, f) + impl ::core::fmt::Display for $name { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + ::core::fmt::Display::fmt(&self.0, f) } } - impl core::fmt::Debug for $name { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + impl ::core::fmt::Debug for $name { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { write!(f, "{}({})", stringify!($name), self.0) } } - impl core::str::FromStr for $name { + impl ::core::str::FromStr for $name { type Err = $crate::ParseHexError; fn from_str(s: &str) -> Result { diff --git a/pkgs/p2p_core/src/msg/addr.rs b/pkgs/p2p_core/src/msg/addr.rs index d4d714b8..fcbb1ec7 100644 --- a/pkgs/p2p_core/src/msg/addr.rs +++ b/pkgs/p2p_core/src/msg/addr.rs @@ -12,7 +12,7 @@ use crate::primitives::ServiceFlags; use dash_primitives::{hash_impl, AddrV2, ServiceV1}; use dash_types::codec::{self, BaseCodec, DecodeError, EncodeBuf}; -use dash_types::TypeId; +use dash_types::{CompactSize, TypeId}; use core::fmt; @@ -47,7 +47,7 @@ pub struct AddrV2Entry { impl BaseCodec for AddrV2Entry { fn decode(data: &mut &[u8]) -> Result { let time = u32::decode(data)?; - let services = ServiceFlags(codec::read_compact_u64(data)?); + let services = ServiceFlags(CompactSize::decode(data)?.get()); let addr = AddrV2::decode(data)?; let port = codec::read_u16_be(data)?; Ok(Self { @@ -60,7 +60,7 @@ impl BaseCodec for AddrV2Entry { fn encode(&self, buf: &mut impl EncodeBuf) { self.time.encode(buf); - codec::write_compact_u64(self.services.0, buf); + CompactSize::from(self.services.0).encode(buf); self.addr.encode(buf); buf.extend_from_slice(&self.port.to_be_bytes()); } diff --git a/pkgs/p2p_core/src/msg/headers.rs b/pkgs/p2p_core/src/msg/headers.rs index e3519304..9e619ebf 100644 --- a/pkgs/p2p_core/src/msg/headers.rs +++ b/pkgs/p2p_core/src/msg/headers.rs @@ -11,8 +11,8 @@ use crate::prelude::*; use crate::primitives::ProtocolVersion; use dash_primitives::{hash_impl, BlockHash, BlockHeader, MerkleRoot}; -use dash_types::codec::{self, BaseCodec, DecodeError, EncodeBuf}; -use dash_types::TypeId; +use dash_types::codec::{BaseCodec, DecodeError, EncodeBuf}; +use dash_types::{CompactSize, TypeId}; /// Maximum headers per message. const MAX_HEADERS: usize = 2_000; @@ -50,7 +50,7 @@ impl_p2p!(Headers); impl BaseCodec for Headers { fn decode(data: &mut &[u8]) -> Result { - let count = codec::read_compact_size(data, MAX_HEADERS)?; + let count = CompactSize::decode(data)?.into_len(MAX_HEADERS)?; let mut headers = Vec::with_capacity(count); for _ in 0..count { headers.push(BlockHeader { @@ -62,13 +62,13 @@ impl BaseCodec for Headers { nonce: u32::decode(data)?, }); // Consume the trailing tx_count (always 0). - codec::read_compact_size(data, 0)?; + CompactSize::decode(data)?.into_len(0)?; } Ok(Self { headers }) } fn encode(&self, buf: &mut impl EncodeBuf) { - codec::write_compact_size(self.headers.len(), buf); + CompactSize::from(self.headers.len()).encode(buf); for h in &self.headers { h.version.encode(buf); h.prev_hash.encode(buf); diff --git a/pkgs/p2p_core/src/msg/headers2.rs b/pkgs/p2p_core/src/msg/headers2.rs index 14a37b0f..8d805ee1 100644 --- a/pkgs/p2p_core/src/msg/headers2.rs +++ b/pkgs/p2p_core/src/msg/headers2.rs @@ -11,8 +11,8 @@ use crate::prelude::*; use crate::primitives::{CompressionState, ProtocolVersion}; use dash_primitives::{hash_impl, BlockHash}; -use dash_types::codec::{self, BaseCodec, DecodeError, EncodeBuf}; -use dash_types::TypeId; +use dash_types::codec::{BaseCodec, DecodeError, EncodeBuf}; +use dash_types::{CompactSize, TypeId}; /// Maximum headers per message. const MAX_HEADERS: usize = 2_000; @@ -47,7 +47,7 @@ impl_p2p!(Headers2); impl BaseCodec for Headers2 { fn decode(data: &mut &[u8]) -> Result { - let count = codec::read_compact_size(data, MAX_HEADERS)?; + let count = CompactSize::decode(data)?.into_len(MAX_HEADERS)?; let mut state = CompressionState::new(); let mut headers = Vec::with_capacity(count); for _ in 0..count { @@ -57,7 +57,7 @@ impl BaseCodec for Headers2 { } fn encode(&self, buf: &mut impl EncodeBuf) { - codec::write_compact_size(self.headers.len(), buf); + CompactSize::from(self.headers.len()).encode(buf); let mut state = CompressionState::new(); for h in &self.headers { state.encode_header(h, buf); diff --git a/pkgs/p2p_core/src/primitives/command.rs b/pkgs/p2p_core/src/primitives/command.rs index 686be154..f5b2194a 100644 --- a/pkgs/p2p_core/src/primitives/command.rs +++ b/pkgs/p2p_core/src/primitives/command.rs @@ -16,7 +16,7 @@ use core::fmt; #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] pub struct CommandString([u8; 12]); -impl_bytes!(12, CommandString); +impl_bytes!(CommandString, 12); hash_impl!(CommandString); diff --git a/pkgs/p2p_core/src/primitives/user_agent.rs b/pkgs/p2p_core/src/primitives/user_agent.rs index 4ca709fa..dedc0cc3 100644 --- a/pkgs/p2p_core/src/primitives/user_agent.rs +++ b/pkgs/p2p_core/src/primitives/user_agent.rs @@ -11,7 +11,7 @@ use crate::prelude::*; use dash_primitives::hash_impl; use dash_types::codec::{self, BaseCodec, DecodeError, EncodeBuf}; -use dash_types::{TypeId, Unencodable}; +use dash_types::{CompactSize, TypeId, Unencodable}; use core::fmt; @@ -39,7 +39,7 @@ impl fmt::Display for UserAgentTooLong { impl BaseCodec for UserAgent { fn decode(data: &mut &[u8]) -> Result { - let len = codec::read_compact_size(data, MAX_USER_AGENT)?; + let len = CompactSize::decode(data)?.into_len(MAX_USER_AGENT)?; let raw = codec::read_bytes(data, len)?; Ok(Self(raw.to_vec())) } diff --git a/pkgs/pkc/src/bls/public_bytes.rs b/pkgs/pkc/src/bls/public_bytes.rs index e699539a..8d669042 100644 --- a/pkgs/pkc/src/bls/public_bytes.rs +++ b/pkgs/pkc/src/bls/public_bytes.rs @@ -10,10 +10,9 @@ use crate::bls::BlsSchemeId; use bitcoin_hashes::sha256d::Hash as Sha256d; use dash_num::Hash256; -use dash_types::codec::{take, BaseCodec, DecodeError, EncodeBuf, Hashable, TypeId}; -use dash_types::{derive_bytes, impl_type}; +use dash_types::codec::{Hashable, TypeId}; +use dash_types::{derive_bytes, impl_bytes}; -use core::fmt; use core::marker::PhantomData; /// Raw BLS public key length (G1 compressed). @@ -25,17 +24,7 @@ pub struct BlsPkBytes { _scheme: PhantomData, } -impl BaseCodec for BlsPkBytes { - fn decode(data: &mut &[u8]) -> Result { - take::(data).map(Self::from_bytes) - } - - fn encode(&self, buf: &mut impl EncodeBuf) { - buf.extend_from_slice(&self.inner); // nosemgrep: codec-no-raw-extend - } -} - -impl_type!(for[S: BlsSchemeId] BlsPkBytes, BLS_PK_LEN); +impl_bytes!(for[S: BlsSchemeId] BlsPkBytes, BLS_PK_LEN); impl Hashable for BlsPkBytes { type Hash = Hash256; @@ -63,11 +52,6 @@ impl BlsPkBytes { pub const fn into_bytes(self) -> [u8; BLS_PK_LEN] { self.inner } - - /// Returns `true` when every byte is zero. - pub fn is_null(&self) -> bool { - self.inner.iter().all(|&b| b == 0) - } } impl TypeId for BlsPkBytes { @@ -75,22 +59,3 @@ impl TypeId for BlsPkBytes { } derive_bytes!(for[S: BlsSchemeId] BlsPkBytes, BLS_PK_LEN); - -impl fmt::Debug for BlsPkBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "BlsPkBytes<{}>(", S::LABEL)?; - for byte in &self.inner { - write!(f, "{byte:02x}")?; - } - write!(f, ")") - } -} - -impl fmt::Display for BlsPkBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for byte in &self.inner { - write!(f, "{byte:02x}")?; - } - Ok(()) - } -} diff --git a/pkgs/pkc/src/bls/schemes.rs b/pkgs/pkc/src/bls/schemes.rs index edee56b7..521de2a9 100644 --- a/pkgs/pkc/src/bls/schemes.rs +++ b/pkgs/pkc/src/bls/schemes.rs @@ -16,8 +16,6 @@ pub trait BlsSchemeId: 'static { const SK_TYPE_ID: u32; /// `TypeId` constant for `BlsSigBytes`. const SIG_TYPE_ID: u32; - /// Human-readable scheme label for `Debug`/`Display`. - const LABEL: &'static str; } /// Legacy (Chia) BLS scheme marker. @@ -31,7 +29,6 @@ impl BlsSchemeId for BlsScChia { const SK_TYPE_ID: u32 = 0x3D50_6855; // xxh32(b"BlsSigBytesChia", 0) const SIG_TYPE_ID: u32 = 0xEF4A_E265; - const LABEL: &'static str = "Chia"; } /// IETF-standard BLS scheme marker. @@ -45,5 +42,4 @@ impl BlsSchemeId for BlsScIetf { const SK_TYPE_ID: u32 = 0xB5CE_BF45; // xxh32(b"BlsSigBytesIetf", 0) const SIG_TYPE_ID: u32 = 0xF57D_EF57; - const LABEL: &'static str = "Ietf"; } diff --git a/pkgs/pkc/src/bls/secret_bytes.rs b/pkgs/pkc/src/bls/secret_bytes.rs index 24dff237..f277dfc0 100644 --- a/pkgs/pkc/src/bls/secret_bytes.rs +++ b/pkgs/pkc/src/bls/secret_bytes.rs @@ -10,12 +10,11 @@ use crate::bls::BlsSchemeId; use bitcoin_hashes::sha256d::Hash as Sha256d; use dash_num::Hash256; -use dash_types::codec::{take, BaseCodec, DecodeError, EncodeBuf, Hashable, TypeId}; -use dash_types::impl_stype; +use dash_types::codec::{Hashable, TypeId}; +use dash_types::{derive_sbytes, impl_sbytes}; use subtle::ConstantTimeEq; -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; +use zeroize::{Zeroize, Zeroizing}; -use core::fmt; use core::marker::PhantomData; /// Raw BLS secret key length (scalar). @@ -27,17 +26,7 @@ pub struct BlsSkBytes { _scheme: PhantomData, } -impl BaseCodec for BlsSkBytes { - fn decode(data: &mut &[u8]) -> Result { - take::(data).map(Self::from_bytes) - } - - fn encode(&self, buf: &mut impl EncodeBuf) { - buf.extend_from_slice(&self.inner); // nosemgrep: codec-no-raw-extend - } -} - -impl_stype!(for[S: BlsSchemeId] BlsSkBytes, BLS_SK_LEN); +impl_sbytes!(for[S: BlsSchemeId] BlsSkBytes, BLS_SK_LEN); impl Hashable for BlsSkBytes { type Hash = Hash256; @@ -65,23 +54,20 @@ impl BlsSkBytes { pub fn to_bytes(&self) -> Zeroizing<[u8; BLS_SK_LEN]> { Zeroizing::new(self.inner) } - - /// Returns `true` when every byte is zero. - pub fn is_null(&self) -> bool { - self.inner.ct_eq(&[0u8; BLS_SK_LEN]).into() - } } impl TypeId for BlsSkBytes { const TYPE_ID: u32 = S::SK_TYPE_ID; } -impl AsRef<[u8; BLS_SK_LEN]> for BlsSkBytes { - fn as_ref(&self) -> &[u8; BLS_SK_LEN] { - &self.inner +impl Zeroize for BlsSkBytes { + fn zeroize(&mut self) { + self.inner.zeroize(); } } +derive_sbytes!(for[S: BlsSchemeId] BlsSkBytes, BLS_SK_LEN); + impl Clone for BlsSkBytes { fn clone(&self) -> Self { Self { @@ -91,20 +77,6 @@ impl Clone for BlsSkBytes { } } -impl Zeroize for BlsSkBytes { - fn zeroize(&mut self) { - self.inner.zeroize(); - } -} - -impl Drop for BlsSkBytes { - fn drop(&mut self) { - ::zeroize(self); - } -} - -impl ZeroizeOnDrop for BlsSkBytes {} - impl Eq for BlsSkBytes {} impl PartialEq for BlsSkBytes { @@ -112,15 +84,3 @@ impl PartialEq for BlsSkBytes { self.inner.ct_eq(&other.inner).into() } } - -impl fmt::Debug for BlsSkBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "BlsSkBytes<{}>(..)", S::LABEL) - } -} - -impl fmt::Display for BlsSkBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(self, f) - } -} diff --git a/pkgs/pkc/src/bls/sig_bytes.rs b/pkgs/pkc/src/bls/sig_bytes.rs index 3c0001a3..c7578622 100644 --- a/pkgs/pkc/src/bls/sig_bytes.rs +++ b/pkgs/pkc/src/bls/sig_bytes.rs @@ -10,10 +10,9 @@ use crate::bls::BlsSchemeId; use bitcoin_hashes::sha256d::Hash as Sha256d; use dash_num::Hash256; -use dash_types::codec::{take, BaseCodec, DecodeError, EncodeBuf, Hashable, TypeId}; -use dash_types::{derive_bytes, impl_type}; +use dash_types::codec::{Hashable, TypeId}; +use dash_types::{derive_bytes, impl_bytes}; -use core::fmt; use core::marker::PhantomData; /// Raw BLS signature length (G2 compressed). @@ -25,17 +24,7 @@ pub struct BlsSigBytes { _scheme: PhantomData, } -impl BaseCodec for BlsSigBytes { - fn decode(data: &mut &[u8]) -> Result { - take::(data).map(Self::from_bytes) - } - - fn encode(&self, buf: &mut impl EncodeBuf) { - buf.extend_from_slice(&self.inner); // nosemgrep: codec-no-raw-extend - } -} - -impl_type!(for[S: BlsSchemeId] BlsSigBytes, BLS_SIG_LEN); +impl_bytes!(for[S: BlsSchemeId] BlsSigBytes, BLS_SIG_LEN); impl Hashable for BlsSigBytes { type Hash = Hash256; @@ -63,11 +52,6 @@ impl BlsSigBytes { pub const fn into_bytes(self) -> [u8; BLS_SIG_LEN] { self.inner } - - /// Returns `true` when every byte is zero. - pub fn is_null(&self) -> bool { - self.inner.iter().all(|&b| b == 0) - } } impl TypeId for BlsSigBytes { @@ -75,22 +59,3 @@ impl TypeId for BlsSigBytes { } derive_bytes!(for[S: BlsSchemeId] BlsSigBytes, BLS_SIG_LEN); - -impl fmt::Debug for BlsSigBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "BlsSigBytes<{}>(", S::LABEL)?; - for byte in &self.inner { - write!(f, "{byte:02x}")?; - } - write!(f, ")") - } -} - -impl fmt::Display for BlsSigBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for byte in &self.inner { - write!(f, "{byte:02x}")?; - } - Ok(()) - } -} diff --git a/pkgs/pkc/src/bls_chia/sk.rs b/pkgs/pkc/src/bls_chia/sk.rs index af5a541d..772baa8c 100644 --- a/pkgs/pkc/src/bls_chia/sk.rs +++ b/pkgs/pkc/src/bls_chia/sk.rs @@ -9,7 +9,11 @@ use super::pk::PublicKey; use super::sig::Signature; use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsScChia}; +use crate::bls::{BlsError, BlsScChia, BlsSkBytes, BLS_SK_LEN}; + +use dash_num::Hash256; +use dash_types::{dlgt_scodec, type_cvrt}; +use zeroize::Zeroizing; use core::fmt; @@ -17,6 +21,8 @@ use core::fmt; #[derive(Clone)] pub struct SecretKey(pub(super) blst::blst_scalar); +dlgt_scodec!(SecretKey => BlsSkBytes, Hash256, BlsError, BLS_SK_LEN); + impl SecretKey { pub(super) fn from_inner(inner: blst::blst_scalar) -> Self { Self(inner) @@ -72,3 +78,11 @@ impl fmt::Debug for SecretKey { write!(f, "SecretKey(..)") } } + +type_cvrt!(From for BlsSkBytes, |sk| { + Self::from_bytes(*Zeroizing::new(sk.to_bytes())) +}); + +type_cvrt!(TryFrom> for SecretKey, BlsError, |bytes| { + Self::from_bytes(bytes.as_bytes()) +}); diff --git a/pkgs/pkc/src/bls_ietf/sk.rs b/pkgs/pkc/src/bls_ietf/sk.rs index dd36e808..f5b11ff0 100644 --- a/pkgs/pkc/src/bls_ietf/sk.rs +++ b/pkgs/pkc/src/bls_ietf/sk.rs @@ -10,10 +10,12 @@ use super::pk::PublicKey; use super::sig::Signature; use super::{DST, DST_POP, DST_POP_PROVE}; use crate::bls::scheme_ops::BlsScheme; -use crate::bls::{BlsError, BlsScIetf}; +use crate::bls::{BlsError, BlsScIetf, BlsSkBytes, BLS_SK_LEN}; use blst::min_pk; -use dash_types::Unencodable; +use dash_num::Hash256; +use dash_types::{dlgt_scodec, type_cvrt, Unencodable}; +use zeroize::Zeroizing; use core::fmt; @@ -33,6 +35,8 @@ pub enum Scheme { #[derive(Clone)] pub struct SecretKey(pub(super) min_pk::SecretKey); +dlgt_scodec!(SecretKey => BlsSkBytes, Hash256, BlsError, BLS_SK_LEN); + impl SecretKey { pub(super) fn from_inner(inner: min_pk::SecretKey) -> Self { Self(inner) @@ -89,3 +93,11 @@ impl fmt::Debug for SecretKey { write!(f, "SecretKey(..)") } } + +type_cvrt!(From for BlsSkBytes, |sk| { + Self::from_bytes(*Zeroizing::new(sk.to_bytes())) +}); + +type_cvrt!(TryFrom> for SecretKey, BlsError, |bytes| { + Self::from_bytes(bytes.as_bytes()) +}); diff --git a/pkgs/pkc/src/common/bls/mod.rs b/pkgs/pkc/src/common/bls/mod.rs index 7957f679..0c89cc67 100644 --- a/pkgs/pkc/src/common/bls/mod.rs +++ b/pkgs/pkc/src/common/bls/mod.rs @@ -12,8 +12,8 @@ pub(crate) mod contract; /// Implement Hash via to_bytes() for a BLS type. macro_rules! impl_hash_via_bytes { ($ty:ty) => { - impl core::hash::Hash for $ty { - fn hash(&self, state: &mut H) { + impl ::core::hash::Hash for $ty { + fn hash(&self, state: &mut H) { self.to_bytes().hash(state); } } diff --git a/pkgs/pkc/src/ecdsa/public_bytes.rs b/pkgs/pkc/src/ecdsa/public_bytes.rs index e0334fcf..5b054efd 100644 --- a/pkgs/pkc/src/ecdsa/public_bytes.rs +++ b/pkgs/pkc/src/ecdsa/public_bytes.rs @@ -11,11 +11,9 @@ use crate::prelude::*; use bitcoin_hashes::{ripemd160, sha256}; use cfg_if::cfg_if; -use dash_types::codec::{ - read_bytes, read_compact_size, write_compact_size, BaseCodec, DecodeError, EncodeBuf, Hashable, -}; +use dash_types::codec::{read_bytes, BaseCodec, DecodeError, EncodeBuf, Hashable}; use dash_types::TypeId; -use dash_types::{enum_map, impl_type}; +use dash_types::{enum_map, impl_type, CompactSize}; use core::cmp::Ordering; use core::fmt; @@ -71,7 +69,7 @@ pub struct EcdsaPkBytes { impl BaseCodec for EcdsaPkBytes { fn decode(data: &mut &[u8]) -> Result { - let n = read_compact_size(data, ECDSA_PK_LEN + 1)?; + let n = CompactSize::decode(data)?.into_len(ECDSA_PK_LEN + 1)?; let raw = read_bytes(data, n)?; let prefix = raw .first() @@ -91,7 +89,7 @@ impl BaseCodec for EcdsaPkBytes { fn encode(&self, buf: &mut impl EncodeBuf) { let bytes = self.as_bytes(); - write_compact_size(bytes.len(), buf); + CompactSize::from(bytes.len()).encode(buf); buf.extend_from_slice(bytes); // nosemgrep: codec-no-raw-extend } } diff --git a/pkgs/pkc/src/ecdsa/public_hash.rs b/pkgs/pkc/src/ecdsa/public_hash.rs index d5da806a..11d3b89d 100644 --- a/pkgs/pkc/src/ecdsa/public_hash.rs +++ b/pkgs/pkc/src/ecdsa/public_hash.rs @@ -9,8 +9,8 @@ use crate::prelude::*; use base58ck::encode_check; -use dash_types::codec::{ArrayBuf, BaseCodec, EncodeBuf}; -use dash_types::make_bytes; +use dash_types::codec::{BaseCodec, EncodeBuf}; +use dash_types::{make_bytes, ArrayBuf}; make_bytes! { /// 20-byte public key hash. diff --git a/pkgs/pkc/src/ecdsa/secret_bytes.rs b/pkgs/pkc/src/ecdsa/secret_bytes.rs index 8192539c..f1ebbc2e 100644 --- a/pkgs/pkc/src/ecdsa/secret_bytes.rs +++ b/pkgs/pkc/src/ecdsa/secret_bytes.rs @@ -10,10 +10,9 @@ use super::Compression; use crate::prelude::*; use base58ck::{decode_check, encode_check}; +use dash_types::derive_sbytes; use subtle::ConstantTimeEq; -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; - -use core::fmt; +use zeroize::{Zeroize, Zeroizing}; /// Raw secp256k1 secret key length. pub const ECDSA_SK_LEN: usize = 32; @@ -23,7 +22,7 @@ pub const ECDSA_SK_LEN: usize = 32; /// Carries a compression flag that decides how the derived public key /// serializes. The bytes are unvalidated: DER needs an in-range scalar, so the /// wire codec lives in [`EcdsaSecretKey`](crate::ecdsa::EcdsaSecretKey). -#[derive(Clone, Zeroize, ZeroizeOnDrop)] +#[derive(Clone, Zeroize)] pub struct EcdsaSkBytes { inner: [u8; ECDSA_SK_LEN], #[zeroize(skip)] @@ -71,11 +70,6 @@ impl EcdsaSkBytes { result.filter(|sk| !sk.is_null()) } - /// Returns `true` when every byte is zero. - pub fn is_null(&self) -> bool { - self.inner.ct_eq(&[0u8; ECDSA_SK_LEN]).into() - } - /// Copy out the raw inner bytes. pub fn to_bytes(&self) -> Zeroizing<[u8; ECDSA_SK_LEN]> { Zeroizing::new(self.inner) @@ -102,29 +96,7 @@ impl EcdsaSkBytes { } } -impl AsRef<[u8]> for EcdsaSkBytes { - fn as_ref(&self) -> &[u8] { - &self.inner - } -} - -impl AsRef<[u8; ECDSA_SK_LEN]> for EcdsaSkBytes { - fn as_ref(&self) -> &[u8; ECDSA_SK_LEN] { - &self.inner - } -} - -impl fmt::Debug for EcdsaSkBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "EcdsaSkBytes(..)") - } -} - -impl fmt::Display for EcdsaSkBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(self, f) - } -} +derive_sbytes!(EcdsaSkBytes, ECDSA_SK_LEN); impl Eq for EcdsaSkBytes {} diff --git a/pkgs/pkc/src/ecdsa/secret_ops.rs b/pkgs/pkc/src/ecdsa/secret_ops.rs index 7432c87f..6309d289 100644 --- a/pkgs/pkc/src/ecdsa/secret_ops.rs +++ b/pkgs/pkc/src/ecdsa/secret_ops.rs @@ -15,8 +15,8 @@ use super::{Compression, EcdsaRecSigBytes}; use bitcoin_hashes::sha256d; use dash_num::Hash256; -use dash_types::codec::{ensure, ArrayBuf, BaseCodec, DecodeError, EncodeBuf, Hashable}; -use dash_types::{impl_stype, type_cvrt, TypeId}; +use dash_types::codec::{ensure, BaseCodec, DecodeError, EncodeBuf, Hashable}; +use dash_types::{impl_stype, type_cvrt, ArrayBuf, TypeId}; use hex_conservative::hex; use k256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey}; use k256::elliptic_curve::ops::Neg; @@ -119,9 +119,8 @@ impl BaseCodec for EcdsaSecretKey { /// /// `buf` receives the secret scalar in plain bytes and is not zeroized by /// this function; callers who need the encoded form not to outlive its use - /// must supply a zeroizing buffer (e.g. - /// [`ArrayBuf`](dash_types::codec::ArrayBuf)) and zeroize or drop it - /// themselves once done. + /// must supply a zeroizing buffer (e.g. [`ArrayBuf`](dash_types::ArrayBuf)) + /// and zeroize or drop it themselves once done. fn encode(&self, buf: &mut impl EncodeBuf) { let scalar = self.to_bytes(); let public = self.inner.verifying_key().to_encoded_point(self.compressed); diff --git a/pkgs/pkc/src/ecdsa/sig_bytes.rs b/pkgs/pkc/src/ecdsa/sig_bytes.rs index 06451990..abf50eb3 100644 --- a/pkgs/pkc/src/ecdsa/sig_bytes.rs +++ b/pkgs/pkc/src/ecdsa/sig_bytes.rs @@ -11,10 +11,8 @@ use crate::prelude::*; use bitcoin_hashes::sha256d; use cfg_if::cfg_if; use dash_num::Hash256; -use dash_types::codec::{ - read_bytes, read_compact_size, write_compact_size, BaseCodec, DecodeError, EncodeBuf, Hashable, -}; -use dash_types::{impl_type, type_cvrt, TypeId}; +use dash_types::codec::{read_bytes, BaseCodec, DecodeError, EncodeBuf, Hashable}; +use dash_types::{impl_type, type_cvrt, CompactSize, TypeId}; use core::fmt; @@ -27,7 +25,7 @@ pub struct EcdsaSigBytes([u8; ECDSA_SIG_LEN]); impl BaseCodec for EcdsaSigBytes { fn decode(data: &mut &[u8]) -> Result { - let n = read_compact_size(data, ECDSA_SIG_LEN)?; + let n = CompactSize::decode(data)?.into_len(ECDSA_SIG_LEN)?; if n != ECDSA_SIG_LEN { return Err(DecodeError::BadLen { expected: vec![ECDSA_SIG_LEN], @@ -40,7 +38,7 @@ impl BaseCodec for EcdsaSigBytes { } fn encode(&self, buf: &mut impl EncodeBuf) { - write_compact_size(self.0.len(), buf); + CompactSize::from(self.0.len()).encode(buf); buf.extend_from_slice(&self.0); // nosemgrep: codec-no-raw-extend } } diff --git a/pkgs/pkc/src/ecdsa/sig_rec_bytes.rs b/pkgs/pkc/src/ecdsa/sig_rec_bytes.rs index 04592d1f..6959029a 100644 --- a/pkgs/pkc/src/ecdsa/sig_rec_bytes.rs +++ b/pkgs/pkc/src/ecdsa/sig_rec_bytes.rs @@ -13,10 +13,8 @@ use crate::prelude::*; use bitcoin_hashes::sha256d; use cfg_if::cfg_if; use dash_num::Hash256; -use dash_types::codec::{ - read_bytes, read_compact_size, write_compact_size, BaseCodec, DecodeError, EncodeBuf, Hashable, -}; -use dash_types::{enum_map, impl_type, type_cvrt, TypeId}; +use dash_types::codec::{read_bytes, BaseCodec, DecodeError, EncodeBuf, Hashable}; +use dash_types::{enum_map, impl_type, type_cvrt, CompactSize, TypeId}; use core::fmt; @@ -91,7 +89,7 @@ pub struct EcdsaRecSigBytes { impl BaseCodec for EcdsaRecSigBytes { fn decode(data: &mut &[u8]) -> Result { - let n = read_compact_size(data, ECDSA_SIG_LEN + 1)?; + let n = CompactSize::decode(data)?.into_len(ECDSA_SIG_LEN + 1)?; if n != ECDSA_SIG_LEN + 1 { return Err(DecodeError::BadLen { expected: vec![ECDSA_SIG_LEN + 1], @@ -115,7 +113,7 @@ impl BaseCodec for EcdsaRecSigBytes { } fn encode(&self, buf: &mut impl EncodeBuf) { - write_compact_size(ECDSA_SIG_LEN + 1, buf); + CompactSize::from(ECDSA_SIG_LEN + 1).encode(buf); buf.push(self.flags.to_base()); let sig = self.sig.as_bytes(); buf.extend_from_slice(sig); // nosemgrep: codec-no-raw-extend diff --git a/pkgs/pow/src/jh/scalar.rs b/pkgs/pow/src/jh/scalar.rs index 715ef158..830fbab4 100644 --- a/pkgs/pow/src/jh/scalar.rs +++ b/pkgs/pow/src/jh/scalar.rs @@ -137,10 +137,10 @@ pub const fn e8(h: &mut [u64; 16]) { } 6 => { // Swap the two u64s in each pair - core::mem::swap(&mut h2, &mut h3); - core::mem::swap(&mut h6, &mut h7); - core::mem::swap(&mut ha, &mut hb); - core::mem::swap(&mut he, &mut hf); + ::core::mem::swap(&mut h2, &mut h3); + ::core::mem::swap(&mut h6, &mut h7); + ::core::mem::swap(&mut ha, &mut hb); + ::core::mem::swap(&mut he, &mut hf); } _ => {} } diff --git a/pkgs/primitives/src/block.rs b/pkgs/primitives/src/block.rs index b977f3bf..515a5b38 100644 --- a/pkgs/primitives/src/block.rs +++ b/pkgs/primitives/src/block.rs @@ -13,8 +13,8 @@ use crate::{codec_base, codec_type, hash_impl}; use bitcoin_hashes::sha256d; use dash_num::{make_hash, Arith256, CompactTarget, Hash256}; use dash_pow::hash as pow_hash; -use dash_types::codec::{ArrayBuf, BaseCodec, Checkable, Hashable}; -use dash_types::{TypeId, Unencodable}; +use dash_types::codec::{BaseCodec, Checkable, Hashable}; +use dash_types::{ArrayBuf, TypeId, Unencodable}; use core::fmt; diff --git a/pkgs/primitives/src/gov.rs b/pkgs/primitives/src/gov.rs index 43ae1866..3126931b 100644 --- a/pkgs/primitives/src/gov.rs +++ b/pkgs/primitives/src/gov.rs @@ -13,8 +13,8 @@ use crate::{codec_base, hash_impl, TxHash}; use bitcoin_hashes::sha256d; use bitcoin_units::Amount; use dash_num::Hash256; -use dash_types::codec::{ArrayBuf, BaseCodec, Checkable, Hashable}; -use dash_types::{enum_map, impl_num, TypeId, Unencodable}; +use dash_types::codec::{BaseCodec, Checkable, Hashable}; +use dash_types::{enum_map, impl_num, ArrayBuf, TypeId, Unencodable}; use hex_conservative::DisplayHex; use core::fmt; diff --git a/pkgs/primitives/src/payload/cbtx.rs b/pkgs/primitives/src/payload/cbtx.rs index 9f803d62..9031406b 100644 --- a/pkgs/primitives/src/payload/cbtx.rs +++ b/pkgs/primitives/src/payload/cbtx.rs @@ -11,8 +11,8 @@ use crate::{hash_impl, MerkleRoot}; use bitcoin_units::BlockHeight; use dash_pkc::bls::{BlsScIetf, BlsSigBytes}; -use dash_types::codec::{self, BaseCodec, Checkable, DecodeError, EncodeBuf}; -use dash_types::{TypeId, Unencodable}; +use dash_types::codec::{BaseCodec, Checkable, DecodeError, EncodeBuf}; +use dash_types::{CompactSize, TypeId, Unencodable}; use core::fmt; @@ -55,7 +55,7 @@ impl BaseCodec for CoinbaseCommitment { }; let (best_cl_height_diff, best_cl_signature, credit_pool_balance) = if version >= 3 { ( - Some(codec::read_compact_u64(data)?), + Some(CompactSize::decode(data)?.get()), Some(BlsSigBytes::::decode(data)?), Some(i64::decode(data)?), ) @@ -86,7 +86,7 @@ impl BaseCodec for CoinbaseCommitment { self.best_cl_signature, self.credit_pool_balance, ) { - codec::write_compact_u64(diff, buf); + CompactSize::from(diff).encode(buf); sig.encode(buf); bal.encode(buf); } diff --git a/pkgs/primitives/src/support.rs b/pkgs/primitives/src/support.rs index 0ad9e57c..b363d864 100644 --- a/pkgs/primitives/src/support.rs +++ b/pkgs/primitives/src/support.rs @@ -10,7 +10,7 @@ use crate::hash_impl; use crate::prelude::*; use dash_types::codec::{self, BaseCodec, DecodeError, EncodeBuf}; -use dash_types::{enum_map, impl_num, impl_type, TypeId, Unencodable}; +use dash_types::{enum_map, impl_num, impl_type, CompactSize, TypeId, Unencodable}; enum_map! { /// LLMQ type (quorum size/threshold configuration). @@ -91,7 +91,7 @@ struct DynBitsetSerde { impl BaseCodec for DynBitset { fn decode(data: &mut &[u8]) -> Result { - let num_bits = codec::read_compact_u64(data)?; + let num_bits = CompactSize::decode(data)?.get(); let byte_len = usize::try_from(num_bits.div_ceil(8)).map_err(|_| DecodeError::CompactSizeExceedsLimit { limit: usize::MAX, value: num_bits, @@ -114,7 +114,7 @@ impl BaseCodec for DynBitset { } fn encode(&self, buf: &mut impl EncodeBuf) { - codec::write_compact_u64(self.num_bits, buf); + CompactSize::from(self.num_bits).encode(buf); let required = (self.num_bits as usize).div_ceil(8); let src = &self.data; let take = src.len().min(required); diff --git a/pkgs/primitives/src/transaction.rs b/pkgs/primitives/src/transaction.rs index 9f6933fd..9063c7ba 100644 --- a/pkgs/primitives/src/transaction.rs +++ b/pkgs/primitives/src/transaction.rs @@ -16,7 +16,7 @@ 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::{impl_type, TypeId, Unencodable}; +use dash_types::{impl_type, CompactSize, TypeId, Unencodable}; use core::fmt; @@ -216,7 +216,7 @@ impl BaseCodec for Transaction { outputs: Vec::decode(data)?, lock_time: u32::decode(data)?, extra_payload: if version >= 3 && tx_type != TxType::Spend { - let len = codec::read_compact_size(data, crate::codec::MAX_SPTX_PAYLOAD_SIZE)?; + let len = CompactSize::decode(data)?.into_len(crate::codec::MAX_SPTX_PAYLOAD_SIZE)?; codec::read_bytes(data, len)?.to_vec() } else { Vec::new() diff --git a/pkgs/primitives/src/types/addrv1.rs b/pkgs/primitives/src/types/addrv1.rs index d5d2c561..666aae9b 100644 --- a/pkgs/primitives/src/types/addrv1.rs +++ b/pkgs/primitives/src/types/addrv1.rs @@ -24,7 +24,7 @@ const IPV4_MAPPED_PREFIX: [u8; 12] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff]; #[derive(Clone, Copy, Default, Eq, Hash, PartialEq, TypeId)] pub struct AddrV1(pub [u8; 16]); -impl_bytes!(16, AddrV1); +impl_bytes!(AddrV1, 16); impl Checkable for AddrV1 { type Error = NetAddrError; @@ -51,16 +51,21 @@ impl Checkable for AddrV1 { hash_impl!(AddrV1); impl AddrV1 { - /// Returns the inner byte array. - pub const fn to_bytes(self) -> [u8; 16] { - self.0 - } - /// Borrows the inner byte array. pub const fn as_bytes(&self) -> &[u8; 16] { &self.0 } + /// Wraps raw bytes without validation. + pub const fn from_bytes(bytes: [u8; 16]) -> Self { + Self(bytes) + } + + /// Returns the inner byte array. + pub const fn to_bytes(self) -> [u8; 16] { + self.0 + } + /// Returns `true` when every byte is zero. pub fn is_null(&self) -> bool { self.0.iter().all(|&b| b == 0) diff --git a/pkgs/primitives/src/types/addrv2.rs b/pkgs/primitives/src/types/addrv2.rs index 550ebdf0..0291808b 100644 --- a/pkgs/primitives/src/types/addrv2.rs +++ b/pkgs/primitives/src/types/addrv2.rs @@ -14,7 +14,7 @@ use crate::prelude::*; use bitcoin_hashes::sha3_256; use dash_types::codec::{self, BaseCodec, Checkable, DecodeError, EncodeBuf, NumCodec}; -use dash_types::{impl_type, type_cvrt, TypeId}; +use dash_types::{impl_type, type_cvrt, CompactSize, TypeId}; use core::fmt; use core::net::{Ipv4Addr, Ipv6Addr}; @@ -50,7 +50,7 @@ impl BaseCodec for AddrV2 { fn decode(data: &mut &[u8]) -> Result { let net_byte = u8::decode(data)?; let network = NetworkType::from_base(net_byte); - let len = codec::read_compact_size(data, MAX_ADDR_LEN)?; + let len = CompactSize::decode(data)?.into_len(MAX_ADDR_LEN)?; if let Some(expected) = network.expected_len() { if len != expected { return Err(DecodeError::BadLen { @@ -101,7 +101,7 @@ impl BaseCodec for AddrV2 { fn encode(&self, buf: &mut impl EncodeBuf) { self.network().to_base().encode(buf); let bytes = self.bytes(); - codec::write_compact_size(bytes.len(), buf); + CompactSize::from(bytes.len()).encode(buf); buf.extend_from_slice(bytes); // nosemgrep: codec-no-raw-extend } } diff --git a/pkgs/primitives/src/types/netinfo.rs b/pkgs/primitives/src/types/netinfo.rs index 8ee5fff7..a0bba220 100644 --- a/pkgs/primitives/src/types/netinfo.rs +++ b/pkgs/primitives/src/types/netinfo.rs @@ -12,7 +12,7 @@ use crate::hash_impl; use crate::prelude::*; use dash_types::codec::{self, BaseCodec, Checkable, DecodeError, EncodeBuf, NumCodec}; -use dash_types::{enum_map, impl_num, impl_type, TypeId, Unencodable}; +use dash_types::{enum_map, impl_num, impl_type, CompactSize, TypeId, Unencodable}; use core::fmt; @@ -154,7 +154,7 @@ impl BaseCodec for NIEntry { match NIEntryCode::from_base(u8::decode(data)?) { NIEntryCode::Service => Ok(Self::Service(ServiceV2::decode(data)?)), NIEntryCode::Domain => { - let name_len = codec::read_compact_size(data, data.len())?; + let name_len = CompactSize::decode(data)?.into_len(data.len())?; let name = codec::read_bytes(data, name_len)?.to_vec(); let port = codec::read_u16_be(data)?; Ok(Self::Domain { name, port }) @@ -302,11 +302,11 @@ impl_type!(NetInfoV2); impl BaseCodec for NetInfoV2 { fn decode(data: &mut &[u8]) -> Result { let version = u8::decode(data)?; - let purpose_count = codec::read_compact_size(data, data.len())?; + let purpose_count = CompactSize::decode(data)?.into_len(data.len())?; let mut entries = Vec::with_capacity(purpose_count); for _ in 0..purpose_count { let purpose = NIPurpose::from_base(u8::decode(data)?); - let entry_count = codec::read_compact_size(data, data.len())?; + let entry_count = CompactSize::decode(data)?.into_len(data.len())?; let mut group = Vec::with_capacity(entry_count); for _ in 0..entry_count { group.push(NIEntry::decode(data)?); @@ -318,10 +318,10 @@ impl BaseCodec for NetInfoV2 { fn encode(&self, buf: &mut impl EncodeBuf) { self.version.encode(buf); - codec::write_compact_size(self.entries.len(), buf); + CompactSize::from(self.entries.len()).encode(buf); for (purpose, group) in &self.entries { purpose.to_base().encode(buf); - codec::write_compact_size(group.len(), buf); + CompactSize::from(group.len()).encode(buf); for entry in group { entry.encode(buf); } diff --git a/pkgs/types/Cargo.toml b/pkgs/types/Cargo.toml index 67cea7e4..6b19b883 100644 --- a/pkgs/types/Cargo.toml +++ b/pkgs/types/Cargo.toml @@ -35,6 +35,7 @@ dash-types-marker = { version = "0.0.0", path = "marker" } hex-conservative = { version = "0.3", default-features = false, features = [ "alloc", ], optional = true } +subtle = { version = "2", default-features = false } serde = { version = "1", default-features = false, features = [ "derive", "alloc", diff --git a/pkgs/types/src/adapters.rs b/pkgs/types/src/adapters.rs index 42c3028b..d9489ff3 100644 --- a/pkgs/types/src/adapters.rs +++ b/pkgs/types/src/adapters.rs @@ -11,14 +11,14 @@ macro_rules! adapt_codec { (<$gen:ident>, $ty:ty) => { impl<$gen> $crate::codec::BaseCodec for $ty { fn decode(data: &mut &[u8]) -> Result { - let n = $crate::codec::read_compact_size(data, data.len())?; + let n = $crate::CompactSize::decode(data)?.into_len(data.len())?; let bytes = $crate::codec::read_bytes(data, n)?; Ok(Self::from_bytes(bytes.to_vec())) } fn encode(&self, buf: &mut impl $crate::codec::EncodeBuf) { let bytes = self.as_bytes(); - $crate::codec::write_compact_size(bytes.len(), buf); + $crate::CompactSize::from(bytes.len()).encode(buf); buf.extend_from_slice(bytes); } } @@ -41,9 +41,10 @@ macro_rules! adapt_codec { #[cfg(feature = "bitcoin-primitives")] pub mod bitcoin_primitives { - use crate::codec::{ArrayBuf, BaseCodec, EncodeBuf, Hashable}; + use crate::codec::{BaseCodec, EncodeBuf, Hashable}; use crate::make_bytes; use crate::prelude::*; + use crate::secret::ArrayBuf; use base58ck::encode_check; use bitcoin_hashes::{ripemd160, sha256}; diff --git a/pkgs/types/src/codec.rs b/pkgs/types/src/codec.rs index 20f54c4e..db2727a3 100644 --- a/pkgs/types/src/codec.rs +++ b/pkgs/types/src/codec.rs @@ -7,8 +7,7 @@ //! Codec traits and helpers. use crate::prelude::*; - -use zeroize::Zeroize; +use crate::CompactSize; use core::convert::Infallible; use core::fmt; @@ -158,45 +157,6 @@ pub fn read_bytes<'a>(data: &mut &'a [u8], n: usize) -> Result<&'a [u8], DecodeE Ok(head) } -/// Reads a CompactSize-encoded `u64` with minimal encoding check. -pub fn read_compact_u64(data: &mut &[u8]) -> Result { - let first = u8::decode(data)?; - match first { - 0..=0xFC => Ok(u64::from(first)), - 0xFD => { - let v = u16::decode(data)?; - if v < 0xFD { - return Err(DecodeError::NonMinimalCompactSize { value: u64::from(v) }); - } - Ok(u64::from(v)) - } - 0xFE => { - let v = u32::decode(data)?; - if v < 0x10000 { - return Err(DecodeError::NonMinimalCompactSize { value: u64::from(v) }); - } - Ok(u64::from(v)) - } - 0xFF => { - let v = u64::decode(data)?; - if v < 0x1_0000_0000 { - return Err(DecodeError::NonMinimalCompactSize { value: v }); - } - Ok(v) - } - } -} - -/// Reads a CompactSize-encoded length with a limit. -pub fn read_compact_size(data: &mut &[u8], limit: usize) -> Result { - let value = read_compact_u64(data)?; - let n = usize::try_from(value).map_err(|_| DecodeError::CompactSizeExceedsLimit { limit, value })?; - if n > limit { - return Err(DecodeError::CompactSizeExceedsLimit { limit, value }); - } - Ok(n) -} - /// Append-only byte buffer used by [`BaseCodec::encode`]. pub trait EncodeBuf { /// Appends a single byte. @@ -216,104 +176,6 @@ impl EncodeBuf for Vec { } } -/// Fixed-size encode buffer backed by `[u8; N]`. -/// -/// # Panics -/// -/// Writing more than `N` bytes (via the [`EncodeBuf`] impl) panics with an -/// index-out-of-bounds. -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub struct ArrayBuf { - buf: [u8; N], - len: usize, -} - -impl ArrayBuf { - /// Creates an empty buffer. - pub const fn new() -> Self { - Self { buf: [0u8; N], len: 0 } - } - - /// Borrows the written bytes. - pub fn as_bytes(&self) -> &[u8] { - &self.buf[..self.len] - } - - /// Returns `true` when nothing has been written. - pub const fn is_empty(&self) -> bool { - self.len == 0 - } - - /// Number of bytes written so far. - pub const fn len(&self) -> usize { - self.len - } - - /// Remaining writable capacity. - pub const fn spare(&self) -> usize { - N - self.len - } - - /// Returns the written bytes as a fixed array. - /// - /// # Panics - /// - /// Panics if exactly `N` bytes were not written. - pub fn into_array(self) -> [u8; N] { - assert!(self.len == N, "expected {N} bytes, wrote {}", self.len); - self.buf - } -} - -impl Zeroize for ArrayBuf { - fn zeroize(&mut self) { - self.buf.zeroize(); - self.len = 0; - } -} - -impl Default for ArrayBuf { - fn default() -> Self { - Self::new() - } -} - -impl EncodeBuf for ArrayBuf { - fn push(&mut self, byte: u8) { - self.buf[self.len] = byte; - self.len += 1; - } - - fn extend_from_slice(&mut self, data: &[u8]) { - self.buf[self.len..self.len + data.len()].copy_from_slice(data); - self.len += data.len(); - } -} - -/// Encodes a `usize` as a CompactSize integer. -pub fn write_compact_size(value: usize, buf: &mut impl EncodeBuf) { - write_compact_u64(value as u64, buf); -} - -/// Encodes a `u64` as a CompactSize integer. -pub fn write_compact_u64(value: u64, buf: &mut impl EncodeBuf) { - match value { - 0..=0xFC => buf.push(value as u8), - 0xFD..=0xFFFF => { - buf.push(0xFD); - buf.extend_from_slice(&(value as u16).to_le_bytes()); - } - 0x1_0000..=0xFFFF_FFFF => { - buf.push(0xFE); - buf.extend_from_slice(&(value as u32).to_le_bytes()); - } - _ => { - buf.push(0xFF); - buf.extend_from_slice(&value.to_le_bytes()); - } - } -} - /// Links a type to its underlying base integer type. pub trait NumCodec: Sized { /// Constructs from the base integer. @@ -451,7 +313,7 @@ impl BaseCodec for [u8; N] { impl BaseCodec for Vec { fn decode(data: &mut &[u8]) -> Result { - let count = read_compact_size(data, data.len())?; + let count = CompactSize::decode(data)?.into_len(data.len())?; let batch = MAX_VECTOR_ALLOCATE / core::mem::size_of::().max(1); let mut items = Vec::new(); let mut allocated = 0usize; @@ -466,7 +328,7 @@ impl BaseCodec for Vec { } fn encode(&self, buf: &mut impl EncodeBuf) { - write_compact_size(self.len(), buf); + CompactSize::from(self.len()).encode(buf); for item in self { item.encode(buf); } @@ -480,7 +342,7 @@ impl BaseCodec for String { } fn encode(&self, buf: &mut impl EncodeBuf) { - write_compact_size(self.len(), buf); + CompactSize::from(self.len()).encode(buf); buf.extend_from_slice(self.as_bytes()); } } @@ -540,6 +402,7 @@ impl __CodecMarker for T {} mod tests { use super::DecodeError; use crate::prelude::*; + use crate::{VecDecoder, VecEncoder}; use rstest::*; @@ -583,4 +446,21 @@ mod tests { assert_eq!(lifted.to_string(), before); assert!(!matches!(lifted, DecodeError::DecError(_))); } + + /// Consumes the whole cursor, so `end()` sees no trailing bytes. + fn take_all(data: &mut &[u8]) -> Result, DecodeError> { + let out = data.to_vec(); + *data = &[]; + Ok(out) + } + + /// Both encoders redact: a `{:?}` in a panic must not print key material. + #[rstest] + fn debug_impls_redact_contents() { + let venc = VecEncoder::new(vec![0xFFu8; 8]); + assert!(!format!("{venc:?}").contains("255")); + + let vdec = VecDecoder::>::new(take_all, 16); + assert!(format!("{vdec:?}").contains("limit: 16")); + } } diff --git a/pkgs/types/src/compact.rs b/pkgs/types/src/compact.rs new file mode 100644 index 00000000..9cdf78d9 --- /dev/null +++ b/pkgs/types/src/compact.rs @@ -0,0 +1,177 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! CompactSize-encoded integers. + +use crate::codec::{BaseCodec, DecodeError, EncodeBuf}; + +/// An unsigned integer encoded in variable-width CompactSize. +#[repr(transparent)] +#[cfg_attr(feature = "serde", derive(::serde::Deserialize, ::serde::Serialize))] +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct CompactSize(u64); + +impl BaseCodec for CompactSize { + fn decode(data: &mut &[u8]) -> Result { + let first = u8::decode(data)?; + let value = match first { + 0..=0xFC => u64::from(first), + 0xFD => { + let value = u16::decode(data)?; + if value < 0xFD { + return Err(DecodeError::NonMinimalCompactSize { + value: u64::from(value), + }); + } + u64::from(value) + } + 0xFE => { + let value = u32::decode(data)?; + if value < 0x1_0000 { + return Err(DecodeError::NonMinimalCompactSize { + value: u64::from(value), + }); + } + u64::from(value) + } + 0xFF => { + let value = u64::decode(data)?; + if value < 0x1_0000_0000 { + return Err(DecodeError::NonMinimalCompactSize { value }); + } + value + } + }; + Ok(Self(value)) + } + + fn encode(&self, buf: &mut impl EncodeBuf) { + match self.0 { + 0..=0xFC => buf.push(self.0 as u8), + 0xFD..=0xFFFF => { + buf.push(0xFD); + buf.extend_from_slice(&(self.0 as u16).to_le_bytes()); + } + 0x1_0000..=0xFFFF_FFFF => { + buf.push(0xFE); + buf.extend_from_slice(&(self.0 as u32).to_le_bytes()); + } + _ => { + buf.push(0xFF); + buf.extend_from_slice(&self.0.to_le_bytes()); + } + } + } +} + +impl CompactSize { + /// Wraps an integer for CompactSize encoding. + pub const fn new(value: u64) -> Self { + Self(value) + } + + /// Returns the wrapped integer. + pub const fn get(self) -> u64 { + self.0 + } + + /// Converts the value to a length no greater than `limit`. + /// + /// # Errors + /// + /// Returns [`DecodeError::CompactSizeExceedsLimit`] when the value does not + /// fit in `usize` or exceeds `limit`. + pub fn into_len(self, limit: usize) -> Result { + let value = self.0; + let len = usize::try_from(value).map_err(|_| DecodeError::CompactSizeExceedsLimit { limit, value })?; + if len > limit { + return Err(DecodeError::CompactSizeExceedsLimit { limit, value }); + } + Ok(len) + } +} + +impl From for CompactSize { + fn from(value: u64) -> Self { + Self(value) + } +} + +impl From for CompactSize { + fn from(value: usize) -> Self { + Self(value as u64) + } +} + +impl From for u64 { + fn from(value: CompactSize) -> Self { + value.0 + } +} + +#[cfg(test)] +mod tests { + use super::CompactSize; + use crate::codec::{BaseCodec, DecodeError}; + use crate::prelude::*; + + use rstest::*; + + #[rstest] + #[case::single_min(0, &[0x00])] + #[case::single_max(0xFC, &[0xFC])] + #[case::u16_min(0xFD, &[0xFD, 0xFD, 0x00])] + #[case::u16_max(0xFFFF, &[0xFD, 0xFF, 0xFF])] + #[case::u32_min(0x1_0000, &[0xFE, 0x00, 0x00, 0x01, 0x00])] + #[case::u32_max(0xFFFF_FFFF, &[0xFE, 0xFF, 0xFF, 0xFF, 0xFF])] + #[case::u64_min(0x1_0000_0000, &[0xFF, 0, 0, 0, 0, 0x01, 0, 0, 0])] + #[case::u64_max(u64::MAX, &[0xFF; 9])] + fn roundtrips_at_every_width_boundary(#[case] value: u64, #[case] wire: &[u8]) { + let mut buf = Vec::new(); + CompactSize::new(value).encode(&mut buf); + assert_eq!(buf, wire, "encoding {value:#x}"); + + let mut cursor = wire; + assert_eq!(CompactSize::decode(&mut cursor).map(CompactSize::get), Ok(value)); + assert!(cursor.is_empty(), "decode left {} bytes", cursor.len()); + } + + #[rstest] + #[case::u16_holds_single(&[0xFD, 0xFC, 0x00], 0xFC)] + #[case::u32_holds_u16(&[0xFE, 0xFF, 0xFF, 0x00, 0x00], 0xFFFF)] + #[case::u64_holds_u32(&[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0, 0], 0xFFFF_FFFF)] + fn rejects_non_minimal_encodings(#[case] wire: &[u8], #[case] value: u64) { + assert_eq!( + CompactSize::decode(&mut &*wire), + Err(DecodeError::NonMinimalCompactSize { value }) + ); + } + + #[rstest] + #[case::marker_only(&[0xFD])] + #[case::short_u32(&[0xFE, 0x00, 0x00])] + #[case::short_u64(&[0xFF, 0x00, 0x00, 0x00, 0x00])] + fn rejects_truncated_input(#[case] wire: &[u8]) { + assert!(matches!(CompactSize::decode(&mut &*wire), Err(DecodeError::Eof { .. }))); + } + + #[rstest] + fn into_len_enforces_the_caller_limit() { + assert_eq!(CompactSize::new(8).into_len(8), Ok(8)); + assert_eq!( + CompactSize::new(9).into_len(8), + Err(DecodeError::CompactSizeExceedsLimit { limit: 8, value: 9 }) + ); + // A count field with no room to spare still admits the empty case. + assert_eq!(CompactSize::new(0).into_len(0), Ok(0)); + } + + #[rstest] + fn conversions_preserve_the_value() { + assert_eq!(u64::from(CompactSize::from(0xDEAD_u64)), 0xDEAD); + assert_eq!(CompactSize::from(7usize).get(), 7); + } +} diff --git a/pkgs/types/src/entity.rs b/pkgs/types/src/entity.rs index eaf96817..e51a3ff0 100644 --- a/pkgs/types/src/entity.rs +++ b/pkgs/types/src/entity.rs @@ -4,14 +4,12 @@ // See the accompanying file LICENSE or https://opensource.org/license/MIT // -//! Bridge utilities for `BaseCodec` types to `bitcoin_consensus_encoding` -//! traits. +//! Buffered codec implementation. -use crate::codec::{ArrayBuf, DecodeError, EncodeBuf}; +use crate::codec::DecodeError; use crate::prelude::*; use bitcoin_consensus_encoding::{Decoder, Encoder}; -use zeroize::Zeroize; use core::convert::Infallible; use core::fmt; @@ -19,123 +17,35 @@ use core::fmt; /// Maximum serialized object size (32 MiB). pub const MAX_SER_SIZE: usize = 0x0200_0000; -/// Widest buffer [`ArrEncoder`] and [`ArrDecoder`] will wipe. -pub const MAX_ARR_SIZE: usize = 512; - -/// A decoder that buffers all input and decodes in `end()`. -/// -/// Wraps types with complex sequential decode logic (conditional fields, -/// version branching) that cannot be expressed as a composable push-decoder -/// without excessive boilerplate. -pub struct BufferDecoder { - buf: Vec, - limit: usize, - decode_fn: fn(&mut &[u8]) -> Result>, -} - -impl BufferDecoder { - /// Creates a new decoder with the given decode function and - /// maximum buffer size. - pub const fn new(decode_fn: fn(&mut &[u8]) -> Result>, limit: usize) -> Self { - Self { - buf: Vec::new(), - limit, - decode_fn, - } - } -} - -impl fmt::Debug for BufferDecoder { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("BufferDecoder") - .field("buf_len", &self.buf.len()) - .field("limit", &self.limit) - .finish() - } -} - -impl Clone for BufferDecoder { - fn clone(&self) -> Self { - Self { - buf: self.buf.clone(), - limit: self.limit, - decode_fn: self.decode_fn, - } - } -} - -impl Decoder for BufferDecoder { - type Output = T; - type Error = DecodeError; - - fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result { - let remaining = self.limit.saturating_sub(self.buf.len()); - if remaining == 0 { - return Ok(false); - } - let take = bytes.len().min(remaining); - self.buf.extend_from_slice(&bytes[..take]); - *bytes = &bytes[take..]; - Ok(true) - } - - fn end(self) -> Result { - let mut cursor = &self.buf[..]; - let result = (self.decode_fn)(&mut cursor)?; - if !cursor.is_empty() { - return Err(DecodeError::TrailingBytes { - remaining: cursor.len(), - }); - } - Ok(result) - } - - fn read_limit(&self) -> usize { - self.limit.saturating_sub(self.buf.len()) - } -} - -/// An encoder for values whose encoded width is bounded at compile time. -/// -/// Costs a byte-wise volatile write per byte of `N`, so it suits key material -/// and other small fixed records, not block-sized payloads. [`MAX_ARR_SIZE`] -/// caps `N` due to performance cost. -pub struct ArrEncoder { - data: ArrayBuf, +/// An encoder that wraps a pre-built byte vector. +#[derive(Clone)] +pub struct VecEncoder { + data: Vec, done: bool, } -impl ArrEncoder { - /// Wraps a filled buffer. - /// - /// Refuses to compile when `N` exceeds [`MAX_ARR_SIZE`]. - pub const fn new(data: ArrayBuf) -> Self { - const { assert!(N <= MAX_ARR_SIZE, "unusually large zeroized buffer") }; +impl VecEncoder { + /// Creates a new encoder wrapping the given bytes. + pub fn new(data: Vec) -> Self { Self { data, done: false } } } -impl fmt::Debug for ArrEncoder { +impl fmt::Debug for VecEncoder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ArrEncoder") + f.debug_struct("VecEncoder") .field("len", &self.data.len()) .field("done", &self.done) .finish() } } -impl Drop for ArrEncoder { - fn drop(&mut self) { - self.data.zeroize(); - } -} - -impl Encoder for ArrEncoder { +impl Encoder for VecEncoder { fn current_chunk(&self) -> &[u8] { if self.done { &[] } else { - self.data.as_bytes() + &self.data } } @@ -149,46 +59,54 @@ impl Encoder for ArrEncoder { } } -/// A decoder for values whose encoded width is bounded by `N`. -pub struct ArrDecoder { - buf: ArrayBuf, +/// A decoder that buffers all input and decodes in `end()`. +/// +/// Wraps types with complex sequential decode logic (conditional fields, +/// version branching) that cannot be expressed as a composable push-decoder +/// without excessive boilerplate. +pub struct VecDecoder { + buf: Vec, + limit: usize, decode_fn: fn(&mut &[u8]) -> Result>, } -impl ArrDecoder { - /// Creates a decoder that accepts at most `N` bytes. - /// - /// Refuses to compile when `N` exceeds [`MAX_ARR_SIZE`]. - pub const fn new(decode_fn: fn(&mut &[u8]) -> Result>) -> Self { - const { assert!(N <= MAX_ARR_SIZE, "unusually large zeroized buffer") }; +impl VecDecoder { + /// Creates a new decoder with the given decode function and + /// maximum buffer size. + pub const fn new(decode_fn: fn(&mut &[u8]) -> Result>, limit: usize) -> Self { Self { - buf: ArrayBuf::new(), + buf: Vec::new(), + limit, decode_fn, } } } -impl fmt::Debug for ArrDecoder { +impl fmt::Debug for VecDecoder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("ArrDecoder") + f.debug_struct("VecDecoder") .field("buf_len", &self.buf.len()) - .field("limit", &N) + .field("limit", &self.limit) .finish() } } -impl Drop for ArrDecoder { - fn drop(&mut self) { - self.buf.zeroize(); +impl Clone for VecDecoder { + fn clone(&self) -> Self { + Self { + buf: self.buf.clone(), + limit: self.limit, + decode_fn: self.decode_fn, + } } } -impl Decoder for ArrDecoder { +impl Decoder for VecDecoder { type Output = T; type Error = DecodeError; fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result { - let remaining = self.buf.spare(); + let remaining = self.limit.saturating_sub(self.buf.len()); if remaining == 0 { return Ok(false); } @@ -199,9 +117,7 @@ impl Decoder for ArrDecoder { } fn end(self) -> Result { - // Borrow rather than destructure: `Drop` wipes the buffer on the way out, - // including on the early return below. - let mut cursor = self.buf.as_bytes(); + let mut cursor = &self.buf[..]; let result = (self.decode_fn)(&mut cursor)?; if !cursor.is_empty() { return Err(DecodeError::TrailingBytes { @@ -212,61 +128,19 @@ impl Decoder for ArrDecoder { } fn read_limit(&self) -> usize { - self.buf.spare() - } -} - -/// An encoder that wraps a pre-built byte vector. -#[derive(Clone)] -pub struct VecEncoder { - data: Vec, - done: bool, -} - -impl VecEncoder { - /// Creates a new encoder wrapping the given bytes. - pub fn new(data: Vec) -> Self { - Self { data, done: false } - } -} - -impl fmt::Debug for VecEncoder { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("VecEncoder") - .field("len", &self.data.len()) - .field("done", &self.done) - .finish() - } -} - -impl Encoder for VecEncoder { - fn current_chunk(&self) -> &[u8] { - if self.done { - &[] - } else { - &self.data - } - } - - fn advance(&mut self) -> bool { - if self.done { - false - } else { - self.done = true; - false - } + self.limit.saturating_sub(self.buf.len()) } } /// Generates `Encodable` + `Decodable` for a `BaseCodec` implementor. /// -/// Stages through the growable [`VecEncoder`]/[`BufferDecoder`] pair. For +/// Stages through the growable [`VecEncoder`]/[`VecDecoder`] pair. For /// secret material use [`impl_stype!`](crate::impl_stype) instead, which is /// the same generator over the wiping fixed-width pair. #[macro_export] macro_rules! impl_type { (@parse [$($impl_generics:tt)*] $ty:ty, $max:expr, $err:ty) => { - impl $($impl_generics)* $crate::__private::bitcoin_consensus_encoding::Encodable for $ty { + impl<$($impl_generics)*> $crate::__private::bitcoin_consensus_encoding::Encodable for $ty { type Encoder<'e> = $crate::VecEncoder; fn encoder(&self) -> Self::Encoder<'_> { let mut buf = ::alloc::vec::Vec::new(); @@ -275,10 +149,10 @@ macro_rules! impl_type { } } - impl $($impl_generics)* $crate::__private::bitcoin_consensus_encoding::Decodable for $ty { - type Decoder = $crate::BufferDecoder<$ty, $err>; + impl<$($impl_generics)*> $crate::__private::bitcoin_consensus_encoding::Decodable for $ty { + type Decoder = $crate::VecDecoder<$ty, $err>; fn decoder() -> Self::Decoder { - $crate::BufferDecoder::new(<$ty as $crate::codec::BaseCodec<$err>>::decode, $max) + $crate::VecDecoder::new(<$ty as $crate::codec::BaseCodec<$err>>::decode, $max) } } }; @@ -293,152 +167,247 @@ macro_rules! impl_type { $crate::impl_type!(@parse [$($impl_generics)*] $ty, $crate::MAX_SER_SIZE); }; (for[$($generic:tt)*] $($args:tt)*) => { - $crate::impl_type!(@parse [<$($generic)*>] $($args)*); + $crate::impl_type!(@parse [$($generic)*] $($args)*); }; ($($args:tt)*) => { $crate::impl_type!(@parse [] $($args)*); }; } -/// Generates `Encodable` + `Decodable` for a `BaseCodec` implementor whose -/// wire image is secret. +/// Generates `BaseCodec` + `Encodable` + `Decodable` + `From<[u8; N]>` for a +/// fixed-size byte newtype, expressed only through `from_bytes` / `as_bytes`. +/// +/// Staged through the growable [`VecEncoder`]. For a newtype whose contents +/// are secret use [`impl_sbytes!`](crate::impl_sbytes). #[macro_export] -macro_rules! impl_stype { - (@parse [$($impl_generics:tt)*] $ty:ty, $n:expr, $err:ty) => { - impl $($impl_generics)* $crate::__private::bitcoin_consensus_encoding::Encodable for $ty { - type Encoder<'e> = $crate::ArrEncoder<{ $n }>; - fn encoder(&self) -> Self::Encoder<'_> { - let mut buf = $crate::codec::ArrayBuf::<{ $n }>::new(); - <$ty as $crate::codec::BaseCodec<$err>>::encode(self, &mut buf); - $crate::ArrEncoder::new(buf) +macro_rules! impl_bytes { + // Shared by `impl_bytes!` and `impl_sbytes!`, only the encoder pair differs. + (@codec [$($g:tt)*] $ty:ty, $n:expr) => { + impl<$($g)*> $crate::codec::BaseCodec for $ty { + fn decode( + data: &mut &[u8], + ) -> Result { + $crate::codec::take::<$n>(data).map(Self::from_bytes) } - } - impl $($impl_generics)* $crate::__private::bitcoin_consensus_encoding::Decodable for $ty { - type Decoder = $crate::ArrDecoder<$ty, { $n }, $err>; - fn decoder() -> Self::Decoder { - $crate::ArrDecoder::new(<$ty as $crate::codec::BaseCodec<$err>>::decode) + fn encode(&self, buf: &mut impl $crate::codec::EncodeBuf) { + buf.extend_from_slice(self.as_bytes()); } } + + impl<$($g)*> ::core::convert::From<[u8; $n]> for $ty { + fn from(bytes: [u8; $n]) -> Self { Self::from_bytes(bytes) } + } }; - (@parse [$($impl_generics:tt)*] $ty:ty, $n:expr) => { - $crate::impl_stype!( - @parse [$($impl_generics)*] $ty, - $n, - ::core::convert::Infallible - ); - }; - (@parse [$($impl_generics:tt)*] $ty:ty) => { - ::core::compile_error!(concat!( - "impl_stype! needs the fixed width of ", - stringify!($ty), - ": write impl_stype!(", stringify!($ty), ", N)" - )); + (@parse [$($g:tt)*] $ty:ty, $n:expr) => { + $crate::impl_bytes!(@codec [$($g)*] $ty, $n); + + $crate::impl_type!(@parse [$($g)*] $ty, $n); }; (for[$($generic:tt)*] $($args:tt)*) => { - $crate::impl_stype!(@parse [<$($generic)*>] $($args)*); + $crate::impl_bytes!(@parse [$($generic)*] $($args)*); }; ($($args:tt)*) => { - $crate::impl_stype!(@parse [] $($args)*); + $crate::impl_bytes!(@parse [] $($args)*); }; } -#[cfg(test)] -mod tests { - use super::{ArrDecoder, ArrEncoder, BufferDecoder, VecEncoder, MAX_ARR_SIZE}; - use crate::codec::{ArrayBuf, DecodeError, EncodeBuf}; - use crate::prelude::*; +/// 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. +/// +/// 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) => { + impl<$($g)*> ::core::clone::Clone for $ty { + fn clone(&self) -> Self { *self } + } - use bitcoin_consensus_encoding::{Decoder, Encoder}; - use rstest::*; - use zeroize::Zeroize; + impl<$($g)*> ::core::marker::Copy for $ty {} - fn filled(fill: u8, len: usize) -> ArrayBuf { - let mut b = ArrayBuf::::new(); - b.extend_from_slice(&vec![fill; len]); - b - } + impl<$($g)*> ::core::default::Default for $ty { + fn default() -> Self { Self::from_bytes([0u8; $n]) } + } - /// Consumes the whole cursor, so `end()` sees no trailing bytes. - fn take_all(data: &mut &[u8]) -> Result, DecodeError> { - let out = data.to_vec(); - *data = &[]; - Ok(out) - } + impl<$($g)*> ::core::cmp::Eq for $ty {} - #[rstest] - fn arr_encoder_emits_written_prefix_only() { - // A short write into a wide buffer must not leak the zero padding. - let mut enc = ArrEncoder::new(filled::<64>(0xAB, 10)); - assert_eq!(enc.current_chunk(), [0xAB; 10]); - assert!(!enc.advance()); - assert_eq!(enc.current_chunk(), &[] as &[u8]); - } + impl<$($g)*> ::core::cmp::PartialEq for $ty { + fn eq(&self, other: &Self) -> bool { self.as_bytes() == other.as_bytes() } + } - /// The wipe itself. `Drop` on both types delegates straight to this, and - /// observing the freed storage directly would need `unsafe`, which the - /// workspace denies. - #[rstest] - fn arrbuf_zeroize_clears_contents_and_len() { - let mut buf = filled::<32>(0xCD, 32); - assert_eq!(buf.as_bytes(), [0xCD; 32]); - buf.zeroize(); - assert_eq!(buf.len(), 0); - assert_eq!(buf.spare(), 32); - assert_eq!(buf.as_bytes(), &[] as &[u8]); - // Re-fill and confirm the backing array really was zeroed, not just the - // length reset. - buf.extend_from_slice(&[0u8; 32]); - assert_eq!(buf.as_bytes(), [0u8; 32]); - } + impl<$($g)*> ::core::cmp::Ord for $ty { + fn cmp(&self, other: &Self) -> ::core::cmp::Ordering { + self.as_bytes().cmp(other.as_bytes()) + } + } - /// The cap is a compile-time assert, so only the accepted side is testable - /// here; `N` above the bound fails to build with "unusually large zeroized - /// buffer" wherever the encoder or decoder is instantiated. - #[rstest] - fn max_width_is_accepted() { - let enc = ArrEncoder::new(ArrayBuf::<{ MAX_ARR_SIZE }>::new()); - assert_eq!(enc.current_chunk(), &[] as &[u8]); - let dec = ArrDecoder::, { MAX_ARR_SIZE }>::new(take_all); - assert_eq!(dec.read_limit(), MAX_ARR_SIZE); - } + 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)) + } + } - #[rstest] - fn arr_decoder_roundtrips_and_bounds_reads() { - let mut dec = ArrDecoder::, 8>::new(take_all); - assert_eq!(dec.read_limit(), 8); - let mut input: &[u8] = &[1, 2, 3]; - assert!(dec.push_bytes(&mut input).unwrap_or(false)); - assert!(input.is_empty()); - assert_eq!(dec.read_limit(), 5); - assert_eq!(dec.end().unwrap_or_default(), vec![1, 2, 3]); - } + impl<$($g)*> ::core::hash::Hash for $ty { + fn hash(&self, state: &mut H) { + ::core::hash::Hash::hash(self.as_bytes(), state); + } + } - #[rstest] - fn arr_decoder_stops_at_capacity() { - let mut dec = ArrDecoder::, 4>::new(take_all); - let mut input: &[u8] = &[9; 10]; - assert!(dec.push_bytes(&mut input).unwrap_or(false)); - assert_eq!(input.len(), 6, "excess must be left for the caller"); - assert_eq!(dec.read_limit(), 0); - assert!(!dec.push_bytes(&mut input).unwrap_or(true)); - } + impl<$($g)*> ::core::convert::AsRef<[u8]> for $ty { + fn as_ref(&self) -> &[u8] { self.as_bytes() } + } - /// Both encoders redact: a `{:?}` in a panic must not print key material. - #[rstest] - fn debug_impls_redact_contents() { - let enc = ArrEncoder::new(filled::<8>(0xFF, 8)); - let dbg = format!("{enc:?}"); - assert!(!dbg.contains("255") && !dbg.contains("ff"), "{dbg}"); - assert!(dbg.contains("len: 8")); + impl<$($g)*> ::core::convert::AsRef<[u8; $n]> for $ty { + fn as_ref(&self) -> &[u8; $n] { self.as_bytes() } + } - let venc = VecEncoder::new(vec![0xFFu8; 8]); - assert!(!format!("{venc:?}").contains("255")); + impl<$($g)*> ::core::convert::From<$ty> for [u8; $n] { + fn from(val: $ty) -> Self { *val.as_bytes() } + } - let vdec = BufferDecoder::>::new(take_all, 16); - assert!(format!("{vdec:?}").contains("limit: 16")); + impl<$($g)*> $ty { + /// Returns `true` when every byte is zero. + pub fn is_null(&self) -> bool { self.as_bytes().iter().all(|&b| b == 0) } + } - let adec = ArrDecoder::, 16>::new(take_all); - assert!(format!("{adec:?}").contains("limit: 16")); - } + 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(")") + } + } + + impl<$($g)*> ::core::fmt::Display for $ty { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + for byte in self.as_bytes() { + ::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, + { + use $crate::__private::hex_conservative::DisplayHex as _; + serializer.serialize_str(&self.as_bytes().to_lower_hex_string()) + } + } + + 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)?; + <[u8; $n] as $crate::__private::hex_conservative::FromHex>::from_hex(&s) + .map(Self::from_bytes) + .map_err(D::Error::custom) + } + } + } + }; + (for[$($generic:tt)*] $($args:tt)*) => { + $crate::derive_bytes!(@parse [$($generic)*] $($args)*); + }; + ($($args:tt)*) => { + $crate::derive_bytes!(@parse [] $($args)*); + }; +} + +/// Declares a fixed-size byte newtype over `[u8; N]` with the `from_bytes` / +/// `to_bytes` / `as_bytes` accessors. +/// +/// Invokes [`impl_bytes!`](crate::impl_bytes) and +/// [`derive_bytes!`](crate::derive_bytes). A newtype that needs a validating +/// constructor, a scheme tag, or its own trait set should define itself and +/// invoke those macros manually. +#[macro_export] +macro_rules! make_bytes { + ( + $(#[$attr:meta])* + $name:ident, $n:literal + ) => { + $(#[$attr])* + #[derive($crate::TypeId)] + pub struct $name(pub [u8; $n]); + + $crate::impl_bytes!($name, $n); + + $crate::derive_bytes!($name, $n); + + impl $name { + /// Wraps raw bytes without validation. + pub const fn from_bytes(bytes: [u8; $n]) -> Self { + Self(bytes) + } + + /// Returns the inner byte array. + pub const fn to_bytes(self) -> [u8; $n] { + self.0 + } + + /// Borrows the inner byte array. + pub const fn as_bytes(&self) -> &[u8; $n] { + &self.0 + } + } + }; +} + +/// Delegates `BaseCodec`, `Hashable`, and `impl_type!` through another type. +/// +/// Decode is fallible: `$bytes` is unvalidated, so `TryFrom<$bytes>` guards +/// the operational type. Encode is not: the value is already valid, so +/// `From<&$ops> for $bytes` must exist and must be infallible, since a failing +/// encode could only emit nothing or a placeholder, corrupting the wire image. +/// +/// `$max` bounds the `impl_type!` decoder buffer to the wrapped type's own +/// maximum encoded length. For a secret wire image use +/// [`dlgt_scodec!`](crate::dlgt_scodec). +#[macro_export] +macro_rules! dlgt_codec { + // Shared by `dlgt_codec!` and `dlgt_scodec!`, only the encoder pair differs. + (@delegate [$($impl_generics:tt)*] $ops:ty => $bytes:ty, $hash:ty, $err:ty) => { + impl<$($impl_generics)*> $crate::codec::BaseCodec<$err> for $ops { + fn decode(data: &mut &[u8]) -> Result> { + let inner = <$bytes as $crate::codec::BaseCodec>::decode(data).map_err(|e| e.lift())?; + Self::try_from(inner).map_err($crate::codec::DecodeError::DecError) + } + + fn encode(&self, buf: &mut impl $crate::codec::EncodeBuf) { + $crate::codec::BaseCodec::encode(&<$bytes as ::core::convert::From<&Self>>::from(self), buf); + } + } + + impl<$($impl_generics)*> $crate::codec::Hashable for $ops { + type Hash = $hash; + + fn hash(&self) -> $hash { + $crate::codec::Hashable::hash(&<$bytes as ::core::convert::From<&Self>>::from(self)) + } + } + }; + (@parse [$($impl_generics:tt)*] $ops:ty => $bytes:ty, $hash:ty, $err:ty, $max:expr) => { + $crate::dlgt_codec!(@delegate [$($impl_generics)*] $ops => $bytes, $hash, $err); + + $crate::impl_type!(@parse [$($impl_generics)*] $ops, $max, $err); + }; + (for[$($generic:tt)*] $($args:tt)*) => { + $crate::dlgt_codec!(@parse [$($generic)*] $($args)*); + }; + ($($args:tt)*) => { + $crate::dlgt_codec!(@parse [] $($args)*); + }; } diff --git a/pkgs/types/src/hex.rs b/pkgs/types/src/hex.rs deleted file mode 100644 index 2268f3cb..00000000 --- a/pkgs/types/src/hex.rs +++ /dev/null @@ -1,228 +0,0 @@ -// -// Copyright (c) 2026-present, The Dash Core developers -// SPDX-License-Identifier: MIT -// See the accompanying file LICENSE or https://opensource.org/license/MIT -// - -//! Fixed-size byte newtype macros. - -/// Generates `BaseCodec` + `Encodable` + `Decodable` + `From<[u8; N]>` -/// for a fixed-size byte newtype that wraps `[u8; N]` and exposes -/// `as_bytes()`. -#[macro_export] -macro_rules! impl_bytes { - ($n:literal, $($name:ident),* $(,)?) => { $( - impl $crate::codec::BaseCodec for $name { - fn decode( - data: &mut &[u8], - ) -> Result { - $crate::codec::take::<$n>(data).map(|b| Self(b)) - } - - fn encode(&self, buf: &mut impl $crate::codec::EncodeBuf) { - buf.extend_from_slice(self.as_bytes()); - } - } - - $crate::impl_type!($name); - - impl From<[u8; $n]> for $name { - fn from(bytes: [u8; $n]) -> Self { Self(bytes) } - } - )* }; -} - -/// Generates the consensus encoding traits for a fixed-size byte newtype with -/// secret contents. -#[macro_export] -macro_rules! impl_sbyte { - ($n:literal, $($name:ident),* $(,)?) => { $( - impl $crate::codec::BaseCodec for $name { - fn decode( - data: &mut &[u8], - ) -> Result { - $crate::codec::take::<$n>(data).map(|b| Self(b)) - } - - fn encode(&self, buf: &mut impl $crate::codec::EncodeBuf) { - buf.extend_from_slice(self.as_bytes()); - } - } - - $crate::impl_stype!($name, $n); - - impl From<[u8; $n]> for $name { - fn from(bytes: [u8; $n]) -> Self { Self(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`, `AsRef<[u8]>`, `AsRef<[u8; N]>`, `From for [u8; N]`, and the -/// hex `serde` pair. -#[macro_export] -macro_rules! derive_bytes { - (@parse [$($g:tt)*] $ty:ty, $n: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() } - } - - #[cfg(feature = "serde")] - impl<$($g)*> ::serde::Serialize for $ty { - fn serialize(&self, serializer: Z) -> Result { - use $crate::__private::hex_conservative::DisplayHex as _; - serializer.serialize_str(&self.as_bytes().to_lower_hex_string()) - } - } - - #[cfg(feature = "serde")] - impl<'de, $($g)*> ::serde::Deserialize<'de> for $ty { - fn deserialize>(deserializer: D) -> Result { - use ::serde::de::Error as _; - let s = <::alloc::string::String as ::serde::Deserialize>::deserialize(deserializer)?; - <[u8; $n] as $crate::__private::hex_conservative::FromHex>::from_hex(&s) - .map(Self::from_bytes) - .map_err(D::Error::custom) - } - } - }; - (for[$($generic:tt)*] $($args:tt)*) => { - $crate::derive_bytes!(@parse [$($generic)*] $($args)*); - }; - ($($args:tt)*) => { - $crate::derive_bytes!(@parse [] $($args)*); - }; -} - -/// Generates a fixed-size byte newtype with consensus encoding traits and -/// standard trait implementations. -#[macro_export] -macro_rules! make_bytes { - ( - $(#[$attr:meta])* - $name:ident, $n:literal - ) => { - $(#[$attr])* - #[derive($crate::TypeId)] - pub struct $name(pub [u8; $n]); - - $crate::impl_bytes!($n, $name); - - $crate::derive_bytes!($name, $n); - - impl $name { - /// Wraps raw bytes without validation. - pub const fn from_bytes(bytes: [u8; $n]) -> Self { - Self(bytes) - } - - /// Returns the inner byte array. - pub const fn to_bytes(self) -> [u8; $n] { - self.0 - } - - /// Borrows the inner byte array. - pub const fn as_bytes(&self) -> &[u8; $n] { - &self.0 - } - - /// Returns `true` when every byte is zero. - pub fn is_null(&self) -> bool { - self.0.iter().all(|&b| b == 0) - } - } - - impl core::fmt::Debug for $name { - fn fmt( - &self, - f: &mut core::fmt::Formatter<'_>, - ) -> core::fmt::Result { - write!(f, "{}(", stringify!($name))?; - for byte in &self.0 { - write!(f, "{:02x}", byte)?; - } - write!(f, ")") - } - } - - impl core::fmt::Display for $name { - fn fmt( - &self, - f: &mut core::fmt::Formatter<'_>, - ) -> core::fmt::Result { - for byte in &self.0 { - write!(f, "{:02x}", byte)?; - } - Ok(()) - } - } - }; -} - -/// Wire-order hex for `Vec` and fixed-size byte arrays. -/// -/// Use with `#[serde(with = "dash_types::serialize::hex")]` on -/// `Vec` fields. For fixed-size byte arrays use a sub-module -/// (e.g. `hex::w16` for `[u8; 16]`). -#[cfg(feature = "serde")] -pub mod serde { - use crate::prelude::*; - - use hex_conservative::{DisplayHex, FromHex}; - - /// Serializes bytes as a wire-order hex string. - pub fn serialize(data: &[u8], serializer: S) -> Result { - serializer.serialize_str(&data.to_lower_hex_string()) - } - - /// Deserializes a hex string into bytes. - pub fn deserialize<'de, D: ::serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { - let s = ::deserialize(deserializer)?; - Vec::::from_hex(&s).map_err(::serde::de::Error::custom) - } -} diff --git a/pkgs/types/src/lib.rs b/pkgs/types/src/lib.rs index 2693b6a4..9699b3b7 100644 --- a/pkgs/types/src/lib.rs +++ b/pkgs/types/src/lib.rs @@ -15,19 +15,22 @@ extern crate std; #[allow(unused_macros, reason = "used by feature-gated submodules")] mod adapters; +mod compact; mod entity; -mod hex; mod macros; #[allow(unused_imports, reason = "ergonomic shim, exports may be unused")] mod prelude; +mod secret; mod uint; pub mod codec; #[cfg(feature = "serde")] pub mod serialize; +pub use compact::CompactSize; pub use dash_types_marker::{TypeId, Unencodable}; -pub use entity::{ArrDecoder, ArrEncoder, BufferDecoder, VecEncoder, MAX_ARR_SIZE, MAX_SER_SIZE}; +pub use entity::{VecDecoder, VecEncoder, MAX_SER_SIZE}; +pub use secret::{qtypestr, ArrDecoder, ArrEncoder, ArrayBuf, MAX_ARR_SIZE}; #[doc(hidden)] pub mod __private { @@ -37,4 +40,8 @@ pub mod __private { pub use bitcoin_consensus_encoding; #[cfg(feature = "serde")] pub use hex_conservative; + #[cfg(feature = "serde")] + pub use serde; + pub use subtle; + pub use zeroize; } diff --git a/pkgs/types/src/macros.rs b/pkgs/types/src/macros.rs index bf6c4dfc..d497a9a8 100644 --- a/pkgs/types/src/macros.rs +++ b/pkgs/types/src/macros.rs @@ -6,6 +6,29 @@ //! Shared macro definitions. +/// Emits its body only when *this* crate has the `serde` feature. +/// +/// `#[cfg(feature = "serde")]` written inside an exported macro resolves +/// against the invoking crate, which doesn't need have a `serde` 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 = "serde")] +#[doc(hidden)] +#[macro_export] +macro_rules! cfg_serde { + ($($item:tt)*) => { $($item)* }; +} + +#[cfg(not(feature = "serde"))] +#[doc(hidden)] +#[macro_export] +macro_rules! cfg_serde { + ($($item:tt)*) => {}; +} + /// Maps enum variants to integer constants and display strings. /// /// Generates the enum definition, integer mapping (via `NumCodec` or inherent @@ -227,10 +250,10 @@ macro_rules! enum_map { (@display_catch_all $enum:ident, $catch_all:ident { $($variant:ident = $display:expr),+ }) => { - impl core::fmt::Display for $enum { + impl ::core::fmt::Display for $enum { fn fmt( - &self, f: &mut core::fmt::Formatter<'_>, - ) -> core::fmt::Result { + &self, f: &mut ::core::fmt::Formatter<'_>, + ) -> ::core::fmt::Result { match self { $(Self::$variant => f.write_str($display),)+ Self::$catch_all(v) => write!(f, "unknown({v})"), @@ -242,10 +265,10 @@ macro_rules! enum_map { (@display $enum:ident { $($variant:ident = $display:expr),+ }) => { - impl core::fmt::Display for $enum { + impl ::core::fmt::Display for $enum { fn fmt( - &self, f: &mut core::fmt::Formatter<'_>, - ) -> core::fmt::Result { + &self, f: &mut ::core::fmt::Formatter<'_>, + ) -> ::core::fmt::Result { match self { $(Self::$variant => f.write_str($display),)+ } @@ -259,73 +282,36 @@ macro_rules! enum_map { #[macro_export] macro_rules! type_cvrt { (@parse [$($impl_generics:tt)*] From<$src:ty> for $dst:ty, |$v:ident| $body:expr) => { - impl $($impl_generics)* core::convert::From<&$src> for $dst { + impl<$($impl_generics)*> ::core::convert::From<&$src> for $dst { fn from($v: &$src) -> Self { $body } } - impl $($impl_generics)* core::convert::From<$src> for $dst { + impl<$($impl_generics)*> ::core::convert::From<$src> for $dst { fn from(v: $src) -> Self { Self::from(&v) } } }; (@parse [$($impl_generics:tt)*] TryFrom<$src:ty> for $dst:ty, $err:ty, |$v:ident| $body:expr) => { - impl $($impl_generics)* core::convert::TryFrom<&$src> for $dst { + impl<$($impl_generics)*> ::core::convert::TryFrom<&$src> for $dst { type Error = $err; fn try_from($v: &$src) -> Result { $body } } - impl $($impl_generics)* core::convert::TryFrom<$src> for $dst { + impl<$($impl_generics)*> ::core::convert::TryFrom<$src> for $dst { type Error = $err; fn try_from(v: $src) -> Result { Self::try_from(&v) } } }; - ($($args:tt)*) => { - $crate::type_cvrt!(@parse [] $($args)*); - }; -} - -/// Delegates `BaseCodec`, `Hashable`, and `impl_type!` through another type. -/// -/// Decoding is fallible (`$bytes` is unvalidated, so `TryFrom` guards the -/// operational type), encoding is not: the operational type is already valid, -/// so `From<&$ops> for $bytes` must exist. -/// -/// An encode direction that could fail would have to either emit nothing or -/// hash a placeholder, both of which silently corrupt the wire image. -/// -/// `$max` bounds the `impl_type!` decoder buffer to the wrapped type's own -/// maximum encoded length. -#[macro_export] -macro_rules! dlgt_codec { - (@parse [$($impl_generics:tt)*] $ops:ty => $bytes:ty, $hash:ty, $err:ty, $max:expr) => { - impl $($impl_generics)* $crate::codec::BaseCodec<$err> for $ops { - fn decode(data: &mut &[u8]) -> Result> { - let inner = <$bytes as $crate::codec::BaseCodec>::decode(data).map_err(|e| e.lift())?; - Self::try_from(inner).map_err($crate::codec::DecodeError::DecError) - } - - fn encode(&self, buf: &mut impl $crate::codec::EncodeBuf) { - <$bytes as core::convert::From<&Self>>::from(self).encode(buf); - } - } - - impl $($impl_generics)* $crate::codec::Hashable for $ops { - type Hash = $hash; - - fn hash(&self) -> $hash { - $crate::codec::Hashable::hash(&<$bytes as core::convert::From<&Self>>::from(self)) - } - } - - $crate::impl_type!(@parse [$($impl_generics)*] $ops, $max, $err); + (for[$($generic:tt)*] $($args:tt)*) => { + $crate::type_cvrt!(@parse [$($generic)*] $($args)*); }; ($($args:tt)*) => { - $crate::dlgt_codec!(@parse [] $($args)*); + $crate::type_cvrt!(@parse [] $($args)*); }; } diff --git a/pkgs/types/src/secret.rs b/pkgs/types/src/secret.rs new file mode 100644 index 00000000..3e2ef1c5 --- /dev/null +++ b/pkgs/types/src/secret.rs @@ -0,0 +1,516 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Secret-holding codec implementation. + +use crate::codec::{DecodeError, EncodeBuf}; + +use bitcoin_consensus_encoding::{Decoder, Encoder}; +use zeroize::Zeroize; + +use core::convert::Infallible; +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 +/// it goes out of scope. To hold secret material, wrap it in `Zeroizing` or +/// move it into [`ArrEncoder`] or [`ArrDecoder`], which wipe on drop. +/// +/// # Panics +/// +/// Writing more than `N` bytes (via the [`EncodeBuf`] impl) panics with an +/// index-out-of-bounds. +#[derive(Clone)] +pub struct ArrayBuf { + buf: [u8; N], + len: usize, +} + +impl ArrayBuf { + /// Creates an empty buffer. + pub const fn new() -> Self { + Self { buf: [0u8; N], len: 0 } + } + + /// Borrows the written bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.buf[..self.len] + } + + /// Returns `true` when nothing has been written. + pub const fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Number of bytes written so far. + pub const fn len(&self) -> usize { + self.len + } + + /// Remaining writable capacity. + pub const fn spare(&self) -> usize { + N - self.len + } + + /// Returns the written bytes as a fixed array. + /// + /// # Panics + /// + /// Panics if exactly `N` bytes were not written. + pub fn into_array(self) -> [u8; N] { + assert!(self.len == N, "expected {N} bytes, wrote {}", self.len); + self.buf + } +} + +impl fmt::Debug for ArrayBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ArrayBuf").field("len", &self.len).finish() + } +} + +impl Default for ArrayBuf { + fn default() -> Self { + Self::new() + } +} + +impl EncodeBuf for ArrayBuf { + fn push(&mut self, byte: u8) { + self.buf[self.len] = byte; + self.len += 1; + } + + fn extend_from_slice(&mut self, data: &[u8]) { + self.buf[self.len..self.len + data.len()].copy_from_slice(data); + self.len += data.len(); + } +} + +impl Zeroize for ArrayBuf { + fn zeroize(&mut self) { + self.buf.zeroize(); + self.len = 0; + } +} + +/// An encoder for values whose encoded width is bounded at compile time. +/// +/// Costs a byte-wise volatile write per byte of `N`, so it suits key material +/// and other small fixed records, not block-sized payloads. [`MAX_ARR_SIZE`] +/// caps `N` due to performance cost. +pub struct ArrEncoder { + data: ArrayBuf, + done: bool, +} + +impl ArrEncoder { + /// Wraps a filled buffer. + /// + /// Refuses to compile when `N` exceeds [`MAX_ARR_SIZE`]. + pub const fn new(data: ArrayBuf) -> Self { + const { assert!(N <= MAX_ARR_SIZE, "unusually large zeroized buffer") }; + Self { data, done: false } + } +} + +impl fmt::Debug for ArrEncoder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ArrEncoder") + .field("len", &self.data.len()) + .field("done", &self.done) + .finish() + } +} + +impl Drop for ArrEncoder { + fn drop(&mut self) { + self.data.zeroize(); + } +} + +impl Encoder for ArrEncoder { + fn current_chunk(&self) -> &[u8] { + if self.done { + &[] + } else { + self.data.as_bytes() + } + } + + fn advance(&mut self) -> bool { + if self.done { + false + } else { + self.done = true; + false + } + } +} + +/// A decoder for values whose encoded width is bounded by `N`. +pub struct ArrDecoder { + buf: ArrayBuf, + decode_fn: fn(&mut &[u8]) -> Result>, +} + +impl ArrDecoder { + /// Creates a decoder that accepts at most `N` bytes. + /// + /// Refuses to compile when `N` exceeds [`MAX_ARR_SIZE`]. + pub const fn new(decode_fn: fn(&mut &[u8]) -> Result>) -> Self { + const { assert!(N <= MAX_ARR_SIZE, "unusually large zeroized buffer") }; + Self { + buf: ArrayBuf::new(), + decode_fn, + } + } +} + +impl fmt::Debug for ArrDecoder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ArrDecoder") + .field("buf_len", &self.buf.len()) + .field("limit", &N) + .finish() + } +} + +impl Drop for ArrDecoder { + fn drop(&mut self) { + self.buf.zeroize(); + } +} + +impl Decoder for ArrDecoder { + type Output = T; + type Error = DecodeError; + + fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result { + let remaining = self.buf.spare(); + if remaining == 0 { + return Ok(false); + } + let take = bytes.len().min(remaining); + self.buf.extend_from_slice(&bytes[..take]); + *bytes = &bytes[take..]; + Ok(true) + } + + fn end(self) -> Result { + // Borrow rather than destructure: `Drop` wipes the buffer on the way out, + // including on the early return below. + let mut cursor = self.buf.as_bytes(); + let result = (self.decode_fn)(&mut cursor)?; + if !cursor.is_empty() { + return Err(DecodeError::TrailingBytes { + remaining: cursor.len(), + }); + } + Ok(result) + } + + fn read_limit(&self) -> usize { + self.buf.spare() + } +} + +/// Generates `Encodable` + `Decodable` for a `BaseCodec` implementor whose +/// wire image is secret. +/// +/// Stages through the wiping [`ArrEncoder`]/[`ArrDecoder`] pair, both sized by +/// `$n` and capped at [`MAX_ARR_SIZE`]. For public material use +/// [`impl_type!`](crate::impl_type), the same generator over the growable +/// pair. +#[macro_export] +macro_rules! impl_stype { + (@parse [$($impl_generics:tt)*] $ty:ty, $n:expr, $err:ty) => { + impl<$($impl_generics)*> $crate::__private::bitcoin_consensus_encoding::Encodable for $ty { + type Encoder<'e> = $crate::ArrEncoder<{ $n }>; + fn encoder(&self) -> Self::Encoder<'_> { + let mut buf = $crate::ArrayBuf::<{ $n }>::new(); + <$ty as $crate::codec::BaseCodec<$err>>::encode(self, &mut buf); + $crate::ArrEncoder::new(buf) + } + } + + impl<$($impl_generics)*> $crate::__private::bitcoin_consensus_encoding::Decodable for $ty { + type Decoder = $crate::ArrDecoder<$ty, { $n }, $err>; + fn decoder() -> Self::Decoder { + $crate::ArrDecoder::new(<$ty as $crate::codec::BaseCodec<$err>>::decode) + } + } + }; + (@parse [$($impl_generics:tt)*] $ty:ty, $n:expr) => { + $crate::impl_stype!( + @parse [$($impl_generics)*] $ty, + $n, + ::core::convert::Infallible + ); + }; + (@parse [$($impl_generics:tt)*] $ty:ty) => { + ::core::compile_error!(concat!( + "impl_stype! needs the fixed width of ", stringify!($ty), ": write impl_stype!(", stringify!($ty), ", N)" + )); + }; + (for[$($generic:tt)*] $($args:tt)*) => { + $crate::impl_stype!(@parse [$($generic)*] $($args)*); + }; + ($($args:tt)*) => { + $crate::impl_stype!(@parse [] $($args)*); + }; +} + +/// The secret counterpart to [`impl_bytes!`](crate::impl_bytes), for a +/// fixed-size byte newtype whose contents are key material. +/// +/// Same `BaseCodec` and `From<[u8; N]>`, staged through the wiping +/// [`ArrEncoder`] rather than the growable [`VecEncoder`](crate::VecEncoder). +#[macro_export] +macro_rules! impl_sbytes { + (@parse [$($g:tt)*] $ty:ty, $n:expr) => { + $crate::impl_bytes!(@codec [$($g)*] $ty, $n); + + $crate::impl_stype!(@parse [$($g)*] $ty, $n); + }; + (for[$($generic:tt)*] $($args:tt)*) => { + $crate::impl_sbytes!(@parse [$($generic)*] $($args)*); + }; + ($($args:tt)*) => { + $crate::impl_sbytes!(@parse [] $($args)*); + }; +} + +/// 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. +/// +/// Same delegation through `$bytes`, staged through +/// [`impl_stype!`](crate::impl_stype)'s wiping pair rather than the growable +/// one, which would strand the plaintext in a heap buffer nothing wipes. +/// +/// `$n` bounds the encoded width rather than fixing it: the staging buffer is +/// an [`ArrayBuf<$n>`](crate::ArrayBuf), so a narrower image is emitted as +/// written and a wider one panics on the overflowing write. +#[macro_export] +macro_rules! dlgt_scodec { + (@parse [$($impl_generics:tt)*] $ops:ty => $bytes:ty, $hash:ty, $err:ty, $n:expr) => { + $crate::dlgt_codec!(@delegate [$($impl_generics)*] $ops => $bytes, $hash, $err); + + $crate::impl_stype!(@parse [$($impl_generics)*] $ops, $n, $err); + }; + (for[$($generic:tt)*] $($args:tt)*) => { + $crate::dlgt_scodec!(@parse [$($generic)*] $($args)*); + }; + ($($args:tt)*) => { + $crate::dlgt_scodec!(@parse [] $($args)*); + }; +} + +#[cfg(test)] +mod tests { + use super::{qtypestr, ArrDecoder, ArrEncoder, ArrayBuf, MAX_ARR_SIZE}; + use crate::codec::{DecodeError, EncodeBuf}; + use crate::prelude::*; + + use bitcoin_consensus_encoding::{Decoder, Encoder}; + 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]); + b + } + + /// Consumes the whole cursor, so `end()` sees no trailing bytes. + fn take_all(data: &mut &[u8]) -> Result, DecodeError> { + let out = data.to_vec(); + *data = &[]; + Ok(out) + } + + #[rstest] + fn arr_encoder_emits_written_prefix_only() { + // A short write into a wide buffer must not leak the zero padding. + let mut enc = ArrEncoder::new(filled::<64>(0xAB, 10)); + assert_eq!(enc.current_chunk(), [0xAB; 10]); + assert!(!enc.advance()); + assert_eq!(enc.current_chunk(), &[] as &[u8]); + } + + /// The wipe itself. `Drop` on both types delegates straight to this, and + /// observing the freed storage directly would need `unsafe`, which the + /// workspace denies. + #[rstest] + fn arrbuf_zeroize_clears_contents_and_len() { + let mut buf = filled::<32>(0xCD, 32); + assert_eq!(buf.as_bytes(), [0xCD; 32]); + buf.zeroize(); + assert_eq!(buf.len(), 0); + assert_eq!(buf.spare(), 32); + assert_eq!(buf.as_bytes(), &[] as &[u8]); + } + + /// The cap is a compile-time assert, so only the accepted side is testable + /// here; `N` above the bound fails to build with "unusually large zeroized + /// buffer" wherever the encoder or decoder is instantiated. + #[rstest] + fn max_width_is_accepted() { + let enc = ArrEncoder::new(ArrayBuf::<{ MAX_ARR_SIZE }>::new()); + assert_eq!(enc.current_chunk(), &[] as &[u8]); + let dec = ArrDecoder::, { MAX_ARR_SIZE }>::new(take_all); + assert_eq!(dec.read_limit(), MAX_ARR_SIZE); + } + + #[rstest] + fn arr_decoder_roundtrips_and_bounds_reads() { + let mut dec = ArrDecoder::, 8>::new(take_all); + assert_eq!(dec.read_limit(), 8); + let mut input: &[u8] = &[1, 2, 3]; + assert!(dec.push_bytes(&mut input).unwrap_or(false)); + assert!(input.is_empty()); + assert_eq!(dec.read_limit(), 5); + assert_eq!(dec.end().unwrap_or_default(), vec![1, 2, 3]); + } + + #[rstest] + fn arr_decoder_stops_at_capacity() { + let mut dec = ArrDecoder::, 4>::new(take_all); + let mut input: &[u8] = &[9; 10]; + assert!(dec.push_bytes(&mut input).unwrap_or(false)); + assert_eq!(input.len(), 6, "excess must be left for the caller"); + assert_eq!(dec.read_limit(), 0); + assert!(!dec.push_bytes(&mut input).unwrap_or(true)); + } + + /// Both encoders redact: a `{:?}` in a panic must not print key material. + #[rstest] + fn debug_impls_redact_contents() { + let enc = ArrEncoder::new(filled::<8>(0xFF, 8)); + let dbg = format!("{enc:?}"); + assert!(!dbg.contains("255") && !dbg.contains("ff"), "{dbg}"); + assert!(dbg.contains("len: 8")); + + 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); + } +} diff --git a/pkgs/types/src/serialize.rs b/pkgs/types/src/serialize.rs index d661dabf..f89ffa73 100644 --- a/pkgs/types/src/serialize.rs +++ b/pkgs/types/src/serialize.rs @@ -6,34 +6,30 @@ //! Reusable serde helpers for `#[serde(with = "...")]`. -// nosemgrep: use-pub-roots-only -pub use crate::hex::serde as hex; - -/// UTF-8 serde for `Vec` fields that hold text. -pub mod utf8 { +/// Wire-order hex for `Vec`. +pub mod hex { use crate::prelude::*; - use core::str::from_utf8; + use hex_conservative::{DisplayHex, FromHex}; - /// Serializes bytes as a UTF-8 string. + /// Serializes bytes as a wire-order hex string. /// /// # Errors /// - /// Returns a serialization error when bytes are not valid UTF-8. + /// Returns a serialization error when the serializer rejects the string. pub fn serialize(data: &[u8], serializer: S) -> Result { - let s = from_utf8(data).map_err(::serde::ser::Error::custom)?; - serializer.serialize_str(s) + serializer.serialize_str(&data.to_lower_hex_string()) } - /// Deserializes a string into bytes. + /// Deserializes a hex string into bytes. /// /// # Errors /// - /// Returns a deserialization error when the input is not - /// a valid string. + /// Returns a deserialization error when the input is not a string, or when + /// it is not valid hex. pub fn deserialize<'de, D: ::serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { let s = ::deserialize(deserializer)?; - Ok(s.into_bytes()) + Vec::::from_hex(&s).map_err(::serde::de::Error::custom) } } @@ -67,3 +63,30 @@ pub mod str_u64 { d.deserialize_any(Visitor) } } + +/// UTF-8 serde for `Vec` fields that hold text. +pub mod utf8 { + use crate::prelude::*; + + use core::str::from_utf8; + + /// Serializes bytes as a UTF-8 string. + /// + /// # Errors + /// + /// Returns a serialization error when bytes are not valid UTF-8. + pub fn serialize(data: &[u8], serializer: S) -> Result { + let s = from_utf8(data).map_err(::serde::ser::Error::custom)?; + serializer.serialize_str(s) + } + + /// Deserializes a string into bytes. + /// + /// # Errors + /// + /// Returns a deserialization error when the input is not a valid string. + pub fn deserialize<'de, D: ::serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = ::deserialize(deserializer)?; + Ok(s.into_bytes()) + } +} diff --git a/pkgs/types/src/uint.rs b/pkgs/types/src/uint.rs index b0727f90..c9007db4 100644 --- a/pkgs/types/src/uint.rs +++ b/pkgs/types/src/uint.rs @@ -40,25 +40,25 @@ macro_rules! impl_num { $crate::impl_type!($name); - #[cfg(feature = "serde")] - impl ::serde::Serialize for $name { - fn serialize( - &self, serializer: S, - ) -> Result { - ::serde::Serialize::serialize( - &>::to_base(self), - serializer, - ) + $crate::cfg_serde! { + impl $crate::__private::serde::Serialize for $name { + fn serialize( + &self, serializer: S, + ) -> Result { + $crate::__private::serde::Serialize::serialize( + &>::to_base(self), + serializer, + ) + } } - } - #[cfg(feature = "serde")] - impl<'de> ::serde::Deserialize<'de> for $name { - fn deserialize>( - deserializer: D, - ) -> Result { - <$uint as ::serde::Deserialize>::deserialize(deserializer) - .map(>::from_base) + impl<'de> $crate::__private::serde::Deserialize<'de> for $name { + fn deserialize>( + deserializer: D, + ) -> Result { + <$uint as $crate::__private::serde::Deserialize>::deserialize(deserializer) + .map(>::from_base) + } } } }; @@ -114,15 +114,15 @@ macro_rules! make_num { } } - impl core::fmt::Debug for $name { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + impl ::core::fmt::Debug for $name { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { write!(f, "{}({})", stringify!($name), self.0) } } - impl core::fmt::Display for $name { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - core::fmt::Display::fmt(&self.0, f) + impl ::core::fmt::Display for $name { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + ::core::fmt::Display::fmt(&self.0, f) } } };