diff --git a/Cargo.lock b/Cargo.lock index e9c9cbb..0ca2a72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,7 +369,7 @@ dependencies = [ [[package]] name = "pushtx" -version = "0.2.4" +version = "0.3.0" dependencies = [ "bitcoin", "crossbeam-channel", @@ -385,7 +385,7 @@ dependencies = [ [[package]] name = "pushtx-cli" -version = "0.2.4" +version = "0.3.0" dependencies = [ "anyhow", "clap", diff --git a/README.md b/README.md index 9f98161..ae99802 100644 --- a/README.md +++ b/README.md @@ -32,21 +32,26 @@ Install with Cargo: `cargo install pushtx-cli` ### Library ```rust - // our hex-encoded transaction that we want to parse and broadcast + // this is our hex-encoded transaction that we want to parse and broadcast let tx = "6afcc7949dd500000....".parse().unwrap(); // we start the broadcast process and acquire a receiver to the info events let receiver = pushtx::broadcast(vec![tx], pushtx::Opts::default()); // start reading info events until `Done` is received - let how_many = loop { - match receiver.recv().unwrap() { - pushtx::Info::Done { broadcasts, .. } => break broadcasts, + loop { + match receiver.recv().unwrap() { + pushtx::Info::Done(Ok(report)) => { + println!("we successfully broadcast to {} peers", report.broadcasts); + break; + } + pushtx::Info::Done(Err(err)) => { + println!("we failed to broadcast to any peers, reason = {err}"); + break; + } _ => {} } - }; - - println!("we successfully broadcast to {how_many} peers"); + } ``` ### Disclaimer diff --git a/pushtx-cli/Cargo.toml b/pushtx-cli/Cargo.toml index bee32f5..00146ec 100644 --- a/pushtx-cli/Cargo.toml +++ b/pushtx-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pushtx-cli" -version = "0.2.4" +version = "0.3.0" edition = "2021" authors = ["Alfred Hodler "] license = "MIT" @@ -18,5 +18,5 @@ anyhow = "1.0.86" clap = { version = "4.5.4", features = ["derive"] } env_logger = { version = "0.11.3", default-features = false } log = "0.4.20" -pushtx = { version = "0.2.4", path = "../pushtx" } +pushtx = { version = "0.3.0", path = "../pushtx" } thiserror = "1.0.61" diff --git a/pushtx-cli/src/main.rs b/pushtx-cli/src/main.rs index 940b8ac..7010a19 100644 --- a/pushtx-cli/src/main.rs +++ b/pushtx-cli/src/main.rs @@ -15,15 +15,14 @@ use clap::Parser; /// fresh Tor circuit. Running the Tor browser in the background /// is usually sufficient for this to work. /// -/// Logging can be enabled by running the program with the -/// following environment variable: RUST_LOG=debug. -/// Available log levels are: trace, debug, info, warn, error. +/// More verbose (debug) output can be enabled by specifying the +/// -v or --verbose switch up to three times. #[derive(Parser)] -#[command(version, about, long_about, verbatim_doc_comment)] +#[command(version, about, long_about, verbatim_doc_comment, name = "pushtx")] struct Cli { - /// Connect through clearnet even if Tor is available. - #[arg(short, long)] - no_tor: bool, + /// Tor mode. Default is `try`. + #[arg(short = 'm', long)] + tor_mode: Option, /// Dry-run mode. Performs the whole process except the sending part. #[arg(short, long)] @@ -33,17 +32,33 @@ struct Cli { #[arg(short, long)] testnet: bool, - /// Zero or one paths to a file containing line-delimited hex encoded or binary transactions + /// Zero or one paths to a file containing line-delimited hex encoded transactions /// /// If not present, stdin is used instead (hex only, one tx per line). #[arg(short = 'f', long = "file", value_name = "FILE")] txs: Option, + + /// Print debug info (use multiple times for more verbosity; max 3) + #[arg(short, long, action = clap::ArgAction::Count)] + verbose: u8, } fn main() -> anyhow::Result<()> { - env_logger::init(); let cli = Cli::parse(); + let log_level = match cli.verbose { + 0 => None, + 1 => Some(log::Level::Info), + 2 => Some(log::Level::Debug), + 3.. => Some(log::Level::Trace), + }; + + if let Some(level) = log_level { + env_logger::Builder::default() + .filter_level(level.to_level_filter()) + .init(); + } + let txs: Result, Error> = match cli.txs { Some(path) => { let mut contents = String::new(); @@ -55,16 +70,19 @@ fn main() -> anyhow::Result<()> { .map(|line| pushtx::Transaction::from_hex(line).map_err(Into::into)) .collect() } - None => std::io::stdin() - .lines() - .filter_map(|line| match line { - Ok(line) if !line.trim().is_empty() => { - Some(pushtx::Transaction::from_hex(line).map_err(Into::into)) - } - Ok(_) => None, - Err(err) => Some(Err(Error::Io(err))), - }) - .collect(), + None => { + eprintln!("Go ahead and paste some hex-encoded transactions (one per line) ... "); + std::io::stdin() + .lines() + .filter_map(|line| match line { + Ok(line) if !line.trim().is_empty() => { + Some(pushtx::Transaction::from_hex(line).map_err(Into::into)) + } + Ok(_) => None, + Err(err) => Some(Err(Error::Io(err))), + }) + .collect() + } }; if cli.dry_run { @@ -86,16 +104,10 @@ fn main() -> anyhow::Result<()> { Err(err) => Err(err), }?; - let use_tor = if cli.no_tor { - pushtx::UseTor::No - } else { - pushtx::UseTor::BestEffort - }; - let receiver = broadcast( txs, Opts { - use_tor, + use_tor: cli.tor_mode.unwrap_or_default().into(), network: if cli.testnet { Network::Testnet } else { @@ -120,16 +132,15 @@ fn main() -> anyhow::Result<()> { } } Ok(Info::Broadcast { peer }) => println!("* Successful broadcast to peer {}", peer), - Ok(Info::Done { + Ok(Info::Done(Ok(Report { broadcasts, rejects, - }) => { - if broadcasts > 0 { - println!("* Done! Broadcast to {broadcasts} peers with {rejects} rejections"); - break Ok(()); - } else { - break Err(Error::FailedToBroadcast.into()); - } + }))) => { + println!("* Done! Broadcast to {broadcasts} peers with {rejects} rejections"); + break Ok(()); + } + Ok(Info::Done(Err(error))) => { + break Err(Error::FailedToBroadcast(error).into()); } Err(_) => panic!("worker thread disconnected"), } @@ -138,12 +149,34 @@ fn main() -> anyhow::Result<()> { #[derive(Debug, thiserror::Error)] enum Error { - #[error("IO error while parsing transaction(s): {0}")] + #[error("IO error while reading transaction(s): {0}")] Io(#[from] std::io::Error), - #[error("{0}")] + #[error("Error while parsing transaction(s): {0}")] Parse(#[from] pushtx::ParseTxError), #[error("Empty transaction set, did you pass at least one transaction?")] EmptyTxSet, - #[error("Failed to broadcast to any peers")] - FailedToBroadcast, + #[error("Failed to broadcast: {0}")] + FailedToBroadcast(pushtx::Error), +} + +/// Determines how to use Tor. +#[derive(Debug, Default, Clone, clap::ValueEnum)] +pub enum TorMode { + /// Use Tor if available. If not available, connect through clearnet. + #[default] + Try, + /// Do not use Tor even if available and running. + No, + /// Exclusively use Tor. If not available, do not broadcast. + Must, +} + +impl From for pushtx::TorMode { + fn from(value: TorMode) -> Self { + match value { + TorMode::Try => Self::BestEffort, + TorMode::No => Self::No, + TorMode::Must => Self::Must, + } + } } diff --git a/pushtx/Cargo.toml b/pushtx/Cargo.toml index fbfc3cb..c4086ad 100644 --- a/pushtx/Cargo.toml +++ b/pushtx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pushtx" -version = "0.2.4" +version = "0.3.0" edition = "2021" authors = ["Alfred Hodler "] license = "MIT" diff --git a/pushtx/README.md b/pushtx/README.md index 668a471..324e3d4 100644 --- a/pushtx/README.md +++ b/pushtx/README.md @@ -21,21 +21,26 @@ also works. ### Usage ```rust - // our hex-encoded transaction that we want to parse and broadcast + // this is our hex-encoded transaction that we want to parse and broadcast let tx = "6afcc7949dd500000....".parse().unwrap(); // we start the broadcast process and acquire a receiver to the info events let receiver = pushtx::broadcast(vec![tx], pushtx::Opts::default()); // start reading info events until `Done` is received - let how_many = loop { - match receiver.recv().unwrap() { - pushtx::Info::Done { broadcasts, .. } => break broadcasts, + loop { + match receiver.recv().unwrap() { + pushtx::Info::Done(Ok(report)) => { + println!("we successfully broadcast to {} peers", report.broadcasts); + break; + } + pushtx::Info::Done(Err(err)) => { + println!("we failed to broadcast to any peers, reason = {err}"); + break; + } _ => {} } - }; - - println!("we successfully broadcast to {how_many} peers"); + } ``` An executable is also available (`pushtx-cli`). diff --git a/pushtx/src/broadcast.rs b/pushtx/src/broadcast.rs index 4f886cd..000881e 100644 --- a/pushtx/src/broadcast.rs +++ b/pushtx/src/broadcast.rs @@ -5,7 +5,7 @@ use std::time::Duration; use crate::handshake::{self, Handshake}; use crate::p2p::{self, Outbox, Receiver, Sender}; -use crate::{net, seeds, FindPeerStrategy, Info, Opts, Transaction}; +use crate::{net, seeds, Error, FindPeerStrategy, Info, Opts, Report, Transaction}; use bitcoin::p2p::message::NetworkMessage; use bitcoin::p2p::message_blockdata::Inventory; use crossbeam_channel::RecvTimeoutError; @@ -31,9 +31,9 @@ impl Runner { pub fn run(self) { std::thread::spawn(move || { let (must_use_tor, proxy) = match self.opts.use_tor { - crate::UseTor::No => (false, None), - crate::UseTor::BestEffort => (false, detect_tor_proxy()), - crate::UseTor::Exclusively => (true, detect_tor_proxy()), + crate::TorMode::No => (false, None), + crate::TorMode::BestEffort => (false, detect_tor_proxy()), + crate::TorMode::Must => (true, detect_tor_proxy()), }; if self.opts.dry_run { @@ -43,10 +43,11 @@ impl Runner { log::info!("Tor proxy status: {:?}", proxy); if proxy.is_none() && must_use_tor { log::error!("Tor usage required but local proxy not found"); + let _ = self.info_tx.send(Info::Done(Err(Error::TorNotFound))); return; } - let client = p2p::client(proxy, self.opts.network); + let client = p2p::client(proxy, self.opts.network, self.opts.ua); let mut state = HashMap::new(); let _ = self.info_tx.send(Info::ResolvingPeers); @@ -202,10 +203,14 @@ impl Runner { std::thread::sleep(std::time::Duration::from_millis(500)); client.shutdown().join().unwrap().unwrap(); - let _ = self.info_tx.send(Info::Done { - broadcasts, - rejects, - }); + let done = match broadcasts.try_into() { + Ok(broadcasts) => Ok(Report { + broadcasts, + rejects, + }), + Err(_) => Err(Error::Timeout), + }; + let _ = self.info_tx.send(Info::Done(done)); }); } } diff --git a/pushtx/src/lib.rs b/pushtx/src/lib.rs index 1c9ba3b..e159099 100644 --- a/pushtx/src/lib.rs +++ b/pushtx/src/lib.rs @@ -22,14 +22,19 @@ //! let receiver = pushtx::broadcast(vec![tx], pushtx::Opts::default()); //! //! // start reading info events until `Done` is received -//! let how_many = loop { -//! match receiver.recv().unwrap() { -//! pushtx::Info::Done { broadcasts, .. } => break broadcasts, +//! loop { +//! match receiver.recv().unwrap() { +//! pushtx::Info::Done(Ok(report)) => { +//! println!("we successfully broadcast to {} peers", report.broadcasts); +//! break; +//! } +//! pushtx::Info::Done(Err(err)) => { +//! println!("we failed to broadcast to any peers, reason = {err}"); +//! break; +//! } //! _ => {} //! } -//! }; -//! -//! println!("we successfully broadcast to {how_many} peers"); +//! } //!``` mod broadcast; @@ -38,7 +43,7 @@ mod net; mod p2p; mod seeds; -use std::{net::SocketAddr, str::FromStr}; +use std::{net::SocketAddr, num::NonZeroUsize, str::FromStr}; use bitcoin::consensus::Decodable; @@ -104,7 +109,7 @@ impl std::fmt::Display for ParseTxError { /// Determines how to use Tor. The default is `BestEffort`. #[derive(Debug, Default, Clone)] -pub enum UseTor { +pub enum TorMode { /// Detects whether Tor is running locally at the usual port and attempts to use it. If no Tor /// is detected, the connection to the p2p network is established through clearnet. #[default] @@ -112,7 +117,7 @@ pub enum UseTor { /// Do not use Tor even if it is available and running. No, /// Exclusively use Tor. If it is not available, do not use clearnet. - Exclusively, + Must, } /// Defines how the initial pool of peers that we broadcast to is found. @@ -155,7 +160,7 @@ pub struct Opts { /// Which Bitcoin network to connect to. pub network: Network, /// Whether to broadcast through Tor if a local instance of it is found running. - pub use_tor: UseTor, + pub use_tor: TorMode, /// Which strategy to use to find the pool to draw peers from. pub find_peer_strategy: FindPeerStrategy, /// The maximum allowed duration for broadcasting regardless of the result. Terminates afterward. @@ -172,6 +177,9 @@ pub struct Opts { pub dry_run: bool, /// How many peers to connect to. pub target_peers: u8, + /// Custom user agent, POSIX time (secs) and block height to send during peer handshakes. + /// Exercise caution modifying this. + pub ua: Option<(String, u64, u64)>, } impl Default for Opts { @@ -184,6 +192,7 @@ impl Default for Opts { send_unsolicited: false, dry_run: false, target_peers: 10, + ua: None, } } } @@ -200,12 +209,32 @@ pub enum Info { /// A tx broadcast to a particular peer was completed. Broadcast { peer: String }, /// The broadcast process is done. - Done { - /// How many peers we managed to broadcast to. - broadcasts: usize, - /// How many rejects we got back. - rejects: usize, - }, + Done(Result), +} + +/// An informational report on a successful broadcast process. +#[derive(Debug, Clone)] +pub struct Report { + /// How many peers we managed to broadcast to. + pub broadcasts: NonZeroUsize, + /// How many rejects we got back. + pub rejects: usize, +} + +/// Possible error variants while broadcasting. +#[derive(Debug, Clone)] +pub enum Error { + TorNotFound, + Timeout, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::TorNotFound => write!(f, "Tor was required but a Tor proxy was not found"), + Error::Timeout => write!(f, "Time out"), + } + } } /// Connects to the p2p network and broadcasts a series of transactions. This runs fully in the diff --git a/pushtx/src/p2p.rs b/pushtx/src/p2p.rs index 8ff17cb..decb3eb 100644 --- a/pushtx/src/p2p.rs +++ b/pushtx/src/p2p.rs @@ -115,6 +115,7 @@ pub enum DisconnectReason { pub fn client( socks_proxy: Option, network: crate::Network, + ua: Option<(String, u64, u64)>, ) -> impl Sender + Receiver + Outbox { - client::client(socks_proxy, network) + client::client(socks_proxy, network, ua) } diff --git a/pushtx/src/p2p/client.rs b/pushtx/src/p2p/client.rs index ac66e5d..984f854 100644 --- a/pushtx/src/p2p/client.rs +++ b/pushtx/src/p2p/client.rs @@ -12,7 +12,11 @@ use crate::net; use super::protocol; -pub fn client(socks_proxy: Option, network: crate::Network) -> Client { +pub fn client( + socks_proxy: Option, + network: crate::Network, + ua: Option<(String, u64, u64)>, +) -> Client { let config = peerlink::Config { stream_config: peerlink::StreamConfig { tx_buf_min_size: 4096, @@ -44,6 +48,8 @@ pub fn client(socks_proxy: Option, network: crate::Network) -> Clien } }; + let (user_agent, timestamp, start_height) = ua.unwrap_or(("/pynode:0.0.1/".to_string(), 0, 0)); + Client { peerlink: handle, commands: Default::default(), @@ -52,7 +58,7 @@ pub fn client(socks_proxy: Option, network: crate::Network) -> Clien our_version: VersionMessage { version: 70016, services: bitcoin::p2p::ServiceFlags::NONE, - timestamp: 0, + timestamp: timestamp as i64, receiver: bitcoin::p2p::Address { services: bitcoin::p2p::ServiceFlags::NONE, address: [0; 8], @@ -64,8 +70,8 @@ pub fn client(socks_proxy: Option, network: crate::Network) -> Clien port: 0, }, nonce: fastrand::u64(..), - user_agent: "/pynode:0.0.1/".to_string(), - start_height: 0, + user_agent, + start_height: start_height as i32, relay: false, }, }