From d9cf7270eb457fd660fa505701895ab4756e394d Mon Sep 17 00:00:00 2001 From: rustaceanrob Date: Mon, 2 Jun 2025 15:34:43 +0100 Subject: [PATCH 1/6] Move `bitcoin/p2p` into `p2p` Moves all of the content from `bitcoin/p2p` into `p2p`. `TryFrom` must be implemented for `Network -> Magic` now that `Network` is a foreign, non-exhaustive type. Ser/de test is updated accordingly, as well as the `Magic::from_network` constructor, which I have opted to return an `Option`. The `TryFrom` implementation uses a new `UnknownNetworkError(Network)` that mirrors the `Magic -> Network` error. The example handshake does not generate a random nonce for the version message, as there is no `rand` dependency in this crate and the program only makes a single, user-defined connection. It appears we can do better than copying and pasting the consensus encoding macros and functions from `bitcoin` without doing weird cross-crate macros that require some special knowledge of the crate to know when they will compile. --- Cargo-minimal.lock | 10 + Cargo-recent.lock | 10 + bitcoin/Cargo.toml | 4 - bitcoin/src/lib.rs | 1 - bitcoin/src/p2p/deser.rs | 42 -- bitcoin/src/p2p/mod.rs | 495 ------------------ fuzz/Cargo.toml | 1 + fuzz/fuzz_targets/bitcoin/deser_net_msg.rs | 2 +- .../bitcoin/p2p_address_roundtrip.rs | 2 +- p2p/Cargo.toml | 10 + {bitcoin => p2p}/examples/handshake.rs | 12 +- {bitcoin/src/p2p => p2p/src}/address.rs | 29 +- p2p/src/consensus.rs | 95 ++++ p2p/src/lib.rs | 492 +++++++++++++++++ {bitcoin/src/p2p => p2p/src}/message.rs | 47 +- .../src/p2p => p2p/src}/message_blockdata.rs | 15 +- {bitcoin/src/p2p => p2p/src}/message_bloom.rs | 6 +- .../p2p => p2p/src}/message_compact_blocks.rs | 4 +- .../src/p2p => p2p/src}/message_filter.rs | 6 +- .../src/p2p => p2p/src}/message_network.rs | 17 +- 20 files changed, 687 insertions(+), 613 deletions(-) delete mode 100644 bitcoin/src/p2p/deser.rs delete mode 100644 bitcoin/src/p2p/mod.rs rename {bitcoin => p2p}/examples/handshake.rs (90%) rename {bitcoin/src/p2p => p2p/src}/address.rs (96%) create mode 100644 p2p/src/consensus.rs rename {bitcoin/src/p2p => p2p/src}/message.rs (96%) rename {bitcoin/src/p2p => p2p/src}/message_blockdata.rs (93%) rename {bitcoin/src/p2p => p2p/src}/message_bloom.rs (89%) rename {bitcoin/src/p2p => p2p/src}/message_compact_blocks.rs (95%) rename {bitcoin/src/p2p => p2p/src}/message_filter.rs (95%) rename {bitcoin/src/p2p => p2p/src}/message_network.rs (94%) diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index 1ff3ceaa3..9667527dd 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -77,6 +77,7 @@ name = "bitcoin-fuzz" version = "0.0.1" dependencies = [ "bitcoin", + "bitcoin-p2p-messages", "honggfuzz", "serde", "serde_json", @@ -109,6 +110,15 @@ dependencies = [ [[package]] name = "bitcoin-p2p-messages" version = "0.1.0" +dependencies = [ + "bitcoin", + "bitcoin-internals", + "bitcoin-io 0.2.0", + "bitcoin-units", + "bitcoin_hashes 0.16.0", + "hex-conservative 0.3.0", + "hex_lit", +] [[package]] name = "bitcoin-primitives" diff --git a/Cargo-recent.lock b/Cargo-recent.lock index edfae1793..ded29f2b8 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -76,6 +76,7 @@ name = "bitcoin-fuzz" version = "0.0.1" dependencies = [ "bitcoin", + "bitcoin-p2p-messages", "honggfuzz", "serde", "serde_json", @@ -108,6 +109,15 @@ dependencies = [ [[package]] name = "bitcoin-p2p-messages" version = "0.1.0" +dependencies = [ + "bitcoin", + "bitcoin-internals", + "bitcoin-io 0.2.0", + "bitcoin-units", + "bitcoin_hashes 0.16.0", + "hex-conservative 0.3.0", + "hex_lit", +] [[package]] name = "bitcoin-primitives" diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml index f6bff55c9..2e3ff37a6 100644 --- a/bitcoin/Cargo.toml +++ b/bitcoin/Cargo.toml @@ -56,10 +56,6 @@ rustdoc-args = ["--cfg", "docsrs"] [[example]] name = "bip32" -[[example]] -name = "handshake" -required-features = ["rand-std"] - [[example]] name = "ecdsa-psbt" required-features = ["std", "bitcoinconsensus"] diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index 60e151355..d30521926 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -94,7 +94,6 @@ extern crate serde; mod internal_macros; #[macro_use] -pub mod p2p; pub mod address; pub mod bip152; pub mod bip158; diff --git a/bitcoin/src/p2p/deser.rs b/bitcoin/src/p2p/deser.rs deleted file mode 100644 index cae2906c3..000000000 --- a/bitcoin/src/p2p/deser.rs +++ /dev/null @@ -1,42 +0,0 @@ -macro_rules! impl_vec_wrapper { - ($wrapper: ident, $type: ty) => { - impl crate::consensus::encode::Encodable for $wrapper { - #[inline] - fn consensus_encode( - &self, - w: &mut W, - ) -> core::result::Result { - let mut len = 0; - len += w.emit_compact_size(self.0.len())?; - for c in self.0.iter() { - len += c.consensus_encode(w)?; - } - Ok(len) - } - } - - impl crate::consensus::encode::Decodable for $wrapper { - #[inline] - fn consensus_decode_from_finite_reader( - r: &mut R, - ) -> core::result::Result<$wrapper, crate::consensus::encode::Error> { - let len = r.read_compact_size()?; - // Do not allocate upfront more items than if the sequence of type - // occupied roughly quarter a block. This should never be the case - // for normal data, but even if that's not true - `push` will just - // reallocate. - // Note: OOM protection relies on reader eventually running out of - // data to feed us. - let max_capacity = - crate::consensus::encode::MAX_VEC_SIZE / 4 / core::mem::size_of::<$type>(); - let mut ret = Vec::with_capacity(core::cmp::min(len as usize, max_capacity)); - for _ in 0..len { - ret.push(Decodable::consensus_decode_from_finite_reader(r)?); - } - Ok($wrapper(ret)) - } - } - }; -} - -pub(crate) use impl_vec_wrapper; diff --git a/bitcoin/src/p2p/mod.rs b/bitcoin/src/p2p/mod.rs deleted file mode 100644 index 0f6b2ccae..000000000 --- a/bitcoin/src/p2p/mod.rs +++ /dev/null @@ -1,495 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! Bitcoin p2p network types. -//! -//! This module defines support for (de)serialization and network transport -//! of Bitcoin data and Bitcoin p2p network messages. - -#[cfg(feature = "std")] -pub mod address; -#[cfg(feature = "std")] -mod deser; -#[cfg(feature = "std")] -pub mod message; -#[cfg(feature = "std")] -pub mod message_blockdata; -#[cfg(feature = "std")] -pub mod message_bloom; -#[cfg(feature = "std")] -pub mod message_compact_blocks; -#[cfg(feature = "std")] -pub mod message_filter; -#[cfg(feature = "std")] -pub mod message_network; - -use core::str::FromStr; -use core::{fmt, ops}; - -use hex::FromHex; -use internals::{impl_to_hex_from_lower_hex, write_err}; -use io::{BufRead, Write}; - -use crate::consensus::encode::{self, Decodable, Encodable}; -use crate::network::{Network, Params, TestnetVersion}; -use crate::prelude::{Borrow, BorrowMut, String, ToOwned}; - -#[rustfmt::skip] -#[doc(inline)] -#[cfg(feature = "std")] -pub use self::address::Address; - -/// Version of the protocol as appearing in network message headers. -/// -/// This constant is used to signal to other peers which features you support. Increasing it implies -/// that your software also supports every feature prior to this version. Doing so without support -/// may lead to you incorrectly banning other peers or other peers banning you. -/// -/// These are the features required for each version: -/// 70016 - Support receiving `wtxidrelay` message between `version` and `verack` message -/// 70015 - Support receiving invalid compact blocks from a peer without banning them -/// 70014 - Support compact block messages `sendcmpct`, `cmpctblock`, `getblocktxn` and `blocktxn` -/// 70013 - Support `feefilter` message -/// 70012 - Support `sendheaders` message and announce new blocks via headers rather than inv -/// 70011 - Support NODE_BLOOM service flag and don't support bloom filter messages if it is not set -/// 70002 - Support `reject` message -/// 70001 - Support bloom filter messages `filterload`, `filterclear` `filteradd`, `merkleblock` and FILTERED_BLOCK inventory type -/// 60002 - Support `mempool` message -/// 60001 - Support `pong` message and nonce in `ping` message -pub const PROTOCOL_VERSION: u32 = 70001; - -/// Flags to indicate which network services a node supports. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ServiceFlags(u64); - -impl ServiceFlags { - /// NONE means no services supported. - pub const NONE: ServiceFlags = ServiceFlags(0); - - /// NETWORK means that the node is capable of serving the complete block chain. It is currently - /// set by all Bitcoin Core non pruned nodes, and is unset by SPV clients or other light - /// clients. - pub const NETWORK: ServiceFlags = ServiceFlags(1 << 0); - - /// GETUTXO means the node is capable of responding to the getutxo protocol request. Bitcoin - /// Core does not support this but a patch set called Bitcoin XT does. - /// See BIP 64 for details on how this is implemented. - pub const GETUTXO: ServiceFlags = ServiceFlags(1 << 1); - - /// BLOOM means the node is capable and willing to handle bloom-filtered connections. Bitcoin - /// Core nodes used to support this by default, without advertising this bit, but no longer do - /// as of protocol version 70011 (= NO_BLOOM_VERSION) - pub const BLOOM: ServiceFlags = ServiceFlags(1 << 2); - - /// WITNESS indicates that a node can be asked for blocks and transactions including witness - /// data. - pub const WITNESS: ServiceFlags = ServiceFlags(1 << 3); - - /// COMPACT_FILTERS means the node will service basic block filter requests. - /// See BIP157 and BIP158 for details on how this is implemented. - pub const COMPACT_FILTERS: ServiceFlags = ServiceFlags(1 << 6); - - /// NETWORK_LIMITED means the same as NODE_NETWORK with the limitation of only serving the last - /// 288 (2 day) blocks. - /// See BIP159 for details on how this is implemented. - pub const NETWORK_LIMITED: ServiceFlags = ServiceFlags(1 << 10); - - /// P2P_V2 indicates that the node supports the P2P v2 encrypted transport protocol. - /// See BIP324 for details on how this is implemented. - pub const P2P_V2: ServiceFlags = ServiceFlags(1 << 11); - - // NOTE: When adding new flags, remember to update the Display impl accordingly. - - /// Add [ServiceFlags] together. - /// - /// Returns itself. - pub fn add(&mut self, other: ServiceFlags) -> ServiceFlags { - self.0 |= other.0; - *self - } - - /// Remove [ServiceFlags] from this. - /// - /// Returns itself. - pub fn remove(&mut self, other: ServiceFlags) -> ServiceFlags { - self.0 &= !other.0; - *self - } - - /// Check whether [ServiceFlags] are included in this one. - pub fn has(self, flags: ServiceFlags) -> bool { (self.0 | flags.0) == self.0 } - - /// Gets the integer representation of this [`ServiceFlags`]. - pub fn to_u64(self) -> u64 { self.0 } -} - -impl fmt::LowerHex for ServiceFlags { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(&self.0, f) } -} -impl_to_hex_from_lower_hex!(ServiceFlags, |service_flags: &ServiceFlags| 16 - - service_flags.0.leading_zeros() as usize / 4); - -impl fmt::UpperHex for ServiceFlags { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::UpperHex::fmt(&self.0, f) } -} - -impl fmt::Display for ServiceFlags { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut flags = *self; - if flags == ServiceFlags::NONE { - return write!(f, "ServiceFlags(NONE)"); - } - let mut first = true; - macro_rules! write_flag { - ($f:ident) => { - if flags.has(ServiceFlags::$f) { - if !first { - write!(f, "|")?; - } - first = false; - write!(f, stringify!($f))?; - flags.remove(ServiceFlags::$f); - } - }; - } - write!(f, "ServiceFlags(")?; - write_flag!(NETWORK); - write_flag!(GETUTXO); - write_flag!(BLOOM); - write_flag!(WITNESS); - write_flag!(COMPACT_FILTERS); - write_flag!(NETWORK_LIMITED); - write_flag!(P2P_V2); - // If there are unknown flags left, we append them in hex. - if flags != ServiceFlags::NONE { - if !first { - write!(f, "|")?; - } - write!(f, "0x{:x}", flags)?; - } - write!(f, ")") - } -} - -impl From for ServiceFlags { - fn from(f: u64) -> Self { ServiceFlags(f) } -} - -impl From for u64 { - fn from(flags: ServiceFlags) -> Self { flags.0 } -} - -impl ops::BitOr for ServiceFlags { - type Output = Self; - - fn bitor(mut self, rhs: Self) -> Self { self.add(rhs) } -} - -impl ops::BitOrAssign for ServiceFlags { - fn bitor_assign(&mut self, rhs: Self) { self.add(rhs); } -} - -impl ops::BitXor for ServiceFlags { - type Output = Self; - - fn bitxor(mut self, rhs: Self) -> Self { self.remove(rhs) } -} - -impl ops::BitXorAssign for ServiceFlags { - fn bitxor_assign(&mut self, rhs: Self) { self.remove(rhs); } -} - -impl Encodable for ServiceFlags { - #[inline] - fn consensus_encode(&self, w: &mut W) -> Result { - self.0.consensus_encode(w) - } -} - -impl Decodable for ServiceFlags { - #[inline] - fn consensus_decode(r: &mut R) -> Result { - Ok(ServiceFlags(Decodable::consensus_decode(r)?)) - } -} -/// Network magic bytes to identify the cryptocurrency network the message was intended for. -#[derive(Copy, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] -pub struct Magic([u8; 4]); - -impl Magic { - /// Bitcoin mainnet network magic bytes. - pub const BITCOIN: Self = Self([0xF9, 0xBE, 0xB4, 0xD9]); - /// Bitcoin testnet3 network magic bytes. - #[deprecated(since = "0.33.0", note = "use `TESTNET3` instead")] - pub const TESTNET: Self = Self([0x0B, 0x11, 0x09, 0x07]); - /// Bitcoin testnet3 network magic bytes. - pub const TESTNET3: Self = Self([0x0B, 0x11, 0x09, 0x07]); - /// Bitcoin testnet4 network magic bytes. - pub const TESTNET4: Self = Self([0x1c, 0x16, 0x3f, 0x28]); - /// Bitcoin signet network magic bytes. - pub const SIGNET: Self = Self([0x0A, 0x03, 0xCF, 0x40]); - /// Bitcoin regtest network magic bytes. - pub const REGTEST: Self = Self([0xFA, 0xBF, 0xB5, 0xDA]); - - /// Construct a new network magic from bytes. - pub const fn from_bytes(bytes: [u8; 4]) -> Magic { Magic(bytes) } - - /// Get network magic bytes. - pub fn to_bytes(self) -> [u8; 4] { self.0 } - - /// Returns the magic bytes for the network defined by `params`. - pub fn from_params(params: impl AsRef) -> Self { params.as_ref().network.into() } -} - -impl FromStr for Magic { - type Err = ParseMagicError; - - fn from_str(s: &str) -> Result { - match <[u8; 4]>::from_hex(s) { - Ok(magic) => Ok(Magic::from_bytes(magic)), - Err(e) => Err(ParseMagicError { error: e, magic: s.to_owned() }), - } - } -} - -impl From for Magic { - fn from(network: Network) -> Self { - match network { - Network::Bitcoin => Magic::BITCOIN, - Network::Testnet(TestnetVersion::V3) => Magic::TESTNET3, - Network::Testnet(TestnetVersion::V4) => Magic::TESTNET4, - Network::Signet => Magic::SIGNET, - Network::Regtest => Magic::REGTEST, - // Remember to add the `TryFrom` for new networks - } - } -} - -impl TryFrom for Network { - type Error = UnknownMagicError; - - fn try_from(magic: Magic) -> Result { - match magic { - Magic::BITCOIN => Ok(Network::Bitcoin), - Magic::TESTNET3 => Ok(Network::Testnet(TestnetVersion::V3)), - Magic::TESTNET4 => Ok(Network::Testnet(TestnetVersion::V4)), - Magic::SIGNET => Ok(Network::Signet), - Magic::REGTEST => Ok(Network::Regtest), - _ => Err(UnknownMagicError(magic)), - } - } -} - -impl fmt::Display for Magic { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - hex::fmt_hex_exact!(f, 4, &self.0, hex::Case::Lower)?; - Ok(()) - } -} - -impl fmt::Debug for Magic { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fmt::Display::fmt(self, f) } -} - -impl fmt::LowerHex for Magic { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - hex::fmt_hex_exact!(f, 4, &self.0, hex::Case::Lower)?; - Ok(()) - } -} -impl_to_hex_from_lower_hex!(Magic, |_| 8); - -impl fmt::UpperHex for Magic { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - hex::fmt_hex_exact!(f, 4, &self.0, hex::Case::Upper)?; - Ok(()) - } -} - -impl Encodable for Magic { - fn consensus_encode(&self, writer: &mut W) -> Result { - self.0.consensus_encode(writer) - } -} - -impl Decodable for Magic { - fn consensus_decode(reader: &mut R) -> Result { - Ok(Magic(Decodable::consensus_decode(reader)?)) - } -} - -impl AsRef<[u8]> for Magic { - fn as_ref(&self) -> &[u8] { &self.0 } -} - -impl AsRef<[u8; 4]> for Magic { - fn as_ref(&self) -> &[u8; 4] { &self.0 } -} - -impl AsMut<[u8]> for Magic { - fn as_mut(&mut self) -> &mut [u8] { &mut self.0 } -} - -impl AsMut<[u8; 4]> for Magic { - fn as_mut(&mut self) -> &mut [u8; 4] { &mut self.0 } -} - -impl Borrow<[u8]> for Magic { - fn borrow(&self) -> &[u8] { &self.0 } -} - -impl Borrow<[u8; 4]> for Magic { - fn borrow(&self) -> &[u8; 4] { &self.0 } -} - -impl BorrowMut<[u8]> for Magic { - fn borrow_mut(&mut self) -> &mut [u8] { &mut self.0 } -} - -impl BorrowMut<[u8; 4]> for Magic { - fn borrow_mut(&mut self) -> &mut [u8; 4] { &mut self.0 } -} - -/// An error in parsing magic bytes. -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub struct ParseMagicError { - /// The error that occurred when parsing the string. - error: hex::HexToArrayError, - /// The byte string that failed to parse. - magic: String, -} - -impl fmt::Display for ParseMagicError { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - write_err!(f, "failed to parse {} as network magic", self.magic; self.error) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for ParseMagicError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.error) } -} - -/// Error in creating a Network from Magic bytes. -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub struct UnknownMagicError(Magic); - -impl fmt::Display for UnknownMagicError { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - write!(f, "unknown network magic {}", self.0) - } -} - -#[cfg(feature = "std")] -impl std::error::Error for UnknownMagicError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::consensus::encode::{deserialize, serialize}; - - #[test] - fn serialize_deserialize() { - assert_eq!(serialize(&Magic::BITCOIN), &[0xf9, 0xbe, 0xb4, 0xd9]); - let magic: Magic = Network::Bitcoin.into(); - assert_eq!(serialize(&magic), &[0xf9, 0xbe, 0xb4, 0xd9]); - assert_eq!(serialize(&Magic::TESTNET3), &[0x0b, 0x11, 0x09, 0x07]); - let magic: Magic = Network::Testnet(TestnetVersion::V3).into(); - assert_eq!(serialize(&magic), &[0x0b, 0x11, 0x09, 0x07]); - assert_eq!(serialize(&Magic::TESTNET4), &[0x1c, 0x16, 0x3f, 0x28]); - let magic: Magic = Network::Testnet(TestnetVersion::V4).into(); - assert_eq!(serialize(&magic), &[0x1c, 0x16, 0x3f, 0x28]); - assert_eq!(serialize(&Magic::SIGNET), &[0x0a, 0x03, 0xcf, 0x40]); - let magic: Magic = Network::Signet.into(); - assert_eq!(serialize(&magic), &[0x0a, 0x03, 0xcf, 0x40]); - assert_eq!(serialize(&Magic::REGTEST), &[0xfa, 0xbf, 0xb5, 0xda]); - let magic: Magic = Network::Regtest.into(); - assert_eq!(serialize(&magic), &[0xfa, 0xbf, 0xb5, 0xda]); - - assert_eq!( - deserialize::(&[0xf9, 0xbe, 0xb4, 0xd9]).ok(), - Some(Network::Bitcoin.into()) - ); - assert_eq!( - deserialize::(&[0x0b, 0x11, 0x09, 0x07]).ok(), - Some(Network::Testnet(TestnetVersion::V3).into()) - ); - assert_eq!( - deserialize::(&[0x1c, 0x16, 0x3f, 0x28]).ok(), - Some(Network::Testnet(TestnetVersion::V4).into()) - ); - assert_eq!( - deserialize::(&[0x0a, 0x03, 0xcf, 0x40]).ok(), - Some(Network::Signet.into()) - ); - assert_eq!( - deserialize::(&[0xfa, 0xbf, 0xb5, 0xda]).ok(), - Some(Network::Regtest.into()) - ); - } - - #[test] - fn service_flags_test() { - let all = [ - ServiceFlags::NETWORK, - ServiceFlags::GETUTXO, - ServiceFlags::BLOOM, - ServiceFlags::WITNESS, - ServiceFlags::COMPACT_FILTERS, - ServiceFlags::NETWORK_LIMITED, - ServiceFlags::P2P_V2, - ]; - - let mut flags = ServiceFlags::NONE; - for f in all.iter() { - assert!(!flags.has(*f)); - } - - flags |= ServiceFlags::WITNESS; - assert_eq!(flags, ServiceFlags::WITNESS); - - let mut flags2 = flags | ServiceFlags::GETUTXO; - for f in all.iter() { - assert_eq!(flags2.has(*f), *f == ServiceFlags::WITNESS || *f == ServiceFlags::GETUTXO); - } - - flags2 ^= ServiceFlags::WITNESS; - assert_eq!(flags2, ServiceFlags::GETUTXO); - - flags2 |= ServiceFlags::COMPACT_FILTERS; - flags2 ^= ServiceFlags::GETUTXO; - assert_eq!(flags2, ServiceFlags::COMPACT_FILTERS); - - // Test formatting. - assert_eq!("ServiceFlags(NONE)", ServiceFlags::NONE.to_string()); - assert_eq!("ServiceFlags(WITNESS)", ServiceFlags::WITNESS.to_string()); - assert_eq!("ServiceFlags(P2P_V2)", ServiceFlags::P2P_V2.to_string()); - let flag = ServiceFlags::WITNESS - | ServiceFlags::BLOOM - | ServiceFlags::NETWORK - | ServiceFlags::P2P_V2; - assert_eq!("ServiceFlags(NETWORK|BLOOM|WITNESS|P2P_V2)", flag.to_string()); - let flag = ServiceFlags::WITNESS | 0xf0.into(); - assert_eq!("ServiceFlags(WITNESS|COMPACT_FILTERS|0xb0)", flag.to_string()); - } - - #[test] - fn magic_from_str() { - let known_network_magic_strs = [ - ("f9beb4d9", Network::Bitcoin), - ("0b110907", Network::Testnet(TestnetVersion::V3)), - ("1c163f28", Network::Testnet(TestnetVersion::V4)), - ("fabfb5da", Network::Regtest), - ("0a03cf40", Network::Signet), - ]; - - for (magic_str, network) in &known_network_magic_strs { - let magic: Magic = magic_str.parse::().unwrap(); - assert_eq!(Network::try_from(magic).unwrap(), *network); - assert_eq!(&magic.to_string(), magic_str); - } - } -} diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 38adb8b8c..5cf789841 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -11,6 +11,7 @@ cargo-fuzz = true [dependencies] honggfuzz = { version = "0.5.56", default-features = false } bitcoin = { path = "../bitcoin", features = [ "serde" ] } +p2p = { path = "../p2p", package = "bitcoin-p2p-messages" } serde = { version = "1.0.103", features = [ "derive" ] } serde_json = "1.0" diff --git a/fuzz/fuzz_targets/bitcoin/deser_net_msg.rs b/fuzz/fuzz_targets/bitcoin/deser_net_msg.rs index 906bcee9f..635a4a7a0 100644 --- a/fuzz/fuzz_targets/bitcoin/deser_net_msg.rs +++ b/fuzz/fuzz_targets/bitcoin/deser_net_msg.rs @@ -1,7 +1,7 @@ use honggfuzz::fuzz; fn do_test(data: &[u8]) { - let _: Result = + let _: Result = bitcoin::consensus::encode::deserialize(data); } diff --git a/fuzz/fuzz_targets/bitcoin/p2p_address_roundtrip.rs b/fuzz/fuzz_targets/bitcoin/p2p_address_roundtrip.rs index 4fa4ffb27..5fba06632 100644 --- a/fuzz/fuzz_targets/bitcoin/p2p_address_roundtrip.rs +++ b/fuzz/fuzz_targets/bitcoin/p2p_address_roundtrip.rs @@ -2,7 +2,7 @@ use std::convert::TryFrom; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use bitcoin::consensus::Decodable; -use bitcoin::p2p::address::AddrV2; +use p2p::address::AddrV2; use honggfuzz::fuzz; fn do_test(data: &[u8]) { diff --git a/p2p/Cargo.toml b/p2p/Cargo.toml index 37166211c..892eee902 100644 --- a/p2p/Cargo.toml +++ b/p2p/Cargo.toml @@ -13,8 +13,18 @@ rust-version = "1.63.0" exclude = ["tests", "contrib"] [dependencies] +bitcoin = { path = "../bitcoin/" } +hashes = { package = "bitcoin_hashes", path = "../hashes", default-features = false, features = ["std"] } +hex = { package = "hex-conservative", version = "0.3.0", default-features = false, features = ["std"] } +internals = { package = "bitcoin-internals", path = "../internals", features = ["std"] } +io = { package = "bitcoin-io", path = "../io", default-features = false, features = ["std"] } +units = { package = "bitcoin-units", path = "../units", default-features = false, features = ["std"] } [dev-dependencies] +hex_lit = "0.1.1" + +[[example]] +name = "handshake" [package.metadata.docs.rs] all-features = true diff --git a/bitcoin/examples/handshake.rs b/p2p/examples/handshake.rs similarity index 90% rename from bitcoin/examples/handshake.rs rename to p2p/examples/handshake.rs index dbc383dd1..ad9194597 100644 --- a/bitcoin/examples/handshake.rs +++ b/p2p/examples/handshake.rs @@ -4,8 +4,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use std::{env, process}; use bitcoin::consensus::{encode, Decodable}; -use bitcoin::p2p::{self, address, message, message_network, Magic}; -use bitcoin::secp256k1::rand::Rng; +use bitcoin_p2p_messages::{self, address, message, message_network, Magic, ServiceFlags}; fn main() { // This example establishes a connection to a Bitcoin node, sends the initial @@ -71,19 +70,20 @@ fn build_version_message(address: SocketAddr) -> message::NetworkMessage { let my_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0); // "bitfield of features to be enabled for this connection" - let services = p2p::ServiceFlags::NONE; + let services = ServiceFlags::NONE; // "standard UNIX timestamp in seconds" let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time error").as_secs(); // "The network address of the node receiving this message" - let addr_recv = address::Address::new(&address, p2p::ServiceFlags::NONE); + let addr_recv = address::Address::new(&address, ServiceFlags::NONE); // "The network address of the node emitting this message" - let addr_from = address::Address::new(&my_address, p2p::ServiceFlags::NONE); + let addr_from = address::Address::new(&my_address, ServiceFlags::NONE); // "Node random nonce, randomly generated every time a version packet is sent. This nonce is used to detect connections to self." - let nonce: u64 = secp256k1::rand::thread_rng().gen(); + // Because this crate does not include the `rand` dependency, this is a fixed value. + let nonce: u64 = 42; // "User Agent (0x00 if string is 0 bytes long)" let user_agent = String::from("rust-example"); diff --git a/bitcoin/src/p2p/address.rs b/p2p/src/address.rs similarity index 96% rename from bitcoin/src/p2p/address.rs rename to p2p/src/address.rs index 7a4af27e4..f83c70563 100644 --- a/bitcoin/src/p2p/address.rs +++ b/p2p/src/address.rs @@ -10,9 +10,8 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV use io::{BufRead, Read, Write}; -use crate::consensus; -use crate::consensus::encode::{self, Decodable, Encodable, ReadExt, WriteExt}; -use crate::p2p::ServiceFlags; +use bitcoin::consensus::encode::{self, Decodable, Encodable, ReadExt, WriteExt}; +use crate::ServiceFlags; /// A message which can be sent on the Bitcoin network #[derive(Clone, PartialEq, Eq, Hash)] @@ -207,7 +206,7 @@ impl Encodable for AddrV2 { network: u8, bytes: &[u8], ) -> Result { - Ok(network.consensus_encode(w)? + encode::consensus_encode_with_size(bytes, w)?) + Ok(network.consensus_encode(w)? + crate::consensus::consensus_encode_with_size(bytes, w)?) } Ok(match *self { AddrV2::Ipv4(ref addr) => encode_addr(w, 1, &addr.octets())?, @@ -225,28 +224,28 @@ impl Decodable for AddrV2 { let network_id = u8::consensus_decode(r)?; let len = r.read_compact_size()?; if len > 512 { - return Err(consensus::parse_failed_error("IP must be <= 512 bytes")); + return Err(crate::consensus::parse_failed_error("IP must be <= 512 bytes")); } Ok(match network_id { 1 => { if len != 4 { - return Err(consensus::parse_failed_error("invalid IPv4 address")); + return Err(crate::consensus::parse_failed_error("invalid IPv4 address")); } let addr: [u8; 4] = Decodable::consensus_decode(r)?; AddrV2::Ipv4(Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3])) } 2 => { if len != 16 { - return Err(consensus::parse_failed_error("invalid IPv6 address")); + return Err(crate::consensus::parse_failed_error("invalid IPv6 address")); } let addr: [u16; 8] = read_be_address(r)?; if addr[0..3] == ONION { - return Err(consensus::parse_failed_error( + return Err(crate::consensus::parse_failed_error( "OnionCat address sent with IPv6 network id", )); } if addr[0..6] == [0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF] { - return Err(consensus::parse_failed_error( + return Err(crate::consensus::parse_failed_error( "IPV4 wrapped address sent with IPv6 network id", )); } @@ -257,26 +256,26 @@ impl Decodable for AddrV2 { 4 => { if len != 32 { - return Err(consensus::parse_failed_error("invalid TorV3 address")); + return Err(crate::consensus::parse_failed_error("invalid TorV3 address")); } let pubkey = Decodable::consensus_decode(r)?; AddrV2::TorV3(pubkey) } 5 => { if len != 32 { - return Err(consensus::parse_failed_error("invalid I2P address")); + return Err(crate::consensus::parse_failed_error("invalid I2P address")); } let hash = Decodable::consensus_decode(r)?; AddrV2::I2p(hash) } 6 => { if len != 16 { - return Err(consensus::parse_failed_error("invalid CJDNS address")); + return Err(crate::consensus::parse_failed_error("invalid CJDNS address")); } let addr: [u16; 8] = read_be_address(r)?; // check the first byte for the CJDNS marker if addr[0] >> 8 != 0xFC { - return Err(consensus::parse_failed_error("invalid CJDNS address")); + return Err(crate::consensus::parse_failed_error("invalid CJDNS address")); } AddrV2::Cjdns(Ipv6Addr::new( addr[0], addr[1], addr[2], addr[3], addr[4], addr[5], addr[6], addr[7], @@ -438,12 +437,12 @@ impl std::error::Error for AddrV2ToIpv6AddrError {} mod test { use std::net::IpAddr; + use bitcoin::consensus::encode::{deserialize, serialize}; use hex::FromHex; use hex_lit::hex; use super::*; - use crate::consensus::encode::{deserialize, serialize}; - use crate::p2p::message::AddrV2Payload; + use crate::message::AddrV2Payload; #[test] fn serialize_address() { diff --git a/p2p/src/consensus.rs b/p2p/src/consensus.rs new file mode 100644 index 000000000..68ea527d4 --- /dev/null +++ b/p2p/src/consensus.rs @@ -0,0 +1,95 @@ +use bitcoin::consensus::encode::WriteExt; +use io::Write; + +pub(crate) fn consensus_encode_with_size( + data: &[u8], + w: &mut W, +) -> Result { + Ok(w.emit_compact_size(data.len())? + w.emit_slice(data)?) +} + +pub(crate) fn parse_failed_error(msg: &'static str) -> bitcoin::consensus::encode::Error { + bitcoin::consensus::encode::Error::Parse(bitcoin::consensus::encode::ParseError::ParseFailed(msg)) +} + +macro_rules! impl_consensus_encoding { + ($thing:ident, $($field:ident),+) => ( + impl bitcoin::consensus::Encodable for $thing { + #[inline] + fn consensus_encode( + &self, + w: &mut W, + ) -> core::result::Result { + let mut len = 0; + $(len += self.$field.consensus_encode(w)?;)+ + Ok(len) + } + } + + impl bitcoin::consensus::Decodable for $thing { + + #[inline] + fn consensus_decode_from_finite_reader( + r: &mut R, + ) -> core::result::Result<$thing, bitcoin::consensus::encode::Error> { + Ok($thing { + $($field: bitcoin::consensus::Decodable::consensus_decode_from_finite_reader(r)?),+ + }) + } + + #[inline] + fn consensus_decode( + r: &mut R, + ) -> core::result::Result<$thing, bitcoin::consensus::encode::Error> { + let mut r = r.take(internals::ToU64::to_u64(bitcoin::consensus::encode::MAX_VEC_SIZE)); + Ok($thing { + $($field: bitcoin::consensus::Decodable::consensus_decode(&mut r)?),+ + }) + } + } + ); +} +pub(crate) use impl_consensus_encoding; + +macro_rules! impl_vec_wrapper { + ($wrapper: ident, $type: ty) => { + impl bitcoin::consensus::encode::Encodable for $wrapper { + #[inline] + fn consensus_encode( + &self, + w: &mut W, + ) -> core::result::Result { + let mut len = 0; + len += w.emit_compact_size(self.0.len())?; + for c in self.0.iter() { + len += c.consensus_encode(w)?; + } + Ok(len) + } + } + + impl bitcoin::consensus::encode::Decodable for $wrapper { + #[inline] + fn consensus_decode_from_finite_reader( + r: &mut R, + ) -> core::result::Result<$wrapper, bitcoin::consensus::encode::Error> { + let len = r.read_compact_size()?; + // Do not allocate upfront more items than if the sequence of type + // occupied roughly quarter a block. This should never be the case + // for normal data, but even if that's not true - `push` will just + // reallocate. + // Note: OOM protection relies on reader eventually running out of + // data to feed us. + let max_capacity = + bitcoin::consensus::encode::MAX_VEC_SIZE / 4 / core::mem::size_of::<$type>(); + let mut ret = Vec::with_capacity(core::cmp::min(len as usize, max_capacity)); + for _ in 0..len { + ret.push(Decodable::consensus_decode_from_finite_reader(r)?); + } + Ok($wrapper(ret)) + } + } + }; +} + +pub(crate) use impl_vec_wrapper; diff --git a/p2p/src/lib.rs b/p2p/src/lib.rs index bb9afc7d9..325026318 100644 --- a/p2p/src/lib.rs +++ b/p2p/src/lib.rs @@ -14,3 +14,495 @@ #![allow(clippy::needless_question_mark)] // https://github.com/rust-bitcoin/rust-bitcoin/pull/2134 #![allow(clippy::manual_range_contains)] // More readable than clippy's format. #![allow(clippy::uninlined_format_args)] // Allow `format!("{}", x)`instead of enforcing `format!("{x}")` + +pub mod address; +pub mod message; +pub mod message_blockdata; +pub mod message_bloom; +pub mod message_compact_blocks; +pub mod message_filter; +pub mod message_network; +mod consensus; + +extern crate alloc; + +use core::str::FromStr; +use core::{fmt, ops}; + +use std::borrow::{Borrow, BorrowMut, ToOwned}; + +use hex::FromHex; +use internals::impl_to_hex_from_lower_hex; +use io::{BufRead, Write}; + +use bitcoin::consensus::encode::{self, Decodable, Encodable}; +use bitcoin::network::{Network, Params, TestnetVersion}; + +#[rustfmt::skip] +#[doc(inline)] +pub use self::address::Address; + +/// Version of the protocol as appearing in network message headers. +/// +/// This constant is used to signal to other peers which features you support. Increasing it implies +/// that your software also supports every feature prior to this version. Doing so without support +/// may lead to you incorrectly banning other peers or other peers banning you. +/// +/// These are the features required for each version: +/// 70016 - Support receiving `wtxidrelay` message between `version` and `verack` message +/// 70015 - Support receiving invalid compact blocks from a peer without banning them +/// 70014 - Support compact block messages `sendcmpct`, `cmpctblock`, `getblocktxn` and `blocktxn` +/// 70013 - Support `feefilter` message +/// 70012 - Support `sendheaders` message and announce new blocks via headers rather than inv +/// 70011 - Support NODE_BLOOM service flag and don't support bloom filter messages if it is not set +/// 70002 - Support `reject` message +/// 70001 - Support bloom filter messages `filterload`, `filterclear` `filteradd`, `merkleblock` and FILTERED_BLOCK inventory type +/// 60002 - Support `mempool` message +/// 60001 - Support `pong` message and nonce in `ping` message +pub const PROTOCOL_VERSION: u32 = 70001; + +/// Flags to indicate which network services a node supports. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ServiceFlags(u64); + +impl ServiceFlags { + /// NONE means no services supported. + pub const NONE: ServiceFlags = ServiceFlags(0); + + /// NETWORK means that the node is capable of serving the complete block chain. It is currently + /// set by all Bitcoin Core non pruned nodes, and is unset by SPV clients or other light + /// clients. + pub const NETWORK: ServiceFlags = ServiceFlags(1 << 0); + + /// GETUTXO means the node is capable of responding to the getutxo protocol request. Bitcoin + /// Core does not support this but a patch set called Bitcoin XT does. + /// See BIP 64 for details on how this is implemented. + pub const GETUTXO: ServiceFlags = ServiceFlags(1 << 1); + + /// BLOOM means the node is capable and willing to handle bloom-filtered connections. Bitcoin + /// Core nodes used to support this by default, without advertising this bit, but no longer do + /// as of protocol version 70011 (= NO_BLOOM_VERSION) + pub const BLOOM: ServiceFlags = ServiceFlags(1 << 2); + + /// WITNESS indicates that a node can be asked for blocks and transactions including witness + /// data. + pub const WITNESS: ServiceFlags = ServiceFlags(1 << 3); + + /// COMPACT_FILTERS means the node will service basic block filter requests. + /// See BIP157 and BIP158 for details on how this is implemented. + pub const COMPACT_FILTERS: ServiceFlags = ServiceFlags(1 << 6); + + /// NETWORK_LIMITED means the same as NODE_NETWORK with the limitation of only serving the last + /// 288 (2 day) blocks. + /// See BIP159 for details on how this is implemented. + pub const NETWORK_LIMITED: ServiceFlags = ServiceFlags(1 << 10); + + /// P2P_V2 indicates that the node supports the P2P v2 encrypted transport protocol. + /// See BIP324 for details on how this is implemented. + pub const P2P_V2: ServiceFlags = ServiceFlags(1 << 11); + + // NOTE: When adding new flags, remember to update the Display impl accordingly. + + /// Add [ServiceFlags] together. + /// + /// Returns itself. + #[must_use] + pub fn add(&mut self, other: ServiceFlags) -> ServiceFlags { + self.0 |= other.0; + *self + } + + /// Remove [ServiceFlags] from this. + /// + /// Returns itself. + #[must_use] + pub fn remove(&mut self, other: ServiceFlags) -> ServiceFlags { + self.0 &= !other.0; + *self + } + + /// Check whether [ServiceFlags] are included in this one. + pub fn has(self, flags: ServiceFlags) -> bool { (self.0 | flags.0) == self.0 } + + /// Gets the integer representation of this [`ServiceFlags`]. + pub fn to_u64(self) -> u64 { self.0 } +} + +impl fmt::LowerHex for ServiceFlags { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(&self.0, f) } +} +impl_to_hex_from_lower_hex!(ServiceFlags, |service_flags: &ServiceFlags| 16 + - service_flags.0.leading_zeros() as usize / 4); + +impl fmt::UpperHex for ServiceFlags { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::UpperHex::fmt(&self.0, f) } +} + +impl fmt::Display for ServiceFlags { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut flags = *self; + if flags == ServiceFlags::NONE { + return write!(f, "ServiceFlags(NONE)"); + } + let mut first = true; + macro_rules! write_flag { + ($f:ident) => { + if flags.has(ServiceFlags::$f) { + if !first { + write!(f, "|")?; + } + first = false; + write!(f, stringify!($f))?; + let _ = flags.remove(ServiceFlags::$f); + } + }; + } + write!(f, "ServiceFlags(")?; + write_flag!(NETWORK); + write_flag!(GETUTXO); + write_flag!(BLOOM); + write_flag!(WITNESS); + write_flag!(COMPACT_FILTERS); + write_flag!(NETWORK_LIMITED); + write_flag!(P2P_V2); + // If there are unknown flags left, we append them in hex. + if flags != ServiceFlags::NONE { + if !first { + write!(f, "|")?; + } + write!(f, "0x{:x}", flags)?; + } + write!(f, ")") + } +} + +impl From for ServiceFlags { + fn from(f: u64) -> Self { ServiceFlags(f) } +} + +impl From for u64 { + fn from(flags: ServiceFlags) -> Self { flags.0 } +} + +impl ops::BitOr for ServiceFlags { + type Output = Self; + + fn bitor(mut self, rhs: Self) -> Self { self.add(rhs) } +} + +impl ops::BitOrAssign for ServiceFlags { + fn bitor_assign(&mut self, rhs: Self) { let _ = self.add(rhs); } +} + +impl ops::BitXor for ServiceFlags { + type Output = Self; + + fn bitxor(mut self, rhs: Self) -> Self { self.remove(rhs) } +} + +impl ops::BitXorAssign for ServiceFlags { + fn bitxor_assign(&mut self, rhs: Self) { let _ = self.remove(rhs); } +} + +impl Encodable for ServiceFlags { + #[inline] + fn consensus_encode(&self, w: &mut W) -> Result { + self.0.consensus_encode(w) + } +} + +impl Decodable for ServiceFlags { + #[inline] + fn consensus_decode(r: &mut R) -> Result { + Ok(ServiceFlags(Decodable::consensus_decode(r)?)) + } +} +/// Network magic bytes to identify the cryptocurrency network the message was intended for. +#[derive(Copy, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)] +pub struct Magic([u8; 4]); + +impl Magic { + /// Bitcoin mainnet network magic bytes. + pub const BITCOIN: Self = Self([0xF9, 0xBE, 0xB4, 0xD9]); + /// Bitcoin testnet3 network magic bytes. + #[deprecated(since = "0.33.0", note = "use `TESTNET3` instead")] + pub const TESTNET: Self = Self([0x0B, 0x11, 0x09, 0x07]); + /// Bitcoin testnet3 network magic bytes. + pub const TESTNET3: Self = Self([0x0B, 0x11, 0x09, 0x07]); + /// Bitcoin testnet4 network magic bytes. + pub const TESTNET4: Self = Self([0x1c, 0x16, 0x3f, 0x28]); + /// Bitcoin signet network magic bytes. + pub const SIGNET: Self = Self([0x0A, 0x03, 0xCF, 0x40]); + /// Bitcoin regtest network magic bytes. + pub const REGTEST: Self = Self([0xFA, 0xBF, 0xB5, 0xDA]); + + /// Construct a new network magic from bytes. + pub const fn from_bytes(bytes: [u8; 4]) -> Magic { Magic(bytes) } + + /// Get network magic bytes. + pub fn to_bytes(self) -> [u8; 4] { self.0 } + + /// Returns the magic bytes for the network defined by `params`. + pub fn from_params(params: impl AsRef) -> Option { params.as_ref().network.try_into().ok() } +} + +impl FromStr for Magic { + type Err = ParseMagicError; + + fn from_str(s: &str) -> Result { + match <[u8; 4]>::from_hex(s) { + Ok(magic) => Ok(Magic::from_bytes(magic)), + Err(e) => Err(ParseMagicError { error: e, magic: s.to_owned() }), + } + } +} + +impl TryFrom for Magic { + type Error = UnknownNetworkError; + + fn try_from(network: Network) -> Result { + match network { + Network::Bitcoin => Ok(Magic::BITCOIN), + Network::Testnet(TestnetVersion::V3) => Ok(Magic::TESTNET3), + Network::Testnet(TestnetVersion::V4) => Ok(Magic::TESTNET4), + Network::Signet => Ok(Magic::SIGNET), + Network::Regtest => Ok(Magic::REGTEST), + _ => Err(UnknownNetworkError(network)), + } + } +} + +impl TryFrom for Network { + type Error = UnknownMagicError; + + fn try_from(magic: Magic) -> Result { + match magic { + Magic::BITCOIN => Ok(Network::Bitcoin), + Magic::TESTNET3 => Ok(Network::Testnet(TestnetVersion::V3)), + Magic::TESTNET4 => Ok(Network::Testnet(TestnetVersion::V4)), + Magic::SIGNET => Ok(Network::Signet), + Magic::REGTEST => Ok(Network::Regtest), + _ => Err(UnknownMagicError(magic)), + } + } +} + +impl fmt::Display for Magic { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + hex::fmt_hex_exact!(f, 4, &self.0, hex::Case::Lower)?; + Ok(()) + } +} + +impl fmt::Debug for Magic { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fmt::Display::fmt(self, f) } +} + +impl fmt::LowerHex for Magic { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + hex::fmt_hex_exact!(f, 4, &self.0, hex::Case::Lower)?; + Ok(()) + } +} +impl_to_hex_from_lower_hex!(Magic, |_| 8); + +impl fmt::UpperHex for Magic { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + hex::fmt_hex_exact!(f, 4, &self.0, hex::Case::Upper)?; + Ok(()) + } +} + +impl Encodable for Magic { + fn consensus_encode(&self, writer: &mut W) -> Result { + self.0.consensus_encode(writer) + } +} + +impl Decodable for Magic { + fn consensus_decode(reader: &mut R) -> Result { + Ok(Magic(Decodable::consensus_decode(reader)?)) + } +} + +impl AsRef<[u8]> for Magic { + fn as_ref(&self) -> &[u8] { &self.0 } +} + +impl AsRef<[u8; 4]> for Magic { + fn as_ref(&self) -> &[u8; 4] { &self.0 } +} + +impl AsMut<[u8]> for Magic { + fn as_mut(&mut self) -> &mut [u8] { &mut self.0 } +} + +impl AsMut<[u8; 4]> for Magic { + fn as_mut(&mut self) -> &mut [u8; 4] { &mut self.0 } +} + +impl Borrow<[u8]> for Magic { + fn borrow(&self) -> &[u8] { &self.0 } +} + +impl Borrow<[u8; 4]> for Magic { + fn borrow(&self) -> &[u8; 4] { &self.0 } +} + +impl BorrowMut<[u8]> for Magic { + fn borrow_mut(&mut self) -> &mut [u8] { &mut self.0 } +} + +impl BorrowMut<[u8; 4]> for Magic { + fn borrow_mut(&mut self) -> &mut [u8; 4] { &mut self.0 } +} + +/// An error in parsing magic bytes. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct ParseMagicError { + /// The error that occurred when parsing the string. + error: hex::HexToArrayError, + /// The byte string that failed to parse. + magic: String, +} + +impl fmt::Display for ParseMagicError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "failed to parse {} as network magic", self.magic) + } +} + +impl std::error::Error for ParseMagicError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.error) } +} + +/// Error in creating a Network from Magic bytes. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct UnknownMagicError(Magic); + +impl fmt::Display for UnknownMagicError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "unknown network magic {}", self.0) + } +} + +impl std::error::Error for UnknownMagicError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } +} + +/// Error in creating a Magic from a Network. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct UnknownNetworkError(Network); + +impl fmt::Display for UnknownNetworkError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "unknown network {}", self.0) + } +} + +impl std::error::Error for UnknownNetworkError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } +} + +#[cfg(test)] +mod tests { + use super::*; + use bitcoin::consensus::encode::{deserialize, serialize}; + + #[test] + fn serialize_deserialize() { + assert_eq!(serialize(&Magic::BITCOIN), &[0xf9, 0xbe, 0xb4, 0xd9]); + let magic: Magic = Network::Bitcoin.try_into().unwrap(); + assert_eq!(serialize(&magic), &[0xf9, 0xbe, 0xb4, 0xd9]); + assert_eq!(serialize(&Magic::TESTNET3), &[0x0b, 0x11, 0x09, 0x07]); + let magic: Magic = Network::Testnet(TestnetVersion::V3).try_into().unwrap(); + assert_eq!(serialize(&magic), &[0x0b, 0x11, 0x09, 0x07]); + assert_eq!(serialize(&Magic::TESTNET4), &[0x1c, 0x16, 0x3f, 0x28]); + let magic: Magic = Network::Testnet(TestnetVersion::V4).try_into().unwrap(); + assert_eq!(serialize(&magic), &[0x1c, 0x16, 0x3f, 0x28]); + assert_eq!(serialize(&Magic::SIGNET), &[0x0a, 0x03, 0xcf, 0x40]); + let magic: Magic = Network::Signet.try_into().unwrap(); + assert_eq!(serialize(&magic), &[0x0a, 0x03, 0xcf, 0x40]); + assert_eq!(serialize(&Magic::REGTEST), &[0xfa, 0xbf, 0xb5, 0xda]); + let magic: Magic = Network::Regtest.try_into().unwrap(); + assert_eq!(serialize(&magic), &[0xfa, 0xbf, 0xb5, 0xda]); + + assert_eq!(deserialize::(&[0xf9, 0xbe, 0xb4, 0xd9]).ok(), Network::Bitcoin.try_into().ok()); + assert_eq!( + deserialize::(&[0x0b, 0x11, 0x09, 0x07]).ok(), + Network::Testnet(TestnetVersion::V3).try_into().ok() + ); + assert_eq!( + deserialize::(&[0x1c, 0x16, 0x3f, 0x28]).ok(), + Network::Testnet(TestnetVersion::V4).try_into().ok() + ); + assert_eq!(deserialize::(&[0x0a, 0x03, 0xcf, 0x40]).ok(), Network::Signet.try_into().ok()); + assert_eq!(deserialize::(&[0xfa, 0xbf, 0xb5, 0xda]).ok(), Network::Regtest.try_into().ok()); + } + + + #[test] + fn service_flags_test() { + let all = [ + ServiceFlags::NETWORK, + ServiceFlags::GETUTXO, + ServiceFlags::BLOOM, + ServiceFlags::WITNESS, + ServiceFlags::COMPACT_FILTERS, + ServiceFlags::NETWORK_LIMITED, + ServiceFlags::P2P_V2, + ]; + + let mut flags = ServiceFlags::NONE; + for f in all.iter() { + assert!(!flags.has(*f)); + } + + flags |= ServiceFlags::WITNESS; + assert_eq!(flags, ServiceFlags::WITNESS); + + let mut flags2 = flags | ServiceFlags::GETUTXO; + for f in all.iter() { + assert_eq!(flags2.has(*f), *f == ServiceFlags::WITNESS || *f == ServiceFlags::GETUTXO); + } + + flags2 ^= ServiceFlags::WITNESS; + assert_eq!(flags2, ServiceFlags::GETUTXO); + + flags2 |= ServiceFlags::COMPACT_FILTERS; + flags2 ^= ServiceFlags::GETUTXO; + assert_eq!(flags2, ServiceFlags::COMPACT_FILTERS); + + // Test formatting. + assert_eq!("ServiceFlags(NONE)", ServiceFlags::NONE.to_string()); + assert_eq!("ServiceFlags(WITNESS)", ServiceFlags::WITNESS.to_string()); + assert_eq!("ServiceFlags(P2P_V2)", ServiceFlags::P2P_V2.to_string()); + let flag = ServiceFlags::WITNESS + | ServiceFlags::BLOOM + | ServiceFlags::NETWORK + | ServiceFlags::P2P_V2; + assert_eq!("ServiceFlags(NETWORK|BLOOM|WITNESS|P2P_V2)", flag.to_string()); + let flag = ServiceFlags::WITNESS | 0xf0.into(); + assert_eq!("ServiceFlags(WITNESS|COMPACT_FILTERS|0xb0)", flag.to_string()); + } + + #[test] + fn magic_from_str() { + let known_network_magic_strs = [ + ("f9beb4d9", Network::Bitcoin), + ("0b110907", Network::Testnet(TestnetVersion::V3)), + ("1c163f28", Network::Testnet(TestnetVersion::V4)), + ("fabfb5da", Network::Regtest), + ("0a03cf40", Network::Signet), + ]; + + for (magic_str, network) in &known_network_magic_strs { + let magic: Magic = magic_str.parse::().unwrap(); + assert_eq!(Network::try_from(magic).unwrap(), *network); + assert_eq!(&magic.to_string(), magic_str); + } + } +} diff --git a/bitcoin/src/p2p/message.rs b/p2p/src/message.rs similarity index 96% rename from bitcoin/src/p2p/message.rs rename to p2p/src/message.rs index 14b11f27d..c83bfde91 100644 --- a/bitcoin/src/p2p/message.rs +++ b/p2p/src/message.rs @@ -6,21 +6,23 @@ //! are used for (de)serializing Bitcoin objects for transmission on the network. use core::{fmt, iter}; +use std::borrow::Cow; +use std::boxed::Box; +use std::borrow::ToOwned; use hashes::sha256d; use internals::ToU64 as _; use io::{BufRead, Write}; -use crate::consensus::encode::{self, CheckedData, Decodable, Encodable, ReadExt, WriteExt}; -use crate::merkle_tree::MerkleBlock; -use crate::p2p::address::{AddrV2Message, Address}; -use crate::p2p::deser::impl_vec_wrapper; -use crate::p2p::{ +use bitcoin::consensus::encode::{self, CheckedData, Decodable, Encodable, ReadExt, WriteExt}; +use bitcoin::merkle_tree::MerkleBlock; +use crate::address::{AddrV2Message, Address}; +use crate::consensus::impl_vec_wrapper; +use crate::{ message_blockdata, message_bloom, message_compact_blocks, message_filter, message_network, Magic, }; -use crate::prelude::{Box, Cow, String, ToOwned, Vec}; -use crate::{block, consensus, transaction}; +use bitcoin::{block, transaction}; /// The maximum number of [super::message_blockdata::Inventory] items in an `inv` message. /// @@ -144,7 +146,6 @@ impl fmt::Display for CommandStringError { } } -#[cfg(feature = "std")] impl std::error::Error for CommandStringError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } } @@ -518,7 +519,7 @@ impl Decodable for HeaderDeserializationWrapper { for _ in 0..len { ret.push(Decodable::consensus_decode(r)?); if u8::consensus_decode(r)? != 0u8 { - return Err(consensus::parse_failed_error( + return Err(crate::consensus::parse_failed_error( "Headers message should not contain transactions", )); } @@ -718,21 +719,21 @@ mod test { use units::BlockHeight; use super::*; - use crate::bip152::BlockTransactionsRequest; - use crate::bip158::{FilterHash, FilterHeader}; - use crate::block::{Block, BlockHash}; - use crate::consensus::encode::{deserialize, deserialize_partial, serialize}; - use crate::p2p::address::AddrV2; - use crate::p2p::message_blockdata::{GetBlocksMessage, GetHeadersMessage, Inventory}; - use crate::p2p::message_bloom::{BloomFlags, FilterAdd, FilterLoad}; - use crate::p2p::message_compact_blocks::{GetBlockTxn, SendCmpct}; - use crate::p2p::message_filter::{ + use bitcoin::bip152::BlockTransactionsRequest; + use bitcoin::bip158::{FilterHash, FilterHeader}; + use bitcoin::block::{Block, BlockHash}; + use bitcoin::consensus::encode::{deserialize, deserialize_partial, serialize}; + use bitcoin::script::ScriptBuf; + use bitcoin::transaction::{Transaction, Txid}; + use crate::address::AddrV2; + use crate::message_blockdata::{GetBlocksMessage, GetHeadersMessage, Inventory}; + use crate::message_bloom::{BloomFlags, FilterAdd, FilterLoad}; + use crate::message_compact_blocks::{GetBlockTxn, SendCmpct}; + use crate::message_filter::{ CFCheckpt, CFHeaders, CFilter, GetCFCheckpt, GetCFHeaders, GetCFilters, }; - use crate::p2p::message_network::{Reject, RejectReason, VersionMessage}; - use crate::p2p::ServiceFlags; - use crate::script::ScriptBuf; - use crate::transaction::{Transaction, Txid}; + use crate::message_network::{Reject, RejectReason, VersionMessage}; + use crate::ServiceFlags; fn hash(array: [u8; 32]) -> sha256d::Hash { sha256d::Hash::from_byte_array(array) } @@ -740,7 +741,7 @@ mod test { fn full_round_ser_der_raw_network_message() { let version_msg: VersionMessage = deserialize(&hex!("721101000100000000000000e6e0845300000000010000000000000000000000000000000000ffff0000000000000100000000000000fd87d87eeb4364f22cf54dca59412db7208d47d920cffce83ee8102f5361746f7368693a302e392e39392f2c9f040001")).unwrap(); let tx: Transaction = deserialize(&hex!("0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000")).unwrap(); - let block: Block = deserialize(&include_bytes!("../../tests/data/testnet_block_000000000000045e0b1660b6445b5e5c5ab63c9a4f956be7e1e69be04fa4497b.raw")[..]).unwrap(); + let block: Block = deserialize(&hex!("00608e2e094d41aecfbcbf8fe70cb60be57516b07db1bafee4c4de5dad760000000000004aec16eab3be95abe9c54e01cf850c14b8c5cad1bc6b2e73e811db5d5998ada404503e66fcff031b4ebd99d701010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff3402983a000404503e6604f1f617271083bc3d6600000000000000000007bb1b0a636b706f6f6c0d506f72746c616e642e484f444cffffffff0200f2052a010000001976a9142ce72b25fe97b52638c199acfaa5e3891ddfed5b88ac0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000")).unwrap(); let header: block::Header = deserialize(&hex!("010000004ddccd549d28f385ab457e98d1b11ce80bfea2c5ab93015ade4973e400000000bf4473e53794beae34e64fccc471dace6ae544180816f89591894e0f417a914cd74d6e49ffff001d323b3a7b")).unwrap(); let script: ScriptBuf = deserialize(&hex!("1976a91431a420903c05a0a7de2de40c9f02ebedbacdc17288ac")).unwrap(); diff --git a/bitcoin/src/p2p/message_blockdata.rs b/p2p/src/message_blockdata.rs similarity index 93% rename from bitcoin/src/p2p/message_blockdata.rs rename to p2p/src/message_blockdata.rs index 3d9231204..e2c388af7 100644 --- a/bitcoin/src/p2p/message_blockdata.rs +++ b/p2p/src/message_blockdata.rs @@ -7,11 +7,10 @@ use io::{BufRead, Write}; -use crate::block::BlockHash; -use crate::consensus::encode::{self, Decodable, Encodable}; -use crate::internal_macros::impl_consensus_encoding; -use crate::p2p; -use crate::transaction::{Txid, Wtxid}; +use bitcoin::block::BlockHash; +use bitcoin::consensus::encode::{self, Decodable, Encodable}; +use bitcoin::transaction::{Txid, Wtxid}; +use crate::consensus::impl_consensus_encoding; /// An inventory item. #[derive(PartialEq, Eq, Clone, Debug, Copy, Hash, PartialOrd, Ord)] @@ -127,7 +126,7 @@ pub struct GetHeadersMessage { impl GetBlocksMessage { /// Construct a new `getblocks` message pub fn new(locator_hashes: Vec, stop_hash: BlockHash) -> GetBlocksMessage { - GetBlocksMessage { version: p2p::PROTOCOL_VERSION, locator_hashes, stop_hash } + GetBlocksMessage { version: crate::PROTOCOL_VERSION, locator_hashes, stop_hash } } } @@ -136,7 +135,7 @@ impl_consensus_encoding!(GetBlocksMessage, version, locator_hashes, stop_hash); impl GetHeadersMessage { /// Construct a new `getheaders` message pub fn new(locator_hashes: Vec, stop_hash: BlockHash) -> GetHeadersMessage { - GetHeadersMessage { version: p2p::PROTOCOL_VERSION, locator_hashes, stop_hash } + GetHeadersMessage { version: crate::PROTOCOL_VERSION, locator_hashes, stop_hash } } } @@ -147,7 +146,7 @@ mod tests { use hex_lit::hex; use super::*; - use crate::consensus::encode::{deserialize, serialize}; + use bitcoin::consensus::encode::{deserialize, serialize}; #[test] fn getblocks_message() { diff --git a/bitcoin/src/p2p/message_bloom.rs b/p2p/src/message_bloom.rs similarity index 89% rename from bitcoin/src/p2p/message_bloom.rs rename to p2p/src/message_bloom.rs index 2a75f9a64..69f079da2 100644 --- a/bitcoin/src/p2p/message_bloom.rs +++ b/p2p/src/message_bloom.rs @@ -6,8 +6,8 @@ use io::{BufRead, Write}; -use crate::consensus::{self, encode, Decodable, Encodable, ReadExt}; -use crate::internal_macros::impl_consensus_encoding; +use bitcoin::consensus::{encode, Decodable, Encodable, ReadExt}; +use crate::consensus::impl_consensus_encoding; /// `filterload` message sets the current bloom filter #[derive(Clone, PartialEq, Eq, Debug)] @@ -52,7 +52,7 @@ impl Decodable for BloomFlags { 0 => BloomFlags::None, 1 => BloomFlags::All, 2 => BloomFlags::PubkeyOnly, - _ => return Err(consensus::parse_failed_error("unknown bloom flag")), + _ => return Err(crate::consensus::parse_failed_error("unknown bloom flag")), }) } } diff --git a/bitcoin/src/p2p/message_compact_blocks.rs b/p2p/src/message_compact_blocks.rs similarity index 95% rename from bitcoin/src/p2p/message_compact_blocks.rs rename to p2p/src/message_compact_blocks.rs index 333a8c243..9b6735c0d 100644 --- a/bitcoin/src/p2p/message_compact_blocks.rs +++ b/p2p/src/message_compact_blocks.rs @@ -3,8 +3,8 @@ //! //! BIP152 Compact Blocks network messages -use crate::bip152; -use crate::internal_macros::impl_consensus_encoding; +use bitcoin::bip152; +use crate::consensus::impl_consensus_encoding; /// sendcmpct message #[derive(PartialEq, Eq, Clone, Debug, Copy, PartialOrd, Ord, Hash)] diff --git a/bitcoin/src/p2p/message_filter.rs b/p2p/src/message_filter.rs similarity index 95% rename from bitcoin/src/p2p/message_filter.rs rename to p2p/src/message_filter.rs index 344e2d0a8..65c9c3d7c 100644 --- a/bitcoin/src/p2p/message_filter.rs +++ b/p2p/src/message_filter.rs @@ -6,9 +6,9 @@ use units::BlockHeight; -use crate::bip158::{FilterHash, FilterHeader}; -use crate::block::BlockHash; -use crate::internal_macros::impl_consensus_encoding; +use bitcoin::bip158::{FilterHash, FilterHeader}; +use bitcoin::block::BlockHash; +use crate::consensus::impl_consensus_encoding; /// getcfilters message #[derive(PartialEq, Eq, Clone, Debug)] diff --git a/bitcoin/src/p2p/message_network.rs b/p2p/src/message_network.rs similarity index 94% rename from bitcoin/src/p2p/message_network.rs rename to p2p/src/message_network.rs index d4a41b9e1..258221a0c 100644 --- a/bitcoin/src/p2p/message_network.rs +++ b/p2p/src/message_network.rs @@ -4,16 +4,15 @@ //! //! This module defines network messages which describe peers and their //! capabilities. +use std::borrow::Cow; use hashes::sha256d; use io::{BufRead, Write}; -use crate::consensus::{self, encode, Decodable, Encodable, ReadExt}; -use crate::internal_macros::impl_consensus_encoding; -use crate::p2p; -use crate::p2p::address::Address; -use crate::p2p::ServiceFlags; -use crate::prelude::{Cow, String}; +use bitcoin::consensus::{encode, Decodable, Encodable, ReadExt}; +use crate::address::Address; +use crate::consensus::impl_consensus_encoding; +use crate::ServiceFlags; // Some simple messages @@ -61,7 +60,7 @@ impl VersionMessage { start_height: i32, ) -> VersionMessage { VersionMessage { - version: p2p::PROTOCOL_VERSION, + version: crate::PROTOCOL_VERSION, services, timestamp, receiver, @@ -126,7 +125,7 @@ impl Decodable for RejectReason { 0x41 => RejectReason::Dust, 0x42 => RejectReason::Fee, 0x43 => RejectReason::Checkpoint, - _ => return Err(consensus::parse_failed_error("unknown reject code")), + _ => return Err(crate::consensus::parse_failed_error("unknown reject code")), }) } } @@ -151,7 +150,7 @@ mod tests { use hex_lit::hex; use super::*; - use crate::consensus::encode::{deserialize, serialize}; + use bitcoin::consensus::encode::{deserialize, serialize}; #[test] fn version_message_test() { From f6dea36e313c59c3dca534aba2b7de459f094e9f Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 19 Jun 2025 13:11:59 +1000 Subject: [PATCH 2/6] units: Make error constructor private We typically do not want to have public constructors on error types. Currently we do on the `TimeOverflowError` so that we can call it in `primitives` but it turns out we can just use `NumberOf512Seconds` constructors instead. --- primitives/src/sequence.rs | 16 +++++----------- units/src/locktime/relative.rs | 12 ------------ 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/primitives/src/sequence.rs b/primitives/src/sequence.rs index 7d68c7cdf..ecad225dc 100644 --- a/primitives/src/sequence.rs +++ b/primitives/src/sequence.rs @@ -20,7 +20,7 @@ use core::fmt; use arbitrary::{Arbitrary, Unstructured}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use units::locktime::relative::TimeOverflowError; +use units::locktime::relative::{NumberOf512Seconds, TimeOverflowError}; use units::parse::{self, PrefixedHexError, UnprefixedHexError}; use crate::locktime::relative; @@ -156,11 +156,8 @@ impl Sequence { /// Will return an error if the input cannot be encoded in 16 bits. #[inline] pub fn from_seconds_floor(seconds: u32) -> Result { - if let Ok(interval) = u16::try_from(seconds / 512) { - Ok(Sequence::from_512_second_intervals(interval)) - } else { - Err(TimeOverflowError::new(seconds)) - } + let intervals = NumberOf512Seconds::from_seconds_floor(seconds)?; + Ok(Sequence::from_512_second_intervals(intervals.to_512_second_intervals())) } /// Constructs a new relative lock-time from seconds, converting the seconds into 512 second @@ -169,11 +166,8 @@ impl Sequence { /// Will return an error if the input cannot be encoded in 16 bits. #[inline] pub fn from_seconds_ceil(seconds: u32) -> Result { - if let Ok(interval) = u16::try_from((seconds + 511) / 512) { - Ok(Sequence::from_512_second_intervals(interval)) - } else { - Err(TimeOverflowError::new(seconds)) - } + let intervals = NumberOf512Seconds::from_seconds_ceil(seconds)?; + Ok(Sequence::from_512_second_intervals(intervals.to_512_second_intervals())) } /// Constructs a new sequence from a u32 value. diff --git a/units/src/locktime/relative.rs b/units/src/locktime/relative.rs index f3dfba28a..5edafe01e 100644 --- a/units/src/locktime/relative.rs +++ b/units/src/locktime/relative.rs @@ -211,18 +211,6 @@ pub struct TimeOverflowError { pub(crate) seconds: u32, } -impl TimeOverflowError { - /// Constructs a new `TimeOverflowError` using `seconds`. - /// - /// # Panics - /// - /// If `seconds` would not actually overflow a `u16`. - pub fn new(seconds: u32) -> Self { - assert!(u16::try_from((seconds + 511) / 512).is_err()); - Self { seconds } - } -} - impl fmt::Display for TimeOverflowError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( From dc5249aae6d5c1375f563cf54a5d92cc8dfa3542 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 19 Jun 2025 12:43:23 +1000 Subject: [PATCH 3/6] Use new type names instead of deprecated aliases Recently we changed the names of some locktime types but kept aliases to the original names. To assist with ongoing maintenance lets use the new names already. Internal change only. --- units/tests/str.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/units/tests/str.rs b/units/tests/str.rs index 8bc714683..713fc15cd 100644 --- a/units/tests/str.rs +++ b/units/tests/str.rs @@ -40,14 +40,14 @@ check! { lock_by_height_absolute_min, absolute::Height, absolute::Height::MIN, "0"; lock_by_height_absolute_max, absolute::Height, absolute::Height::MAX, "499999999"; - lock_by_height_relative_min, relative::Height, relative::Height::MIN, "0"; - lock_by_height_relative_max, relative::Height, relative::Height::MAX, "65535"; + lock_by_height_relative_min, relative::NumberOfBlocks, relative::NumberOfBlocks::MIN, "0"; + lock_by_height_relative_max, relative::NumberOfBlocks, relative::NumberOfBlocks::MAX, "65535"; - lock_by_time_absolute_min, absolute::Time, absolute::Time::MIN, "500000000"; - lock_by_time_absolute_max, absolute::Time, absolute::Time::MAX, "4294967295"; + lock_by_time_absolute_min, absolute::MedianTimePast, absolute::MedianTimePast::MIN, "500000000"; + lock_by_time_absolute_max, absolute::MedianTimePast, absolute::MedianTimePast::MAX, "4294967295"; - lock_by_time_relative_min, relative::Time, relative::Time::MIN, "0"; - lock_by_time_relative_max, relative::Time, relative::Time::MAX, "65535"; + lock_by_time_relative_min, relative::NumberOf512Seconds, relative::NumberOf512Seconds::MIN, "0"; + lock_by_time_relative_max, relative::NumberOf512Seconds, relative::NumberOf512Seconds::MAX, "65535"; weight_min, Weight, Weight::MIN, "0"; weight_max, Weight, Weight::MAX, "18446744073709551615"; From f746aecb61456f1eb97280e1d091434250832e72 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 19 Jun 2025 14:04:55 +1000 Subject: [PATCH 4/6] Use NumberOfBlocks in rustdoc --- units/src/block.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/units/src/block.rs b/units/src/block.rs index 6ba9cb838..f17e8202f 100644 --- a/units/src/block.rs +++ b/units/src/block.rs @@ -6,7 +6,7 @@ //! //! These are general types for abstracting over block heights, they are not designed to use with //! lock times. If you are creating lock times you should be using the -//! [`locktime::absolute::Height`] and [`locktime::relative::Height`] types. +//! [`locktime::absolute::Height`] and [`locktime::relative::NumberOfBlocks`] types. //! //! The difference between these types and the locktime types is that these types are thin wrappers //! whereas the locktime types contain more complex locktime specific abstractions. From a6ab5c9fd0516f03f67a8075affed18c8c6b7245 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 19 Jun 2025 12:37:36 +1000 Subject: [PATCH 5/6] Implement Arbitrary for result types Implement `Arbitrary` for the `NumOpResult` and `MathOp` types from the `result` module. --- units/src/result.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/units/src/result.rs b/units/src/result.rs index 36f9094e0..18f968ea5 100644 --- a/units/src/result.rs +++ b/units/src/result.rs @@ -5,6 +5,8 @@ use core::convert::Infallible; use core::fmt; +#[cfg(feature = "arbitrary")] +use arbitrary::{Arbitrary, Unstructured}; use NumOpResult as R; use crate::{Amount, FeeRate, SignedAmount, Weight}; @@ -320,6 +322,32 @@ impl fmt::Display for MathOp { } } +#[cfg(feature = "arbitrary")] +impl<'a, T: Arbitrary<'a>> Arbitrary<'a> for NumOpResult { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let choice = u.int_in_range(0..=1)?; + match choice { + 0 => Ok(NumOpResult::Valid(T::arbitrary(u)?)), + _ => Ok(NumOpResult::Error(NumOpError(MathOp::arbitrary(u)?))), + } + } +} + +#[cfg(feature = "arbitrary")] +impl<'a> Arbitrary<'a> for MathOp { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let choice = u.int_in_range(0..=5)?; + match choice { + 0 => Ok(MathOp::Add), + 1 => Ok(MathOp::Sub), + 2 => Ok(MathOp::Mul), + 3 => Ok(MathOp::Div), + 4 => Ok(MathOp::Rem), + _ => Ok(MathOp::Neg), + } + } +} + #[cfg(test)] mod tests { use crate::MathOp; From 2b07f59545de16b66b3bf8ee988757c2d0c14afb Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Wed, 18 Jun 2025 10:45:01 +1000 Subject: [PATCH 6/6] units: Fix up the api test A lot has changed over the last few months. Fix up the API integration test to match the current state of the crate. --- units/tests/api.rs | 185 ++++++++++++++++++++++++--------------------- 1 file changed, 99 insertions(+), 86 deletions(-) diff --git a/units/tests/api.rs b/units/tests/api.rs index f3115ecfc..c5db6da46 100644 --- a/units/tests/api.rs +++ b/units/tests/api.rs @@ -12,39 +12,49 @@ #[cfg(feature = "arbitrary")] use arbitrary::{Arbitrary, Unstructured}; // These imports test "typical" usage by user code. -use bitcoin_units::locktime::{absolute, relative}; // Typical usage is `absolute::Height`. +use bitcoin_units::locktime::{absolute, relative}; // Typical usage is `absolute::LockTime`. use bitcoin_units::{ - amount, block, fee_rate, locktime, parse, weight, Amount, BlockHeight, BlockInterval, BlockMtp, - BlockMtpInterval, BlockTime, CheckedSum, FeeRate, SignedAmount, Weight, + amount, block, fee_rate, locktime, parse, time, weight, Amount, BlockHeight, + BlockHeightInterval, BlockMtp, BlockMtpInterval, BlockTime, CheckedSum, FeeRate, MathOp, + NumOpResult, SignedAmount, Weight, }; /// A struct that includes all public non-error enums. #[derive(Debug)] // All public types implement Debug (C-DEBUG). struct Enums { a: amount::Denomination, + b: NumOpResult, + c: MathOp, } impl Enums { - fn new() -> Self { Self { a: amount::Denomination::Bitcoin } } + fn new() -> Self { + Self { + a: amount::Denomination::Bitcoin, + b: NumOpResult::Valid(Amount::MAX), + c: MathOp::Add, + } + } } /// A struct that includes all public non-error structs. #[derive(Debug)] // All public types implement Debug (C-DEBUG). struct Structs { - a: Amount, + // Full path to show alphabetic sort order. + a: amount::Amount, b: amount::Display, - c: SignedAmount, - d: BlockHeight, - e: BlockInterval, - f: FeeRate, - g: absolute::Height, - h: absolute::MedianTimePast, - i: relative::Height, - j: relative::Time, - k: Weight, - l: BlockTime, - m: BlockMtp, - n: BlockMtpInterval, + c: amount::SignedAmount, + d: block::BlockHeight, + e: block::BlockHeightInterval, + f: block::BlockMtp, + g: block::BlockMtpInterval, + h: fee_rate::FeeRate, + i: locktime::absolute::Height, + j: locktime::absolute::MedianTimePast, + k: locktime::relative::NumberOf512Seconds, + l: locktime::relative::NumberOfBlocks, + m: time::BlockTime, + n: weight::Weight, } impl Structs { @@ -54,16 +64,16 @@ impl Structs { b: Amount::MAX.display_in(amount::Denomination::Bitcoin), c: SignedAmount::MAX, d: BlockHeight::MAX, - e: BlockInterval::MAX, - f: FeeRate::MAX, - g: absolute::Height::MAX, - h: absolute::MedianTimePast::MAX, - i: relative::Height::MAX, - j: relative::Time::MAX, - k: Weight::MAX, - l: BlockTime::from_u32(u32::MAX), - m: BlockMtp::MAX, - n: BlockMtpInterval::MAX, + e: BlockHeightInterval::MAX, + f: BlockMtp::MAX, + g: BlockMtpInterval::MAX, + h: FeeRate::MAX, + i: absolute::Height::MAX, + j: absolute::MedianTimePast::MAX, + k: relative::NumberOf512Seconds::MAX, + l: relative::NumberOfBlocks::MAX, + m: BlockTime::from_u32(u32::MAX), + n: Weight::MAX, } } } @@ -83,19 +93,21 @@ impl Types { // C-COMMON-TRAITS excluding `Default` and `Display`. `Display` is done in `./str.rs`. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] struct CommonTraits { - a: Amount, - c: SignedAmount, - d: BlockHeight, - e: BlockInterval, - f: FeeRate, - g: absolute::Height, - h: absolute::MedianTimePast, - i: relative::Height, - j: relative::Time, - k: Weight, - l: BlockTime, - m: BlockMtp, - n: BlockMtpInterval, + // Full path to show alphabetic sort order. + a: amount::Amount, + // b: amount::Display, + c: amount::SignedAmount, + d: block::BlockHeight, + e: block::BlockHeightInterval, + f: block::BlockMtp, + g: block::BlockMtpInterval, + h: fee_rate::FeeRate, + i: locktime::absolute::Height, + j: locktime::absolute::MedianTimePast, + k: locktime::relative::NumberOf512Seconds, + l: locktime::relative::NumberOfBlocks, + m: time::BlockTime, + n: weight::Weight, } /// A struct that includes all types that implement `Default`. @@ -103,10 +115,10 @@ struct CommonTraits { struct Default { a: Amount, b: SignedAmount, - c: BlockInterval, - d: relative::Height, - e: relative::Time, - f: BlockMtpInterval, + c: BlockHeightInterval, + d: BlockMtpInterval, + e: relative::NumberOf512Seconds, + f: relative::NumberOfBlocks, } /// A struct that includes all public error types. @@ -124,28 +136,18 @@ struct Errors { i: amount::PossiblyConfusingDenominationError, j: amount::TooPreciseError, k: amount::UnknownDenominationError, - l: amount::InputTooLargeError, - m: amount::InvalidCharacterError, - n: amount::MissingDenominationError, - o: amount::MissingDigitsError, - p: amount::OutOfRangeError, - q: amount::ParseAmountError, - r: amount::ParseDenominationError, - s: amount::ParseError, - t: amount::PossiblyConfusingDenominationError, - u: amount::TooPreciseError, - v: amount::UnknownDenominationError, - w: block::TooBigForRelativeHeightError, - x: locktime::absolute::ConversionError, - y: locktime::absolute::Height, - z: locktime::absolute::ParseHeightError, - aa: locktime::absolute::ParseTimeError, - ab: locktime::relative::TimeOverflowError, - ac: locktime::relative::InvalidHeightError, - ad: locktime::relative::InvalidTimeError, - ae: parse::ParseIntError, - af: parse::PrefixedHexError, - ag: parse::UnprefixedHexError, + l: block::TooBigForRelativeHeightError, + #[cfg(feature = "serde")] + m: fee_rate::serde::OverflowError, + n: locktime::absolute::ConversionError, + o: locktime::absolute::ParseHeightError, + p: locktime::absolute::ParseTimeError, + q: locktime::relative::InvalidHeightError, + r: locktime::relative::InvalidTimeError, + s: locktime::relative::TimeOverflowError, + t: parse::ParseIntError, + u: parse::PrefixedHexError, + v: parse::UnprefixedHexError, } #[test] @@ -156,8 +158,8 @@ fn api_can_use_modules_from_crate_root() { #[test] fn api_can_use_types_from_crate_root() { use bitcoin_units::{ - Amount, BlockHeight, BlockInterval, BlockMtp, BlockMtpInterval, BlockTime, FeeRate, - SignedAmount, Weight, + Amount, BlockHeight, BlockHeightInterval, BlockInterval, BlockMtp, BlockMtpInterval, + BlockTime, FeeRate, MathOp, NumOpError, NumOpResult, SignedAmount, Weight, }; } @@ -173,11 +175,15 @@ fn api_can_use_all_types_from_module_amount() { #[test] fn api_can_use_all_types_from_module_block() { - use bitcoin_units::block::{BlockHeight, BlockHeightInterval, TooBigForRelativeHeightError}; + use bitcoin_units::block::{ + BlockHeight, BlockHeightInterval, BlockMtp, BlockMtpInterval, TooBigForRelativeHeightError, + }; } #[test] fn api_can_use_all_types_from_module_fee_rate() { + #[cfg(feature = "serde")] + use bitcoin_units::fee_rate::serde::OverflowError; use bitcoin_units::fee_rate::FeeRate; } @@ -190,7 +196,10 @@ fn api_can_use_all_types_from_module_locktime_absolute() { #[test] fn api_can_use_all_types_from_module_locktime_relative() { - use bitcoin_units::locktime::relative::{Height, Time, TimeOverflowError}; + use bitcoin_units::locktime::relative::{ + Height, InvalidHeightError, InvalidTimeError, NumberOf512Seconds, NumberOfBlocks, Time, + TimeOverflowError, + }; } #[test] @@ -262,10 +271,10 @@ fn regression_default() { let want = Default { a: Amount::ZERO, b: SignedAmount::ZERO, - c: BlockInterval::ZERO, - d: relative::Height::ZERO, - e: relative::Time::ZERO, - f: BlockMtpInterval::ZERO, + c: BlockHeightInterval::ZERO, + d: BlockMtpInterval::ZERO, + e: relative::NumberOf512Seconds::ZERO, + f: relative::NumberOfBlocks::ZERO, }; assert_eq!(got, want); } @@ -279,7 +288,7 @@ fn dyn_compatible() { // These traits are explicitly not dyn compatible. // b: Box, // c: Box, - // d: Box, + // d: Box, // Because of core::num::ParseIntError } } @@ -300,16 +309,16 @@ impl<'a> Arbitrary<'a> for Structs { b: Amount::MAX.display_in(amount::Denomination::Bitcoin), c: SignedAmount::arbitrary(u)?, d: BlockHeight::arbitrary(u)?, - e: BlockInterval::arbitrary(u)?, - f: FeeRate::arbitrary(u)?, - g: absolute::Height::arbitrary(u)?, - h: absolute::MedianTimePast::arbitrary(u)?, - i: relative::Height::arbitrary(u)?, - j: relative::Time::arbitrary(u)?, - k: Weight::arbitrary(u)?, - l: BlockTime::arbitrary(u)?, - m: BlockMtp::arbitrary(u)?, - n: BlockMtpInterval::arbitrary(u)?, + e: BlockHeightInterval::arbitrary(u)?, + f: BlockMtp::arbitrary(u)?, + g: BlockMtpInterval::arbitrary(u)?, + h: FeeRate::arbitrary(u)?, + i: absolute::Height::arbitrary(u)?, + j: absolute::MedianTimePast::arbitrary(u)?, + k: relative::NumberOf512Seconds::arbitrary(u)?, + l: relative::NumberOfBlocks::arbitrary(u)?, + m: BlockTime::arbitrary(u)?, + n: Weight::arbitrary(u)?, }; Ok(a) } @@ -318,7 +327,11 @@ impl<'a> Arbitrary<'a> for Structs { #[cfg(feature = "arbitrary")] impl<'a> Arbitrary<'a> for Enums { fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { - let a = Enums { a: amount::Denomination::arbitrary(u)? }; + let a = Enums { + a: amount::Denomination::arbitrary(u)?, + b: NumOpResult::::arbitrary(u)?, + c: MathOp::arbitrary(u)?, + }; Ok(a) } }