Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1757bc0
primitives%refac(test): migrate spend corpus to direct deser
kwvg Jun 8, 2026
d9adcdb
primitives%refac(test): migrate coinbase corpus to direct deser
kwvg Jun 8, 2026
ec28def
primitives%refac(test): migrate data corpus to direct deser
kwvg Jun 8, 2026
4355cbe
primitives%refac(test): migrate block corpus to direct deser
kwvg Jun 8, 2026
87a20f1
primitives%refac(test): migrate proregtx corpus to direct deser
kwvg Jun 8, 2026
d680bbf
primitives%refac(test): migrate proupservtx corpus to direct deser
kwvg Jun 8, 2026
4194eb9
primitives%refac(test): migrate proupregtx corpus to direct deser
kwvg Jun 8, 2026
a37f88e
primitives%refac(test): migrate prouprevtx corpus to direct deser
kwvg Jun 8, 2026
1848c2e
primitives%refac(test): migrate cbtx corpus to direct deser
kwvg Jun 8, 2026
534e7ff
primitives%refac(test): migrate mnhftx corpus to direct deser
kwvg Jun 8, 2026
4b6072e
primitives%refac(test): migrate assetlock corpus to direct deser
kwvg Jun 8, 2026
8edf306
primitives%refac(test): migrate assetunlock corpus to direct deser
kwvg Jun 8, 2026
1debd19
primitives%refac(test): migrate quorum corpus to direct deser
kwvg Jun 9, 2026
4cdf7d7
primitives%refac(codec): use Amount for proposal payment_amount
kwvg Jun 9, 2026
71d876f
primitives%refac(test): migrate governance corpus to direct deser
kwvg Jun 9, 2026
7551fe8
p2p_core%refac(codec): add serde annotations for direct deserialization
kwvg Jun 9, 2026
2b61de6
p2p_core%feat(test): add corpus tests for p2p message types
kwvg Jun 9, 2026
581552a
sdk%feat(test): add shared test utility crate
kwvg Jun 9, 2026
7397504
primitives%feat(test): add govobj and govobjvote corpus tests
kwvg Jun 9, 2026
806bfc8
primitives%refac(test): inline corpus tests into source modules
kwvg Jun 9, 2026
e00c815
p2p_core%refac(test): inline corpus tests into source modules
kwvg Jun 8, 2026
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
21 changes: 15 additions & 6 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"pkgs/dev",
"pkgs/num",
"pkgs/params",
"pkgs/p2p_core",
Expand Down
39 changes: 39 additions & 0 deletions pkgs/dev/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[package]
name = "dash-dev"
version = "0.0.0"
edition = "2021"
license = "MIT"
publish = false

[features]
default = []
std = [
"dep:json5",
"dep:serde_json",
"bitcoin-consensus-encoding/std",
"dash-primitives/std",
"dash-types/std",
"hex-conservative/std",
]
full = ["std", "serde"]
serde = ["dep:serde", "dash-primitives/serde", "dash-types/serde"]
_internal = []

[dependencies]
bitcoin-consensus-encoding = { version = "0.2", default-features = false, features = [
"alloc",
] }
dash-primitives = { version = "0.0.0", path = "../primitives" }
dash-types = { version = "0.0.0", path = "../types", default-features = false }
hex-conservative = { version = "0.3", default-features = false, features = [
"alloc",
] }
json5 = { version = "0.4", optional = true }
serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true }
serde_json = { version = "1", optional = true }

[lints]
workspace = true

[package.rust-version]
workspace = true
107 changes: 107 additions & 0 deletions pkgs/dev/src/corpus.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//
// Copyright (c) 2026-present, The Dash Core developers
// SPDX-License-Identifier: MIT
// See the accompanying file LICENSE or https://opensource.org/license/MIT
//

//! Agnostic corpus read/write logic.

use crate::prelude::*;

/// A typed corpus entry pairing raw wire hex with expected details.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
pub struct CorpusEntry<T> {
pub raw: String,
pub details: T,
}

/// Reads a corpus JSON5 file from disk.
///
/// The file lives at `<manifest_dir>/corpus/<file>.json5`.
///
/// # Panics
///
/// Panics if the file cannot be read.
#[cfg(feature = "std")]
pub fn load_corpus_file(manifest_dir: &str, file: &str) -> String {
let path = format!("{manifest_dir}/corpus/{file}.json5");
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{path}: {e}"))
}

/// Reads a corpus section from JSON5 text.
///
/// Parses `text` as `{ "section": { "label": { raw, details } } }`,
/// hex-decodes `raw` to bytes, calls `check(raw_bytes, &details,
/// label)` for each entry, and returns all details keyed by label.
///
/// # Panics
///
/// Panics if the section is missing, empty, or the check function
/// panics.
#[cfg(all(feature = "std", feature = "serde"))]
pub fn read_corpus<T: ::serde::de::DeserializeOwned>(
text: &str,
section: &str,
mut check: impl FnMut(&[u8], &T, &str),
) -> BTreeMap<String, T> {
use hex_conservative::FromHex;

let mut outer: BTreeMap<String, serde_json::Value> =
json5::from_str(text).unwrap_or_else(|e| panic!("{section}: parse: {e}"));
let section_val = outer.remove(section).unwrap_or_else(|| panic!("{section}: not found"));
let entries: BTreeMap<String, CorpusEntry<T>> =
serde_json::from_value(section_val).unwrap_or_else(|e| panic!("{section}: {e}"));
assert!(!entries.is_empty(), "{section}: empty");

let mut result = BTreeMap::new();
for (label, entry) in entries {
let bytes = Vec::<u8>::from_hex(&entry.raw).unwrap_or_else(|e| panic!("{section}/{label}: hex: {e}"));
check(&bytes, &entry.details, &label);
result.insert(label, entry.details);
}
result
}

/// Serializes corpus entries to JSON in `{ raw, details }` format,
/// wrapped in a section key.
///
/// Produces `{ "section": { "label": { "raw": "", "details": T } } }`
/// so the output can be read back by [`read_corpus`] with a no-op
/// check function to verify the serde round-trip.
///
/// # Panics
///
/// Panics if serialization fails.
#[cfg(all(feature = "std", feature = "serde"))]
pub fn write_corpus<T: ::serde::Serialize>(section: &str, entries: &BTreeMap<String, T>) -> String {
#[derive(::serde::Serialize)]
struct Raw<'a, T: ::serde::Serialize> {
raw: &'a str,
details: &'a T,
}
let inner: BTreeMap<&str, Raw<T>> = entries
.iter()
.map(|(k, v)| (k.as_str(), Raw { raw: "", details: v }))
.collect();
let outer = BTreeMap::from([(section, inner)]);
serde_json::to_string(&outer).unwrap_or_else(|e| panic!("write_corpus: {e}"))
}

/// Verifies the serde round-trip for a set of corpus entries.
///
/// Writes `items` to JSON via [`write_corpus`], reads them back
/// with [`read_corpus`] (no-op check), and asserts equality.
///
/// # Panics
///
/// Panics on round-trip mismatch.
#[cfg(all(feature = "std", feature = "serde"))]
pub fn assert_serde_rt<T>(section: &str, items: &BTreeMap<String, T>)
where
T: ::serde::de::DeserializeOwned + ::serde::Serialize + PartialEq + core::fmt::Debug,
{
let json = write_corpus(section, items);
let rt = read_corpus::<T>(&json, section, |_, _, _| {});
assert_eq!(*items, rt, "{section}: serde round-trip");
}
104 changes: 104 additions & 0 deletions pkgs/dev/src/lambda.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
//
// Copyright (c) 2026-present, The Dash Core developers
// SPDX-License-Identifier: MIT
// See the accompanying file LICENSE or https://opensource.org/license/MIT
//

//! Reusable corpus check lambdas.

use crate::prelude::*;

use bitcoin_consensus_encoding::{decode_from_slice, encode_to_vec, Decodable, Decoder, Encodable};
use dash_primitives::{Transaction, TxHash};
use dash_types::codec::{BaseCodec, Checkable};

use core::fmt::{Debug, Display};

/// Check function for wire-encoded transaction types.
///
/// Asserts that the txid matches `label`, delegates to
/// [`check_wire`] for decode/encode round-trip, and runs
/// [`Checkable::check`] to verify structural invariants.
///
/// # Panics
///
/// Panics on txid mismatch, decode failure, mismatch, or
/// validation failure.
pub fn check_tx<T>(raw: &[u8], details: &T, label: &str)
where
T: Encodable + Decodable + Checkable + PartialEq + Debug,
<T::Decoder as Decoder>::Error: Debug + Display,
T::Error: Display,
{
assert_txid(raw, label);
check_wire(raw, details, label);
if let Some(e) = details.check() {
panic!("{label}: check: {e}");
}
}

/// Check function for wire-encoded types (`Encodable`/`Decodable`).
///
/// Decodes `raw`, asserts equality with `details`, and verifies that
/// re-encoding produces the original bytes.
///
/// # Panics
///
/// Panics on decode failure or mismatch.
pub fn check_wire<T>(raw: &[u8], details: &T, label: &str)
where
T: Encodable + Decodable + PartialEq + Debug,
<T::Decoder as Decoder>::Error: Debug + Display,
{
let decoded: T = decode_from_slice(raw).unwrap_or_else(|e| panic!("{label}: decode: {e}"));
assert_eq!(decoded, *details, "{label}");
assert_eq!(encode_to_vec(&decoded), raw, "{label}: encode");
}

/// Asserts that `SHA256d(raw)` matches `label` interpreted as a hex
/// txid.
///
/// # Panics
///
/// Panics on txid mismatch.
fn assert_txid(raw: &[u8], label: &str) {
let computed = dash_primitives::hash::tx_hash(raw);
let expected = TxHash::from_hex(label).unwrap_or_else(|e| panic!("{label}: bad txid hex: {e}"));
assert_eq!(computed, expected, "{label}: txid mismatch");
}

/// Decodes a [`Transaction`] from raw bytes.
///
/// # Panics
///
/// Panics if decoding fails.
fn decode_tx(raw: &[u8]) -> Transaction {
decode_from_slice::<Transaction>(raw).unwrap_or_else(|e| panic!("tx decode: {e}"))
}

/// Check function for special-transaction payload corpus entries.
///
/// Verifies the txid, decodes the full transaction, then decodes
/// the payload from `extra_payload`, compares with `details`, and
/// re-encodes both payload and full transaction.
///
/// # Panics
///
/// Panics on decode failure, mismatch, or txid mismatch.
pub fn check_sptx<T>(raw: &[u8], details: &T, label: &str)
where
T: BaseCodec + Checkable + PartialEq + Debug,
T::Error: Display,
{
assert_txid(raw, label);
let tx = decode_tx(raw);
let decoded = T::decode(&mut &tx.extra_payload[..]).unwrap_or_else(|e| panic!("{label}: payload: {e}"));
if let Some(e) = decoded.check() {
panic!("{label}: payload check: {e}");
}
assert_eq!(decoded, *details, "{label}");
let mut buf = Vec::new();
decoded.encode(&mut buf);
assert_eq!(buf, tx.extra_payload, "{label}: payload encode");
assert_eq!(encode_to_vec(&tx), raw, "{label}: tx encode");
}
25 changes: 25 additions & 0 deletions pkgs/dev/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Copyright (c) 2026-present, The Dash Core developers
// SPDX-License-Identifier: MIT
// See the accompanying file LICENSE or https://opensource.org/license/MIT
//

//! Development and test utilities.

#![no_std]
#![expect(clippy::panic, reason = "development crate")]

extern crate alloc;
#[cfg(feature = "std")]
extern crate std;

mod corpus;
mod lambda;
mod prelude;

#[cfg(feature = "std")]
pub use corpus::load_corpus_file;
pub use corpus::CorpusEntry;
#[cfg(all(feature = "std", feature = "serde"))]
pub use corpus::{assert_serde_rt, read_corpus, write_corpus};
pub use lambda::{check_sptx, check_tx, check_wire};
12 changes: 12 additions & 0 deletions pkgs/dev/src/prelude.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// Copyright (c) 2026-present, The Dash Core developers
// SPDX-License-Identifier: MIT
// See the accompanying file LICENSE or https://opensource.org/license/MIT
//

//! Re-exports for no_std compatibility.

pub(crate) use alloc::collections::BTreeMap;
pub(crate) use alloc::format;
pub(crate) use alloc::string::String;
pub(crate) use alloc::vec::Vec;
5 changes: 1 addition & 4 deletions pkgs/p2p_core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,9 @@ serde = { version = "1", default-features = false, features = [
], optional = true }

[dev-dependencies]
dash-dev = { version = "0.0.0", path = "../dev", features = ["full"] }
hex-conservative = "0.3"
hex-literal = "0.4"
json5 = "0.4"
rstest = "0.25"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

[lints]
workspace = true
Expand Down
Loading
Loading