Repository: rust-bitcoin/rust-bitcoin (rust)

Issue:
`relative::Locktime` is missing `Arbitrary` impl
The `absolute::LockTime` has one but not the `relative::LockTime`.

Pull request description:
Add Arbitrary impl for relative::LockTime
 Closes #4685 

Patch:
diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs
index 3b09c32c..d3052192 100644
--- a/bitcoin/src/lib.rs
+++ b/bitcoin/src/lib.rs
@@ -131,7 +131,7 @@
 };
 #[doc(inline)]
 pub use units::{
-    amount::{Amount, SignedAmount},
+    amount::{Amount, Denomination, SignedAmount},
     block::{BlockHeight, BlockHeightInterval, BlockMtp},
     fee_rate::FeeRate,
     time::{self, BlockTime},
diff --git a/bitcoin/src/network/mod.rs b/bitcoin/src/network/mod.rs
index 6751117d..d3c6dd97 100644
--- a/bitcoin/src/network/mod.rs
+++ b/bitcoin/src/network/mod.rs
@@ -59,18 +59,9 @@ pub enum TestnetVersion {
     V4,
 }
 
-
 /// The cryptocurrency network to act on.
-///
-/// This is an exhaustive enum, meaning that we cannot add any future networks without defining a
-/// new, incompatible version of this type. If you are using this type directly and wish to support the
-/// new network, this will be a breaking change to your APIs and likely require changes in your code.
-///
-/// If you are concerned about forward compatibility, consider using `T: Into<Params>` instead of this
-/// this type as a parameter to functions in your public API, or directly using the `Params` type.
-// For extensive discussion on the usage of `non_exhaustive` please see:
-// https://github.com/rust-bitcoin/rust-bitcoin/issues/2225
 #[derive(Copy, PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug)]
+#[non_exhaustive]
 pub enum Network {
     /// Mainnet Bitcoin.
     Bitcoin,
diff --git a/hashes/src/siphash24/mod.rs b/hashes/src/siphash24/mod.rs
index c5353629..61f3dcf0 100644
--- a/hashes/src/siphash24/mod.rs
+++ b/hashes/src/siphash24/mod.rs
@@ -218,7 +218,7 @@ pub fn to_u64(self) -> u64 { u64::from_le_bytes(self.0) }
     pub fn from_u64(hash: u64) -> Hash { Hash(hash.to_le_bytes()) }
 }
 
-/// Load a u64 using up to 7 bytes of a byte slice.
+/// Load an u64 using up to 7 bytes of a byte slice.
 ///
 /// Unsafe because: unchecked indexing at `start..start+len`.
 #[inline]
diff --git a/nightly-version b/nightly-version
index 98cd340e..e1fde0ed 100644
--- a/nightly-version
+++ b/nightly-version
@@ -1,1 +1,1 @@
-nightly-2025-07-11
+nightly-2025-06-27
diff --git a/p2p/src/message.rs b/p2p/src/message.rs
index 942e2582..2d4c60f6 100644
--- a/p2p/src/message.rs
+++ b/p2p/src/message.rs
@@ -5,7 +5,7 @@
 //! This module defines the `NetworkMessage` and `RawNetworkMessage` types that
 //! are used for (de)serializing Bitcoin objects for transmission on the network.
 
-use core::fmt;
+use core::{fmt, iter};
 use std::borrow::{Cow, ToOwned};
 use std::boxed::Box;
 
@@ -114,15 +114,14 @@ impl Decodable for CommandString {
     #[inline]
     fn consensus_decode<R: BufRead + ?Sized>(r: &mut R) -> Result<Self, encode::Error> {
         let rawbytes: [u8; 12] = Decodable::consensus_decode(r)?;
-
-        // Find the last non-null byte and trim null padding from the end
-        let trimmed = &rawbytes[..rawbytes.iter().rposition(|&b| b != 0).map_or(0, |i| i + 1)];
-
-        if !trimmed.is_ascii() {
-            return Err(crate::consensus::parse_failed_error("Command string must be ASCII"));
-        }
-
-        Ok(CommandString(Cow::Owned(unsafe { String::from_utf8_unchecked(trimmed.to_vec()) })))
+        let rv = iter::FromIterator::from_iter(rawbytes.iter().filter_map(|&u| {
+            if u > 0 {
+                Some(u as char)
+            } else {
+                None
+            }
+        }));
+        Ok(CommandString(rv))
     }
 }
 
@@ -893,24 +892,9 @@ fn commandstring() {
         assert_eq!(cs.as_ref().unwrap().to_string(), "Andrew".to_owned());
         assert_eq!(cs.unwrap(), CommandString::try_from_static("Andrew").unwrap());
 
-        // Test that embedded null bytes are preserved while trailing nulls are trimmed
-        let cs: Result<CommandString, _> =
-            deserialize(&[0, 0x41u8, 0x6e, 0x64, 0, 0x72, 0x65, 0x77, 0, 0, 0, 0]);
-        assert!(cs.is_ok());
-        assert_eq!(cs.as_ref().unwrap().to_string(), "\0And\0rew".to_owned());
-        assert_eq!(cs.unwrap(), CommandString::try_from_static("\0And\0rew").unwrap());
-
-        // Invalid CommandString, must be ASCII
-        assert!(deserialize::<CommandString>(&[
-            0, 0x41u8, 0x6e, 0xa4, 0, 0x72, 0x65, 0x77, 0, 0, 0, 0
-        ])
-        .is_err());
-
-        // Invalid CommandString, must be 12 bytes
-        assert!(deserialize::<CommandString>(&[
-            0x41u8, 0x6e, 0x64, 0x72, 0x65, 0x77, 0, 0, 0, 0, 0
-        ])
-        .is_err());
+        let short_cs: Result<CommandString, _> =
+            deserialize(&[0x41u8, 0x6e, 0x64, 0x72, 0x65, 0x77, 0, 0, 0, 0, 0]);
+        assert!(short_cs.is_err());
     }
 
     #[test]
diff --git a/primitives/src/locktime/relative.rs b/primitives/src/locktime/relative.rs
index 5bb2dc1a..effcd882 100644
--- a/primitives/src/locktime/relative.rs
+++ b/primitives/src/locktime/relative.rs
@@ -7,6 +7,9 @@
 
 use core::{convert, fmt};
 
+#[cfg(feature = "arbitrary")]
+use arbitrary::{Arbitrary, Unstructured};
+
 use internals::write_err;
 
 use crate::Sequence;
@@ -517,6 +520,16 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
     }
 }
 
+#[cfg(feature = "arbitrary")]
+impl<'a> Arbitrary<'a> for LockTime {
+    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
+        match bool::arbitrary(u)? {
+            true => Ok(LockTime::Blocks(NumberOfBlocks::arbitrary(u)?)),
+            false => Ok(LockTime::Time(NumberOf512Seconds::arbitrary(u)?))
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use units::{BlockHeight, BlockTime};


Test patch:
diff --git a/bitcoin/tests/bip_174.rs b/bitcoin/tests/bip_174.rs
index 0eacb878..10fb81ac 100644
--- a/bitcoin/tests/bip_174.rs
+++ b/bitcoin/tests/bip_174.rs
@@ -3,7 +3,6 @@
 
 use std::collections::BTreeMap;
 
-use bitcoin::amount::{Amount, Denomination};
 use bitcoin::bip32::{Fingerprint, IntoDerivationPath, KeySource, Xpriv, Xpub};
 use bitcoin::consensus::encode::{deserialize, serialize_hex};
 use bitcoin::hex::FromHex;
@@ -12,8 +11,8 @@
 use bitcoin::script::{PushBytes, ScriptBufExt as _};
 use bitcoin::secp256k1::Secp256k1;
 use bitcoin::{
-    absolute, script, transaction, NetworkKind, OutPoint, PrivateKey, PublicKey, ScriptBuf,
-    Sequence, Transaction, TxIn, TxOut, Witness,
+    absolute, script, transaction, Amount, Denomination, NetworkKind, OutPoint, PrivateKey,
+    PublicKey, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness,
 };
 
 #[track_caller]
diff --git a/bitcoin/tests/network.rs b/bitcoin/tests/network.rs
deleted file mode 100644
index 025912d0..00000000
--- a/bitcoin/tests/network.rs
+++ /dev/null
@@ -1,74 +0,0 @@
-//! Tests what a user can do when pattern matching on `Network` and associated types.
-
-use bitcoin::network::{Network, NetworkKind, TestnetVersion};
-
-#[test]
-fn can_match_exhaustively_on_network() {
-    // Returns true if `n` is mainnet.
-    fn is_mainnet(n: Network) -> bool {
-        matches!(n, Network::Bitcoin)
-    }
-
-    assert!(is_mainnet(Network::Bitcoin));
-}
-
-#[test]
-fn can_match_exhaustively_on_testnet() {
-    // Returns true if `n` is any testnet.
-    fn is_testnet(n: Network) -> bool {
-        matches!(n, Network::Testnet(_))
-    }
-
-    assert!(is_testnet(Network::Testnet(TestnetVersion::V3)));
-}
-
-#[test]
-fn can_use_network_kind() {
-    // Returns true if `n` is any mainnet.
-    fn is_mainnet(n: Network) -> bool {
-        NetworkKind::from(n).is_mainnet()
-    }
-
-    // Returns true if `n` is a any testnet.
-    fn is_testnet(n: Network) -> bool {
-        !NetworkKind::from(n).is_mainnet()
-    }
-
-    assert!(is_mainnet(Network::Bitcoin));
-    assert!(!is_testnet(Network::Bitcoin));
-
-    assert!(is_testnet(Network::Testnet(TestnetVersion::V3)));
-    assert!(!is_mainnet(Network::Testnet(TestnetVersion::V3)));
-
-}
-
-#[test]
-fn can_not_match_exaustively_on_testnet_version() {
-    // Returns true if `n` is testnet version 3.
-    fn is_testnet_v3(n: Network) -> bool {
-        match n {
-            Network::Testnet(TestnetVersion::V3) => true,
-            Network::Testnet(TestnetVersion::V4) => false,
-            // Catchall because of `non_exhaustive` attribute.
-            Network::Testnet(_) => false,
-            _ => false,
-        }
-    }
-
-    assert!(is_testnet_v3(Network::Testnet(TestnetVersion::V3)));
-}
-
-#[test]
-fn can_match_on_testnet_version_3_only() {
-    // Returns true if `n` is testnet version 3.
-    fn is_testnet_v3(n: Network) -> bool {
-        match n {
-            Network::Testnet(TestnetVersion::V3) => true,
-            // Catchall because its logically correct to do so.
-            Network::Testnet(_) => false,
-            _ => false,
-        }
-    }
-
-    assert!(is_testnet_v3(Network::Testnet(TestnetVersion::V3)));
-}