What changed, and why it matters
This commit creates a brand-new Rust crate that simply defines which Bitcoin network a program is using (mainnet, testnet, signet, or regtest). It is a pure code-organization/refactoring change with no security relevance.
No security action needed. Treat as normal refactoring/dependency reorganization.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch extracts the existing Network, NetworkKind, and TestnetVersion types into a new workspace crate named bitcoin-network-kind. It adds Cargo.toml, README, CHANGELOG, lint scripts, and a lib.rs containing the enum definitions, Display/FromStr/serde implementations, and unit tests. No existing behavior is changed; no cryptographic, parsing, or network code is modified.
Changed components
network/src/lib.rsnetwork/Cargo.tomlCargo.toml workspace membershipInspect captured patch +452 / −1
diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock
index 286f11bf..b452a1c9 100644
--- a/Cargo-minimal.lock
+++ b/Cargo-minimal.lock
@@ -122,6 +122,16 @@ dependencies = [
"bitcoin_hashes",
]
+[[package]]
+name = "bitcoin-network-kind"
+version = "0.1.0"
+dependencies = [
+ "bitcoin-internals",
+ "serde",
+ "serde_json",
+ "serde_test",
+]
+
[[package]]
name = "bitcoin-p2p-messages"
version = "0.1.0"
diff --git a/Cargo-recent.lock b/Cargo-recent.lock
index 878cb740..71de40b1 100644
--- a/Cargo-recent.lock
+++ b/Cargo-recent.lock
@@ -121,6 +121,16 @@ dependencies = [
"bitcoin_hashes",
]
+[[package]]
+name = "bitcoin-network-kind"
+version = "0.1.0"
+dependencies = [
+ "bitcoin-internals",
+ "serde",
+ "serde_json",
+ "serde_test",
+]
+
[[package]]
name = "bitcoin-p2p-messages"
version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index e1efd3af..720cd642 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
[workspace]
-members = ["addresses", "base58", "bip158", "bitcoin", "chacha20_poly1305", "consensus_encoding", "crypto", "fuzz", "hashes", "internals", "io", "p2p", "primitives", "units"]
+members = ["addresses", "base58", "bip158", "bitcoin", "chacha20_poly1305", "consensus_encoding", "crypto", "fuzz", "hashes", "internals", "io", "network", "p2p", "primitives", "units"]
exclude = ["benches"]
resolver = "2"
diff --git a/network/CHANGELOG.md b/network/CHANGELOG.md
new file mode 100644
index 00000000..ef321a25
--- /dev/null
+++ b/network/CHANGELOG.md
@@ -0,0 +1,9 @@
+# 0.1.0 - Initial release
+
+This is, and should remain, a very simple crate.
+
+Currently provides:
+
+- `Network`
+- `NetworkKind`
+- `TestnetVersion`
diff --git a/network/Cargo.toml b/network/Cargo.toml
new file mode 100644
index 00000000..a1b9fb3d
--- /dev/null
+++ b/network/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "bitcoin-network-kind"
+version = "0.1.0"
+authors = ["Andrew Poelstra <apoelstra@wpsoftware.net>"]
+license = "CC0-1.0"
+repository = "https://github.com/rust-bitcoin/rust-bitcoin"
+description = "Which network we are operating on"
+categories = ["cryptography::cryptocurrencies"]
+keywords = ["bitcoin", "types"]
+readme = "README.md"
+edition = "2021"
+rust-version = "1.74.0"
+exclude = ["tests", "contrib"]
+
+[features]
+default = ["std"]
+std = ["alloc", "internals/std"]
+alloc = ["internals/alloc", "serde?/alloc"]
+
+[dependencies]
+internals = { package = "bitcoin-internals", path = "../internals", version = "0.5.0" }
+
+serde = { version = "1.0.195", default-features = false, features = [ "derive" ], optional = true }
+
+[dev-dependencies]
+serde_json = "1.0.68"
+serde_test = "1.0.19"
+
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
+
+[lints]
+workspace = true
diff --git a/network/README.md b/network/README.md
new file mode 100644
index 00000000..98f86240
--- /dev/null
+++ b/network/README.md
@@ -0,0 +1,4 @@
+# Network
+
+Provides the `Network` type for differentiating which Bitcoin network
+one is operating on e.g., signet, regtest.
diff --git a/network/contrib/extra_lints.sh b/network/contrib/extra_lints.sh
new file mode 100755
index 00000000..5e7f0c94
--- /dev/null
+++ b/network/contrib/extra_lints.sh
@@ -0,0 +1,4 @@
+# No shebang, this file should not be executed.
+# shellcheck disable=SC2148
+
+cargo clippy --all-targets --no-default-features --keep-going -- -D warnings
diff --git a/network/contrib/test_vars.sh b/network/contrib/test_vars.sh
new file mode 100644
index 00000000..92dc0940
--- /dev/null
+++ b/network/contrib/test_vars.sh
@@ -0,0 +1,14 @@
+# No shebang, this file should not be executed.
+# shellcheck disable=SC2148
+#
+# disable verify unused vars, despite the fact that they are used when sourced
+# shellcheck disable=SC2034
+
+# Test all these features with "std" enabled.
+FEATURES_WITH_STD=""
+
+# Test all these features without "std" enabled.
+FEATURES_WITHOUT_STD="alloc"
+
+# Run these examples.
+EXAMPLES=""
diff --git a/network/src/lib.rs b/network/src/lib.rs
new file mode 100644
index 00000000..0987d808
--- /dev/null
+++ b/network/src/lib.rs
@@ -0,0 +1,366 @@
+// SPDX-License-Identifier: CC0-1.0
+
+//! Bitcoin network.
+//!
+//! The term "network" is overloaded, here [`Network`] refers to the specific
+//! Bitcoin network we are operating on e.g., signet, regtest. The terms
+//! "network" and "chain" are often used interchangeably for this concept.
+
+#![no_std]
+// Coding conventions.
+#![warn(missing_docs)]
+#![warn(deprecated_in_future)]
+#![doc(test(attr(warn(unused))))]
+
+#[cfg(feature = "std")]
+extern crate std;
+
+#[cfg(feature = "serde")]
+extern crate serde;
+
+use core::fmt;
+use core::str::FromStr;
+
+use internals::error::InputString;
+#[cfg(feature = "serde")]
+use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
+
+/// What kind of network we are on.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum NetworkKind {
+ /// The Bitcoin mainnet network.
+ Main,
+ /// Some kind of testnet network (testnet, signet, regtest).
+ Test,
+}
+
+impl NetworkKind {
+ /// Returns true if this is real mainnet bitcoin.
+ pub const fn is_mainnet(self) -> bool { matches!(self, Self::Main) }
+}
+
+impl From<Network> for NetworkKind {
+ fn from(network: Network) -> Self {
+ match network {
+ Network::Bitcoin => Self::Main,
+ Network::Testnet(_) | Network::Signet | Network::Regtest => Self::Test,
+ }
+ }
+}
+
+/// 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 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)]
+pub enum Network {
+ /// Mainnet Bitcoin.
+ Bitcoin,
+ /// Bitcoin's testnet network.
+ Testnet(TestnetVersion),
+ /// Bitcoin's signet network.
+ Signet,
+ /// Bitcoin's regtest network.
+ Regtest,
+}
+
+/// The testnet version to act on.
+#[derive(Copy, PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug)]
+#[non_exhaustive]
+pub enum TestnetVersion {
+ /// Testnet version 3.
+ V3,
+ /// Testnet version 4.
+ V4,
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Network {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(self.as_display_str())
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for Network {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ struct NetworkVisitor;
+
+ impl Visitor<'_> for NetworkVisitor {
+ type Value = Network;
+
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("a valid network identifier")
+ }
+
+ fn visit_str<E>(self, value: &str) -> Result<Network, E>
+ where
+ E: serde::de::Error,
+ {
+ Network::from_str(value).map_err(E::custom)
+ }
+ }
+
+ deserializer.deserialize_str(NetworkVisitor)
+ }
+}
+
+impl Network {
+ /// Converts a `Network` to its equivalent `bitcoind -chain` argument name.
+ ///
+ /// ```bash
+ /// $ bitcoin-23.0/bin/bitcoind --help | grep -C 3 '\-chain=<chain>'
+ /// Chain selection options:
+ ///
+ /// -chain=<chain>
+ /// Use the chain <chain> (default: main). Allowed values: main, test, signet, regtest
+ /// ```
+ pub fn to_core_arg(self) -> &'static str {
+ match self {
+ Self::Bitcoin => "main",
+ // For user-side compatibility, testnet3 is retained as test
+ Self::Testnet(TestnetVersion::V3) => "test",
+ Self::Testnet(TestnetVersion::V4) => "testnet4",
+ Self::Signet => "signet",
+ Self::Regtest => "regtest",
+ }
+ }
+
+ /// Converts a `bitcoind -chain` argument name to its equivalent `Network`.
+ ///
+ /// ```bash
+ /// $ bitcoin-23.0/bin/bitcoind --help | grep -C 3 '\-chain=<chain>'
+ /// Chain selection options:
+ ///
+ /// -chain=<chain>
+ /// Use the chain <chain> (default: main). Allowed values: main, test, signet, regtest
+ /// ```
+ ///
+ /// # Errors
+ ///
+ /// Errors if input is not exactly one of:
+ /// * `main`
+ /// * `test`
+ /// * `testnet4`
+ /// * `signet`
+ /// * `regtest`
+ pub fn from_core_arg(core_arg: &str) -> Result<Self, ParseNetworkError> {
+ let network = match core_arg {
+ "main" => Self::Bitcoin,
+ "test" => Self::Testnet(TestnetVersion::V3),
+ "testnet4" => Self::Testnet(TestnetVersion::V4),
+ "signet" => Self::Signet,
+ "regtest" => Self::Regtest,
+ _ => return Err(ParseNetworkError(InputString::from(core_arg))),
+ };
+ Ok(network)
+ }
+
+ /// Returns a string representation of the `Network` enum variant.
+ /// This is useful for displaying the network type as a string.
+ const fn as_display_str(self) -> &'static str {
+ match self {
+ Self::Bitcoin => "bitcoin",
+ Self::Testnet(TestnetVersion::V3) => "testnet",
+ Self::Testnet(TestnetVersion::V4) => "testnet4",
+ Self::Signet => "signet",
+ Self::Regtest => "regtest",
+ }
+ }
+}
+
+impl fmt::Display for Network {
+ fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
+ write!(f, "{}", self.as_display_str())
+ }
+}
+
+#[cfg(feature = "serde")]
+pub mod as_core_arg {
+ //! Module for serialization/deserialization of network variants into/from Bitcoin Core values
+
+ // No need to document these functions, they are well known.
+ #![allow(missing_docs)]
+ #![allow(clippy::missing_errors_doc)]
+
+ use crate::Network;
+
+ #[allow(clippy::trivially_copy_pass_by_ref)] // `serde` controls the API.
+ pub fn serialize<S>(network: &Network, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(network.to_core_arg())
+ }
+
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<Network, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct NetworkVisitor;
+
+ impl serde::de::Visitor<'_> for NetworkVisitor {
+ type Value = Network;
+
+ fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
+ Network::from_core_arg(s).map_err(|_| {
+ E::invalid_value(
+ serde::de::Unexpected::Str(s),
+ &"bitcoin network encoded as a string (either main, test, testnet4, signet or regtest)",
+ )
+ })
+ }
+
+ fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
+ write!(
+ formatter,
+ "bitcoin network encoded as a string (either main, test, testnet4, signet or regtest)"
+ )
+ }
+ }
+
+ deserializer.deserialize_str(NetworkVisitor)
+ }
+}
+
+/// An error in parsing network string.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub struct ParseNetworkError(InputString);
+
+impl fmt::Display for ParseNetworkError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
+ // Outputs 'failed to parse <input string> as network'.
+ write!(f, "{}", self.0.display_cannot_parse("network"))
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for ParseNetworkError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
+}
+
+impl FromStr for Network {
+ type Err = ParseNetworkError;
+
+ #[inline]
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "bitcoin" => Ok(Self::Bitcoin),
+ // For user-side compatibility, testnet3 is retained as testnet
+ "testnet" => Ok(Self::Testnet(TestnetVersion::V3)),
+ "testnet4" => Ok(Self::Testnet(TestnetVersion::V4)),
+ "signet" => Ok(Self::Signet),
+ "regtest" => Ok(Self::Regtest),
+ _ => Err(ParseNetworkError(InputString::from(s))),
+ }
+ }
+}
+
+impl AsRef<Self> for Network {
+ fn as_ref(&self) -> &Self { self }
+}
+
+#[cfg(test)]
+mod tests {
+ #[cfg(feature = "std")]
+ use std::string::ToString;
+
+ #[cfg(feature = "serde")]
+ use serde::{Deserialize, Serialize};
+
+ use super::{Network, TestnetVersion};
+
+ #[test]
+ #[cfg(feature = "std")]
+ fn string() {
+ assert_eq!(Network::Bitcoin.to_string(), "bitcoin");
+ assert_eq!(Network::Testnet(TestnetVersion::V3).to_string(), "testnet");
+ assert_eq!(Network::Testnet(TestnetVersion::V4).to_string(), "testnet4");
+ assert_eq!(Network::Regtest.to_string(), "regtest");
+ assert_eq!(Network::Signet.to_string(), "signet");
+
+ assert_eq!("bitcoin".parse::<Network>().unwrap(), Network::Bitcoin);
+ assert_eq!("testnet".parse::<Network>().unwrap(), Network::Testnet(TestnetVersion::V3));
+ assert_eq!("testnet4".parse::<Network>().unwrap(), Network::Testnet(TestnetVersion::V4));
+ assert_eq!("regtest".parse::<Network>().unwrap(), Network::Regtest);
+ assert_eq!("signet".parse::<Network>().unwrap(), Network::Signet);
+ assert!("fakenet".parse::<Network>().is_err());
+ }
+
+ #[test]
+ #[cfg(feature = "serde")]
+ #[cfg(feature = "std")]
+ fn serde_roundtrip() {
+ use std::{format, vec};
+
+ use Network::*;
+ let tests = vec![
+ (Bitcoin, "bitcoin"),
+ (Testnet(TestnetVersion::V3), "testnet"),
+ (Testnet(TestnetVersion::V4), "testnet4"),
+ (Signet, "signet"),
+ (Regtest, "regtest"),
+ ];
+
+ for tc in tests {
+ let network = tc.0;
+
+ let want = format!("\"{}\"", tc.1);
+ let got = serde_json::to_string(&tc.0).expect("failed to serialize network");
+ assert_eq!(got, want);
+
+ let back: Network = serde_json::from_str(&got).expect("failed to deserialize network");
+ assert_eq!(back, network);
+ }
+ }
+
+ #[test]
+ fn from_to_core_arg() {
+ let expected_pairs = [
+ (Network::Bitcoin, "main"),
+ (Network::Testnet(TestnetVersion::V3), "test"),
+ (Network::Testnet(TestnetVersion::V4), "testnet4"),
+ (Network::Regtest, "regtest"),
+ (Network::Signet, "signet"),
+ ];
+
+ for (net, core_arg) in &expected_pairs {
+ assert_eq!(Network::from_core_arg(core_arg), Ok(*net));
+ assert_eq!(net.to_core_arg(), *core_arg);
+ }
+ }
+
+ #[test]
+ #[cfg(feature = "serde")]
+ fn serde_as_core_arg() {
+ #[derive(Serialize, Deserialize, PartialEq, Debug)]
+ struct T {
+ #[serde(with = "crate::as_core_arg")]
+ pub network: Network,
+ }
+
+ serde_test::assert_tokens(
+ &T { network: Network::Bitcoin },
+ &[
+ serde_test::Token::Struct { name: "T", len: 1 },
+ serde_test::Token::Str("network"),
+ serde_test::Token::Str("main"),
+ serde_test::Token::StructEnd,
+ ],
+ );
+ }
+}
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.