What changed, and why it matters
This commit only adds a new fuzz test for the 'units' crate. It does not change any production code, fix a bug, or alter behavior. There is no security issue introduced or patched here.
No action required. This is a testing/infrastructure addition.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit adds a new fuzz target ‘units_standard_checks’ using the ‘standard_test’ crate to run property-based checks on various unit types (Amount, FeeRate, Weight, etc.). It updates CI, lockfiles, and Cargo.toml to include the new dependency and fuzz binary. No production source code is modified.
Changed components
fuzz/fuzz_targets/units/standard_checks.rsfuzz/Cargo.tomlfuzz/generate-files.sh.github/workflows/cron-daily-fuzz.ymlCargo-minimal.lockCargo-recent.lockInspect captured patch +163 / −0
diff --git a/.github/workflows/cron-daily-fuzz.yml b/.github/workflows/cron-daily-fuzz.yml
index f47c9f60..b0427b44 100644
--- a/.github/workflows/cron-daily-fuzz.yml
+++ b/.github/workflows/cron-daily-fuzz.yml
@@ -46,6 +46,7 @@ jobs:
units_arbitrary_weight,
units_parse_amount,
units_parse_int,
+ units_standard_checks,
]
steps:
- name: Install test dependencies
diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock
index 860f0dd3..ad208a2a 100644
--- a/Cargo-minimal.lock
+++ b/Cargo-minimal.lock
@@ -90,6 +90,7 @@ dependencies = [
"honggfuzz",
"serde",
"serde_json",
+ "standard_test",
]
[[package]]
@@ -443,6 +444,15 @@ dependencies = [
"serde",
]
+[[package]]
+name = "standard_test"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af4bac6cd647192c3cb6e687af2703eb3a30e9187e555c664520cd848443ae70"
+dependencies = [
+ "arbitrary",
+]
+
[[package]]
name = "syn"
version = "2.0.46"
diff --git a/Cargo-recent.lock b/Cargo-recent.lock
index bc02789a..90ebfb6b 100644
--- a/Cargo-recent.lock
+++ b/Cargo-recent.lock
@@ -89,6 +89,7 @@ dependencies = [
"honggfuzz",
"serde",
"serde_json",
+ "standard_test",
]
[[package]]
@@ -451,6 +452,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+[[package]]
+name = "standard_test"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af4bac6cd647192c3cb6e687af2703eb3a30e9187e555c664520cd848443ae70"
+dependencies = [
+ "arbitrary",
+]
+
[[package]]
name = "syn"
version = "2.0.79"
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index 72288071..2b6c372b 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -18,6 +18,7 @@ arbitrary = { version = "1.4.1" }
serde = { version = "1.0.195", features = [ "derive" ] }
serde_json = "1.0.68"
+standard_test = "0.1.0"
[lints.rust]
unexpected_cfgs = { level = "deny", check-cfg = ['cfg(fuzzing)'] }
@@ -141,3 +142,7 @@ path = "fuzz_targets/consensus_encoding/decode_decoder2.rs"
[[bin]]
name = "consensus_encoding_decode_byte_vec"
path = "fuzz_targets/consensus_encoding/decode_byte_vec.rs"
+
+[[bin]]
+name = "units_standard_checks"
+path = "fuzz_targets/units/standard_checks.rs"
diff --git a/fuzz/fuzz_targets/units/standard_checks.rs b/fuzz/fuzz_targets/units/standard_checks.rs
new file mode 100644
index 00000000..933ab633
--- /dev/null
+++ b/fuzz/fuzz_targets/units/standard_checks.rs
@@ -0,0 +1,136 @@
+use bitcoin::{
+ Amount,
+ BlockHeight,
+ BlockHeightInterval,
+ BlockMtp,
+ BlockMtpInterval,
+ BlockTime,
+ FeeRate,
+ Sequence,
+ SignedAmount,
+ Weight,
+ absolute::{
+ Height,
+ MedianTimePast
+ },
+ relative::{
+ NumberOfBlocks,
+ NumberOf512Seconds
+ }
+};
+use standard_test::StandardChecks as _;
+use honggfuzz::fuzz;
+
+/// Implements the traits on the wrapper type $ty. Intended only to be called from inside wrap_for_checks!
+macro_rules! _impl_traits_on_wrapper {
+ ($ty:ident$(, $default:expr)?) => {
+ impl<'a> arbitrary::Arbitrary<'a> for $ty {
+ fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
+ Ok(Self(super::$ty::arbitrary(u)?))
+ }
+ }
+
+ impl core::ops::Deref for $ty {
+ type Target = super::$ty;
+
+ fn deref(&self) -> &Self::Target { &self.0 }
+ }
+
+ $(
+ impl Default for $ty {
+ fn default() -> $ty { Self($default) }
+ }
+ )?
+
+ standard_checks!($ty);
+ };
+}
+
+/// Create a wrapper type for a foreign type so that we can use standard_checks! on it from here.
+macro_rules! wrap_for_checks {
+ ($ty:ident) => {
+ #[derive(Default)]
+ pub(crate) struct $ty (super::$ty);
+
+ _impl_traits_on_wrapper!($ty);
+ };
+ ($ty:ident, $default:expr) => {
+ pub(crate) struct $ty (super::$ty);
+
+ _impl_traits_on_wrapper!($ty, $default);
+ };
+}
+
+
+mod fuzz {
+ use standard_test::standard_checks;
+
+ wrap_for_checks!(Amount);
+ wrap_for_checks!(BlockHeightInterval);
+ wrap_for_checks!(BlockMtpInterval);
+ wrap_for_checks!(NumberOf512Seconds);
+ wrap_for_checks!(NumberOfBlocks);
+ wrap_for_checks!(Sequence);
+ wrap_for_checks!(SignedAmount);
+
+ // Structs that need defaults
+ wrap_for_checks!(BlockHeight, super::BlockHeight::MIN);
+ wrap_for_checks!(BlockMtp, super::BlockMtp::from_u32(1_742_979_600)); // 26 Mar 2025 9:00 UTC
+ wrap_for_checks!(BlockTime, super::BlockTime::from(1_742_979_600)); // 26 Mar 2025 9:00 UTC
+ wrap_for_checks!(FeeRate, super::FeeRate::BROADCAST_MIN);
+ wrap_for_checks!(Height, super::Height::MIN);
+ wrap_for_checks!(MedianTimePast, super::MedianTimePast::MIN);
+ wrap_for_checks!(Weight, super::Weight::MIN_TRANSACTION);
+}
+
+fn do_test(data: &[u8]) {
+ fuzz::Amount::one_iteration(data);
+ fuzz::BlockHeight::one_iteration(data);
+ fuzz::BlockHeightInterval::one_iteration(data);
+ fuzz::BlockMtp::one_iteration(data);
+ fuzz::BlockMtpInterval::one_iteration(data);
+ fuzz::BlockTime::one_iteration(data);
+ fuzz::FeeRate::one_iteration(data);
+ fuzz::Height::one_iteration(data);
+ fuzz::NumberOf512Seconds::one_iteration(data);
+ fuzz::NumberOfBlocks::one_iteration(data);
+ fuzz::MedianTimePast::one_iteration(data);
+ fuzz::Sequence::one_iteration(data);
+ fuzz::SignedAmount::one_iteration(data);
+ fuzz::Weight::one_iteration(data);
+}
+
+fn main() {
+ loop {
+ fuzz!(|data| {
+ do_test(data);
+ });
+ }
+}
+
+#[cfg(all(test, fuzzing))]
+mod tests {
+ fn extend_vec_from_hex(hex: &str, out: &mut Vec<u8>) {
+ let mut b = 0;
+ for (idx, c) in hex.as_bytes().iter().enumerate() {
+ b <<= 4;
+ match *c {
+ b'A'..=b'F' => b |= c - b'A' + 10,
+ b'a'..=b'f' => b |= c - b'a' + 10,
+ b'0'..=b'9' => b |= c - b'0',
+ _ => panic!("Bad hex"),
+ }
+ if (idx & 1) == 1 {
+ out.push(b);
+ b = 0;
+ }
+ }
+ }
+
+ #[test]
+ fn duplicate_crash() {
+ let mut a = Vec::new();
+ extend_vec_from_hex("00000000", &mut a);
+ super::do_test(&a);
+ }
+}
diff --git a/fuzz/generate-files.sh b/fuzz/generate-files.sh
index 2471c5ac..c0c9cb5f 100755
--- a/fuzz/generate-files.sh
+++ b/fuzz/generate-files.sh
@@ -29,6 +29,7 @@ arbitrary = { version = "1.4.1" }
serde = { version = "1.0.195", features = [ "derive" ] }
serde_json = "1.0.68"
+standard_test = "0.1.0"
[lints.rust]
unexpected_cfgs = { level = "deny", check-cfg = ['cfg(fuzzing)'] }
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.