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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions pkgs/dev/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ std = [
"hex-conservative/std",
]
full = ["std", "serde"]
serde = ["dep:serde", "dash-primitives/serde", "dash-types/serde"]
serde = [
"dep:ciborium",
"dep:serde",
"dash-primitives/serde",
"dash-types/serde",
]
bin = [
"full",
"dep:chrono",
"dep:ciborium",
"dep:clap",
"dep:dash-num",
"dep:dash-params",
Expand Down
20 changes: 20 additions & 0 deletions pkgs/dev/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,23 @@ pub fn arr_from_hex<const N: usize>(s: &str) -> [u8; N] {
pub fn vec_from_hex(s: &str) -> Vec<u8> {
decode_to_vec(s).unwrap_or_else(|e| panic!("bad hex: {e}"))
}

/// Asserts `value` serializes to CBOR as the byte string `wire` and reads back.
///
/// # Panics
///
/// Panics on an encoding failure, a mismatched encoding, or a round-trip
/// mismatch.
#[cfg(all(feature = "std", feature = "serde"))]
pub fn assert_cbor_raw<T>(value: &T, wire: &[u8])
where
T: ::serde::Serialize + ::serde::de::DeserializeOwned + PartialEq + core::fmt::Debug,
{
let (mut got, mut want) = (Vec::new(), Vec::new());
ciborium::into_writer(value, &mut got).unwrap_or_else(|e| panic!("to_cbor: {e}"));
ciborium::into_writer(&ciborium::Value::Bytes(wire.to_vec()), &mut want).unwrap_or_else(|e| panic!("to_cbor: {e}"));
assert_eq!(got, want, "{value:?} is not a {}-byte cbor byte string", wire.len());

let restored: T = ciborium::from_reader(got.as_slice()).unwrap_or_else(|e| panic!("from_cbor: {e}"));
assert_eq!(restored, *value, "cbor round-trip");
}
1 change: 1 addition & 0 deletions pkgs/dev/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ cfg_if::cfg_if! {

pub use __private::Value;
pub use corpus::{assert_serde_rt, Corpus};
pub use encode::assert_cbor_raw;
pub use json::{assert_json_rt, from_json, from_json_slice, json_rejects, to_json};
}
}
9 changes: 9 additions & 0 deletions pkgs/num/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Changed

- `HashBlob`, `make_hash!` types and `Arith256` serialize as their little-endian storage bytes for machine-readable
formats, JSON retains hex encoded big-endian string encoding.

### Removed

- `ParseHexError` has moved to `dash-types` as `dash_types::ParseHexError`.

## [0.1.0-beta] - 2026-09-14

- Initial release.
Expand Down
25 changes: 7 additions & 18 deletions pkgs/num/src/arith256.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

//! 256-bit unsigned arithmetic integer.

use crate::{Hash256, ParseHexError};
use crate::Hash256;

use dash_types::Numeric;
use dash_types::{Numeric, ParseHexError};

use core::cmp::Ordering;
use core::fmt;
Expand All @@ -23,6 +23,11 @@ use core::str::FromStr;
/// Stored as two `u128` limbs where `lo` holds bits \[0..128) and `hi` holds
/// bits \[128..256).
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "serde",
derive(::serde::Serialize, ::serde::Deserialize),
serde(from = "Hash256", into = "Hash256")
)]
pub struct Arith256 {
lo: u128,
hi: u128,
Expand Down Expand Up @@ -703,19 +708,3 @@ impl ShrAssign<u32> for Arith256 {
*self = *self >> rhs;
}
}

#[cfg(feature = "serde")]
impl serde::Serialize for Arith256 {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let h = Hash256::from(*self);
h.serialize(serializer)
}
}

#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Arith256 {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let h = Hash256::deserialize(deserializer)?;
Ok(Self::from(h))
}
}
4 changes: 2 additions & 2 deletions pkgs/num/src/compact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

//! Compact difficulty target encoding.

use crate::{Arith256, ParseHexError};
use crate::Arith256;

#[cfg(feature = "codec")]
use dash_types::impl_num;
use dash_types::Numeric;
use dash_types::{Numeric, ParseHexError};

use core::fmt;
use core::str::FromStr;
Expand Down
68 changes: 14 additions & 54 deletions pkgs/num/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,21 @@

//! Fixed-size opaque hash blob types.

use dash_types::__private::__write_hex as write_hex;
#[cfg(feature = "codec")]
use dash_types::impl_type;
use dash_types::{type_cvrt, Numeric};
use hex_conservative::{BytesToHexIter, Case, HexSliceToBytesIter};
#[cfg(feature = "serde")]
use dash_types::serialize::hex as serde_hex;
use dash_types::{type_cvrt, Numeric, ParseHexError};
use hex_conservative::{Case, HexSliceToBytesIter};

use core::fmt::{self, Write as _};
use core::fmt;
use core::hash::Hash;
use core::str::FromStr;

/// Whitespace skipped before a hex prefix.
const WHITESPACE: [char; 6] = [' ', '\x0c', '\n', '\r', '\t', '\x0b'];

/// Error returned when parsing a hex string fails.
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseHexError {
/// The hex string has an odd number of characters.
OddLength,
/// The hex character count does not match the expected length.
InvalidLength {
/// Hex characters the target type accepts, twice its byte width.
expected: usize,
/// Hex characters supplied, after any prefix was stripped.
got: usize,
},
/// A non-hex character was encountered.
InvalidChar(u8),
}

impl fmt::Display for ParseHexError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::OddLength => write!(f, "hex string has odd length"),
Self::InvalidLength { expected, got } => {
write!(f, "expected {expected} hex chars, got {got}")
}
Self::InvalidChar(c) => {
write!(f, "invalid hex character: {:#04x}", c)
}
}
}
}

impl core::error::Error for ParseHexError {}

/// Fixed-size opaque hash blob stored in little-endian byte order.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct HashBlob<const N: usize>([u8; N]);
Expand Down Expand Up @@ -177,34 +147,22 @@ impl<const N: usize> Default for HashBlob<N> {
/// Reversed hex (big-endian display, consensus format).
impl<const N: usize> fmt::Display for HashBlob<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_hex(self.as_bytes(), Case::Lower, f)
write_hex(self.as_bytes(), true, Case::Lower, f)
}
}

/// Big-endian hex, `N * 2` chars zero-padded.
impl<const N: usize> fmt::LowerHex for HashBlob<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_hex(self.as_bytes(), Case::Lower, f)
write_hex(self.as_bytes(), true, Case::Lower, f)
}
}

/// Big-endian hex (uppercase), `N * 2` chars zero-padded.
impl<const N: usize> fmt::UpperHex for HashBlob<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_hex(self.as_bytes(), Case::Upper, f)
}
}

/// Writes little-endian storage as big-endian hex, honouring `{:#x}`.
fn write_hex(bytes: &[u8], case: Case, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.write_str(if case == Case::Lower { "0x" } else { "0X" })?;
}
for [hi, lo] in BytesToHexIter::new(bytes.iter().rev().copied(), case) {
f.write_char(char::from(hi))?;
f.write_char(char::from(lo))?;
write_hex(self.as_bytes(), true, Case::Upper, f)
}
Ok(())
}

impl<const N: usize> fmt::Debug for HashBlob<N> {
Expand Down Expand Up @@ -236,18 +194,20 @@ impl<const N: usize> AsRef<[u8; N]> for HashBlob<N> {
}
}

/// Hex encoded big-endian string or little-endian bytes.
#[cfg(feature = "serde")]
impl<const N: usize> ::serde::Serialize for HashBlob<N> {
fn serialize<S: ::serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&::alloc::format!("{}", self))
serde_hex::serialize_as(self.as_bytes(), self, serializer)
}
}

/// Hex encoded big-endian string through [`HashBlob::from_hex`], or `N`
/// little-endian bytes.
#[cfg(feature = "serde")]
impl<'de, const N: usize> ::serde::Deserialize<'de> for HashBlob<N> {
fn deserialize<D: ::serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = <::alloc::string::String as ::serde::Deserialize>::deserialize(deserializer)?;
Self::from_hex(&s).map_err(::serde::de::Error::custom)
serde_hex::deserialize_as(deserializer, |s: &str| Self::from_hex(s).map(|h| h.0)).map(Self)
}
}

Expand Down
2 changes: 1 addition & 1 deletion pkgs/num/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub mod __private {

pub use crate::arith256::Arith256;
pub use crate::compact::{CompactTarget, DecodedTarget};
pub use crate::hash::{Hash160, Hash256, HashBlob, ParseHexError};
pub use crate::hash::{Hash160, Hash256, HashBlob};

// TODO(kwvg): move to mod __deps with crate-level export
pub use dash_types::Numeric;
4 changes: 2 additions & 2 deletions pkgs/num/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ macro_rules! make_hash {

/// Parse from a big-endian hex string.
#[inline]
pub fn from_hex(s: &str) -> Result<Self, $crate::ParseHexError> {
pub fn from_hex(s: &str) -> Result<Self, $crate::__private::dash_types::ParseHexError> {
<$crate::HashBlob<$len>>::from_hex(s).map(Self)
}
}
Expand Down Expand Up @@ -191,7 +191,7 @@ macro_rules! make_hash {
}

impl ::core::str::FromStr for $name {
type Err = $crate::ParseHexError;
type Err = $crate::__private::dash_types::ParseHexError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_hex(s)
Expand Down
4 changes: 2 additions & 2 deletions pkgs/num/tests/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

#![expect(clippy::unwrap_used, reason = "test code")]

use dash_num::{Hash160, Hash256, ParseHexError};
use dash_types::Numeric;
use dash_num::{Hash160, Hash256};
use dash_types::{Numeric, ParseHexError};
use hex_literal::hex;
use rstest::*;

Expand Down
9 changes: 8 additions & 1 deletion pkgs/num/tests/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

//! Serde roundtrip tests for all types.

use dash_dev::{assert_json_rt, from_json, json_rejects, to_json};
use dash_dev::{assert_cbor_raw, assert_json_rt, from_json, json_rejects, to_json};
use dash_num::{Arith256, CompactTarget, Hash160, Hash256};
use dash_types::Numeric;
use hex_literal::hex;
Expand All @@ -21,6 +21,13 @@ fn hash256_json_roundtrip() {
assert_json_rt(&Hash256::from_lendian([0xff; 32]));
}

#[test]
fn hash_cbor_carries_storage_bytes() {
let bytes = hex!("9c524adbcf5611122b29125e5d35d2d22281aab533f00832d556b1f9eae51d7d");
assert_cbor_raw(&Hash256::from_lendian(bytes), &bytes);
assert_cbor_raw(&Arith256::from(Hash256::from_lendian(bytes)), &bytes);
}

#[test]
fn hash160_json_roundtrip() {
let bytes = hex!("0102030405060708090a0b0c0d0e0f1011121314");
Expand Down
4 changes: 3 additions & 1 deletion pkgs/p2p_core/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
//! Twelve-byte null-padded command string for P2P message dispatch.

use dash_primitives::hash_impl;
use dash_types::impl_bytes;
use dash_types::type_id::TypeId;
use dash_types::{impl_bytes, type_cvrt};

use core::fmt;

Expand Down Expand Up @@ -59,6 +59,8 @@ impl CommandString {
}
}

type_cvrt!(From<[u8; 12]> for CommandString, |bytes| Self(*bytes));

impl fmt::Debug for CommandString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "CommandString(\"{}\")", self.as_str())
Expand Down
10 changes: 10 additions & 0 deletions pkgs/pkc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Changed

- The following types serialize as raw bytes for machine-readable formats, following changes to `dash-types` and
`dash-num`. JSON and other human-readable formats are unaffected by this change.

`BlsPkBytes`, `BlsSigBytes`, `BlsPkHash`, `BlsShareId`, `BlsIes{Blob,Multi}Bytes`, `BlsPublicKey`, `BlsSignature`,
`Bls{Pk,Sig}Share`, `BlsIes{Blob,Multi}`, `EcdsaPkBytes`, `EcdsaSigBytes`, `EcdsaRecSigBytes`, `EcdsaPkHash`,
`EcdsaPublicKey`, `EcdsaSignature`, `EcdsaRecSignature`, `EddsaPkBytes`, `EddsaSigBytes`, `EddsaPkHash`,
`EddsaPublicKey`, `EddsaSignature`.

## [0.1.0-beta] - 2026-09-15

- Initial release.
Expand Down
2 changes: 1 addition & 1 deletion pkgs/pkc/src/bls/ies_bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ cfg_if! {

/// Decodes a whole hex image, rejecting any unconsumed tail.
fn from_image<'de, T: BaseCodec, D: Deserializer<'de>>(deserializer: D) -> Result<T, D::Error> {
let bytes = serde_hex::deserialize(deserializer)?;
let bytes: Vec<u8> = serde_hex::deserialize(deserializer)?;
let mut cursor = bytes.as_slice();
let value = T::decode(&mut cursor).map_err(DeError::custom)?;

Expand Down
14 changes: 13 additions & 1 deletion pkgs/pkc/src/bls/public_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ make_bytes! { // nosemgrep: bytes-rev-means-hash
#[expect(clippy::unwrap_used, reason = "test code")]
mod tests {
use crate::bls::tests::RSEED;
use crate::bls::{BlsScChia, BlsScIetf, BlsSecretKey};
use crate::bls::{BlsPkHash, BlsScChia, BlsScIetf, BlsSecretKey};

use dash_types::Hashable;
use rstest::rstest;
Expand All @@ -35,4 +35,16 @@ mod tests {
assert_ne!(chia.to_bytes(), ietf.to_bytes());
assert_ne!(Hashable::hash(&chia).as_bytes(), Hashable::hash(&ietf).as_bytes());
}

#[rstest]
fn slice_conversion_checks_length() {
let bytes = [7u8; 33];

assert_eq!(
BlsPkHash::<BlsScChia>::try_from(&bytes[..32]).unwrap().as_bytes(),
&[7u8; 32]
);
assert!(BlsPkHash::<BlsScChia>::try_from(&bytes[..]).is_err());
assert!(BlsPkHash::<BlsScChia>::try_from(&bytes[..31]).is_err());
}
}
Loading