From fb922d2a68b7e1a17bd5b90c8d6fd5a808883525 Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Sun, 21 Jan 2024 19:03:26 +0800 Subject: [PATCH 01/10] Refactor test --- src/electrumx/test.rs | 48 ++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/electrumx/test.rs b/src/electrumx/test.rs index f5a4d4a..43ace08 100644 --- a/src/electrumx/test.rs +++ b/src/electrumx/test.rs @@ -1,30 +1,40 @@ +// std +use std::future::Future; +// crates.io +use tokio::runtime::Runtime; // atomicalsir use super::*; -#[tokio::test] -async fn get_by_ticker_should_work() { +fn test(f: F) +where + F: FnOnce(ElectrumX) -> Fut, + Fut: Future, +{ let _ = tracing_subscriber::fmt::try_init(); + let e = ElectrumXBuilder::testnet().build().unwrap(); - ElectrumXBuilder::testnet().build().unwrap().get_by_ticker("quark").await.unwrap(); + Runtime::new().unwrap().block_on(f(e)); } -#[tokio::test] -async fn get_ft_info_should_work() { - let _ = tracing_subscriber::fmt::try_init(); - - let e = ElectrumXBuilder::testnet().build().unwrap(); - - e.get_ft_info(e.get_by_ticker("quark").await.unwrap().atomical_id).await.unwrap(); +#[test] +fn get_by_ticker_should_work() { + test(|e| async move { + e.get_by_ticker("quark").await.unwrap(); + }); } -#[tokio::test] -async fn get_unspent_address_should_work() { - let _ = tracing_subscriber::fmt::try_init(); +#[test] +fn get_ft_info_should_work() { + test(|e| async move { + e.get_ft_info(e.get_by_ticker("quark").await.unwrap().atomical_id).await.unwrap(); + }); +} - ElectrumXBuilder::testnet() - .build() - .unwrap() - .get_unspent_address("tb1pemen3j4wvlryktkqsew8ext7wnsgqhmuzl7267rm3xk0th3gh04qr9wcec") - .await - .unwrap(); +#[test] +fn get_unspent_address_should_work() { + test(|e| async move { + e.get_unspent_address("tb1pemen3j4wvlryktkqsew8ext7wnsgqhmuzl7267rm3xk0th3gh04qr9wcec") + .await + .unwrap(); + }); } From b4e2132e4be61c00b307169e7042cc36e4e1e3d7 Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Sun, 21 Jan 2024 20:19:40 +0800 Subject: [PATCH 02/10] Part I --- src/electrumx.rs | 32 +++++++++++++++++-------------- src/electrumx/type.rs | 2 ++ src/engine/js.rs | 35 +++++++++++++++++++++++++++++++--- src/engine/rust.rs | 4 +++- src/util.rs | 44 +++++++------------------------------------ 5 files changed, 62 insertions(+), 55 deletions(-) diff --git a/src/electrumx.rs b/src/electrumx.rs index 8233432..509b29a 100644 --- a/src/electrumx.rs +++ b/src/electrumx.rs @@ -6,7 +6,7 @@ use r#type::*; // std use std::{str::FromStr, time::Duration}; // crates.io -use bitcoin::{Address, Network}; +use bitcoin::{Address, Amount, Network}; use reqwest::{Client as ReqwestClient, ClientBuilder as ReqwestClientBuilder}; use serde::{de::DeserializeOwned, Serialize}; use tokio::time; @@ -95,23 +95,27 @@ pub trait Api: Config + Http { where S: AsRef, { + let addr = address.as_ref(); + let sat = Amount::from_sat(satoshis); + loop { - for u in self.get_unspent_address(address.as_ref()).await? { - if u.atomicals.is_empty() && u.value >= satoshis { - tracing::info!( - "Detected Funding UTXO {txid}:{vout}) with value {value} for funding...", - txid = u.txid, - vout = u.vout, - value = u.value - ); - return Ok(u); - } + if let Some(u) = self + .get_unspent_address(addr) + .await? + .into_iter() + .find(|u| u.atomicals.is_empty() && u.value >= satoshis) + { + tracing::info!( + "funding UTXO detected {}:{} with a value of {} for funding purposes", + u.txid, + u.vout, + u.value + ); + return Ok(u); } tracing::info!( - "WAITING for UTXO... UNTIL {btc} BTC RECEIVED AT {addr}", - btc = satoshis as f64 / 100000000., - addr = address.as_ref() + "awaiting UTXO confirmation until {sat} BTC is received at address {addr}" ); time::sleep(Duration::from_secs(5)).await; diff --git a/src/electrumx/type.rs b/src/electrumx/type.rs index 584ad16..77e53ca 100644 --- a/src/electrumx/type.rs +++ b/src/electrumx/type.rs @@ -3,6 +3,8 @@ use std::collections::HashMap; // crates.io use serde::{Deserialize, Serialize}; +// TODO: We can remove the unused parts as much as possible. + #[derive(Debug, Serialize)] pub struct Params

where diff --git a/src/engine/js.rs b/src/engine/js.rs index 113f38f..2ad0af4 100644 --- a/src/engine/js.rs +++ b/src/engine/js.rs @@ -1,6 +1,7 @@ // std use std::{ fs::OpenOptions, + future::Future, io::{BufRead, BufReader, Write}, path::Path, process::{Command, Stdio}, @@ -9,7 +10,10 @@ use std::{ Arc, }, thread, + time::Duration, }; +// crates.io +use tokio::time; // atomicalsir use crate::{prelude::*, util, wallet::Wallet}; @@ -38,7 +42,7 @@ impl Wallet { tracing::info!("funding: {}", self.funding.address); let fee = if network == "livenet" { - let f = util::loop_fut(util::query_fee, "fee").await; + let f = loop_fut(util::query_fee, "fee").await; tracing::info!("current priority fee: {f} sat/vB"); @@ -110,7 +114,7 @@ fn execute(mut command: Command) -> Result<()> { _ => (), } - util::kill_process(pid)?; + kill_process(pid)?; break; } @@ -135,7 +139,7 @@ fn execute(mut command: Command) -> Result<()> { if l.contains("worker stopped with exit code 1") { tracing::error!("worker stopped with exit code 1; killing process"); - util::kill_process(pid)?; + kill_process(pid)?; break; } @@ -154,3 +158,28 @@ fn execute(mut command: Command) -> Result<()> { Ok(()) } + +async fn loop_fut(function: F, target: &str) -> T +where + F: Fn() -> Fut, + Fut: Future>, +{ + loop { + if let Ok(f) = function().await { + return f; + } + + tracing::error!("failed to query {target}; retrying in 1 minute"); + + time::sleep(Duration::from_secs(60)).await; + } +} + +fn kill_process(pid: u32) -> Result<()> { + #[cfg(any(target_os = "linux", target_os = "macos"))] + std::process::Command::new("kill").args(["-9", &pid.to_string()]).output()?; + #[cfg(target_os = "windows")] + std::process::Command::new("taskkill").args(["/F", "/PID", &pid.to_string()]).output()?; + + Ok(()) +} diff --git a/src/engine/rust.rs b/src/engine/rust.rs index cccf4a5..8468ce9 100644 --- a/src/engine/rust.rs +++ b/src/engine/rust.rs @@ -346,7 +346,7 @@ impl Miner { psbt.unsigned_tx.output.push(TxOut { value: Amount::ZERO, - script_pubkey: util::solution_tm_nonce_script(unixtime, seq), + script_pubkey: util::time_nonce_script(unixtime, seq), }); psbt.outputs.push(Default::default()); @@ -720,6 +720,8 @@ pub struct PayloadWrapper { #[derive(Debug, Serialize)] pub struct Payload { pub bitworkc: String, + // TODO: This field is unnecessary in the current version. + // pub bitworkr: Option, pub mint_ticker: String, pub nonce: u64, pub time: u64, diff --git a/src/util.rs b/src/util.rs index cd6381c..a9e8c68 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,8 +1,5 @@ // std -use std::{ - future::Future, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; // crates.io use bitcoin::{ opcodes::{ @@ -16,26 +13,9 @@ use bitcoin::{ use rand::Rng; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use tokio::time; // atomicalsir use crate::prelude::*; -pub async fn loop_fut(function: F, target: &str) -> T -where - F: Fn() -> Fut, - Fut: Future>, -{ - loop { - if let Ok(f) = function().await { - return f; - } - - tracing::error!("failed to query {target}; retrying in 1 minute"); - - time::sleep(Duration::from_secs(60)).await; - } -} - pub async fn query_fee() -> Result { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -50,21 +30,18 @@ pub async fn query_fee() -> Result { .fastest_fee) } -pub fn kill_process(pid: u32) -> Result<()> { - #[cfg(any(target_os = "linux", target_os = "macos"))] - std::process::Command::new("kill").args(["-9", &pid.to_string()]).output()?; - #[cfg(target_os = "windows")] - std::process::Command::new("taskkill").args(["/F", "/PID", &pid.to_string()]).output()?; - - Ok(()) -} - pub fn time_nonce() -> (u64, u64) { ( SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), rand::thread_rng().gen_range(1..10_000_000), ) } +pub fn time_nonce_script(time: u64, nonce: u32) -> ScriptBuf { + Script::builder() + .push_opcode(OP_RETURN) + .push_slice(<&PushBytes>::try_from(format!("{time}:{nonce}").as_bytes()).unwrap()) + .into_script() +} pub fn cbor(v: &T) -> Result> where @@ -168,13 +145,6 @@ fn build_reval_script_should_work() { ); } -pub fn solution_tm_nonce_script(time: u64, nonce: u32) -> ScriptBuf { - Script::builder() - .push_opcode(OP_RETURN) - .push_slice(<&PushBytes>::try_from(format!("{time}:{nonce}").as_bytes()).unwrap()) - .into_script() -} - pub fn address2scripthash(address: &Address) -> Result { let mut hasher = Sha256::new(); From 2948e0b7f582c93156eeb8a77eed6eacca3234f2 Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Mon, 22 Jan 2024 00:07:50 +0800 Subject: [PATCH 03/10] Part II --- src/electrumx.rs | 84 +++++++++++++++++++++++++++++++++++++++++++--- src/engine/rust.rs | 75 +++++++++-------------------------------- 2 files changed, 95 insertions(+), 64 deletions(-) diff --git a/src/electrumx.rs b/src/electrumx.rs index 509b29a..5632249 100644 --- a/src/electrumx.rs +++ b/src/electrumx.rs @@ -1,3 +1,6 @@ +// TODO: Make this a single library. +// TODO: Use thiserror. + #[cfg(test)] mod test; pub mod r#type; @@ -139,6 +142,8 @@ impl Api for T where T: Config + Http {} #[derive(Debug)] pub struct ElectrumX { pub client: ReqwestClient, + pub retry_period: Duration, + pub max_retries: MaxRetries, pub network: Network, pub base_uri: String, } @@ -158,25 +163,62 @@ impl Http for ElectrumX { P: Serialize, R: DeserializeOwned, { - let resp = self.client.post(uri.as_ref()).json(¶ms).send().await?.text().await?; + let u = uri.as_ref(); + + for _ in self.max_retries.clone() { + match self.client.post(u).json(¶ms).send().await { + Ok(r) => match r.json().await { + Ok(r) => return Ok(r), + Err(e) => { + tracing::error!("failed to parse response into JSON due to {e}"); + }, + }, + Err(e) => { + tracing::error!("the request to {u} failed due to {e}"); + }, + } - tracing::debug!("{resp}"); + time::sleep(self.retry_period).await; + } - Ok(serde_json::from_str(&resp)?) + Err(anyhow::anyhow!("exceeded maximum retries")) } } #[derive(Debug)] pub struct ElectrumXBuilder { + pub timeout: Duration, + pub retry_period: Duration, + pub max_retries: MaxRetries, pub network: Network, pub base_uri: String, } +// TODO: Remove this cfg. +#[allow(unused)] impl ElectrumXBuilder { #[cfg(test)] pub fn testnet() -> Self { Self { network: Network::Testnet, base_uri: "https://eptestnet.atomicals.xyz/proxy".into() } } + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + + self + } + + pub fn retry_period(mut self, retry_period: Duration) -> Self { + self.retry_period = retry_period; + + self + } + + pub fn max_retries(mut self, max_retries: MaxRetries) -> Self { + self.max_retries = max_retries; + + self + } + pub fn network(mut self, network: Network) -> Self { self.network = network; @@ -194,7 +236,9 @@ impl ElectrumXBuilder { pub fn build(self) -> Result { Ok(ElectrumX { - client: ReqwestClientBuilder::new().timeout(Duration::from_secs(30)).build()?, + client: ReqwestClientBuilder::new().timeout(self.timeout).build()?, + retry_period: self.retry_period, + max_retries: self.max_retries, network: self.network, base_uri: self.base_uri, }) @@ -202,6 +246,36 @@ impl ElectrumXBuilder { } impl Default for ElectrumXBuilder { fn default() -> Self { - Self { network: Network::Bitcoin, base_uri: "https://ep.atomicals.xyz/proxy".into() } + Self { + timeout: Duration::from_secs(30), + retry_period: Duration::from_secs(5), + max_retries: MaxRetries::Finite(5), + network: Network::Bitcoin, + base_uri: "https://ep.atomicals.xyz/proxy".into(), + } + } +} +// TODO: Remove this cfg. +#[allow(unused)] +#[derive(Debug, Clone)] +pub enum MaxRetries { + Infinite, + Finite(u8), +} +impl Iterator for MaxRetries { + type Item = (); + + fn next(&mut self) -> Option { + match self { + Self::Infinite => Some(()), + Self::Finite(n) => + if *n > 0 { + *n -= 1; + + Some(()) + } else { + None + }, + } } } diff --git a/src/engine/rust.rs b/src/engine/rust.rs index 8468ce9..4aa81f3 100644 --- a/src/engine/rust.rs +++ b/src/engine/rust.rs @@ -7,8 +7,8 @@ use std::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, }, - thread::{self, sleep, JoinHandle}, - time::{Duration, SystemTime, UNIX_EPOCH}, + thread::{self, JoinHandle}, + time::{SystemTime, UNIX_EPOCH}, }; // crates.io use bitcoin::{ @@ -63,9 +63,7 @@ struct Miner { } impl Miner { const BASE_BYTES: f64 = 10.5; - const BROADCAST_SLEEP_SECONDS: u32 = 15; const INPUT_BYTES_BASE: f64 = 57.5; - const MAX_BROADCAST_NUM: u32 = 20; const MAX_SEQUENCE: u32 = u32::MAX; // OP_RETURN size // 8-bytes value(roughly estimate), a one-byte script’s size @@ -224,37 +222,16 @@ impl Miner { // TODO: If no solution found. let commit_tx = maybe_commit_tx.lock().unwrap().take().unwrap(); - let commit_txid = commit_tx.txid(); - // tracing::info!("commit txid {}", commit_txid); - tracing::info!("Broadcasting commit tx..."); - let raw_tx = encode::serialize_hex(&commit_tx); - tracing::info!("raw tx: {}", &raw_tx); - - let mut attempts = 0; - while attempts < Self::MAX_BROADCAST_NUM { - if let Err(_) = self.api.broadcast(raw_tx.clone()).await { - tracing::info!( - "Network error, will retry to broadcast commit transaction in {} seconds...", - Self::BROADCAST_SLEEP_SECONDS - ); - sleep(Duration::from_secs(15)); - attempts += 1; - continue; - } - break; - } + let commit_tx_hex = encode::serialize_hex(&commit_tx); - if attempts < Self::MAX_BROADCAST_NUM { - tracing::info!("Successfully sent commit tx {commit_txid}"); - } else { - tracing::info!("❌ Failed to send commit tx {commit_txid}"); - return Ok(()); - } + tracing::info!("broadcasting commit transaction {commit_txid}"); + tracing::debug!("{commit_tx:#?}"); + tracing::info!("{commit_tx_hex}"); - tracing::info!("\nCommit workers have completed their tasks for the commit transaction.\n"); + // TODO?: Handle result. + self.api.broadcast(commit_tx_hex).await?; - let commit_txid = commit_tx.txid(); let commit_txid_ = self .api .wait_until_utxo( @@ -291,6 +268,7 @@ impl Miner { for i in 0..concurrency { tracing::info!("spawning reveal worker thread {i} for bitworkr"); + let secp = secp.clone(); let bitworkr = bitworkr.clone(); let funding_kp = wallet.funding.pair; @@ -332,11 +310,9 @@ impl Miner { (seq + 10000).min(seq_end) ); } - if solution_found.load(Ordering::Relaxed) { return Ok(()); } - if nonces_generated % 10000 == 0 { unixtime = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); @@ -472,34 +448,15 @@ impl Miner { psbt.extract_tx_unchecked_fee_rate() }; - let reveal_txid = reveal_tx.txid(); - tracing::info!("reveal txid {}", reveal_txid); - tracing::info!("reveal tx {reveal_tx:#?}"); - - tracing::info!("Broadcasting reveal tx..."); - let raw_tx = encode::serialize_hex(&reveal_tx); - tracing::info!("raw tx: {}", &raw_tx); - let mut attempts = 0; - while attempts < Self::MAX_BROADCAST_NUM { - if let Err(_) = self.api.broadcast(raw_tx.clone()).await { - tracing::info!( - "Network error, will retry to broadcast reveal transaction in {} seconds...", - Self::BROADCAST_SLEEP_SECONDS - ); - sleep(Duration::from_secs(15)); - attempts += 1; - continue; - } - break; - } + let reveal_tx_hex = encode::serialize_hex(&reveal_tx); - if attempts < Self::MAX_BROADCAST_NUM { - tracing::info!("✅ Successfully sent reveal tx {reveal_txid}"); - tracing::info!("✨Congratulations! Mission completed.✨"); - } else { - tracing::info!("❌ Failed to send reveal tx {reveal_txid}"); - } + tracing::info!("broadcasting reveal transaction {reveal_txid}"); + tracing::debug!("{reveal_tx:#?}"); + tracing::info!("{reveal_tx_hex}"); + + // TODO?: Handle result. + self.api.broadcast(reveal_tx_hex).await?; Ok(()) } From ea515d5418dd67b2b73e0fbe9804e7125dea55e3 Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Mon, 22 Jan 2024 03:36:25 +0800 Subject: [PATCH 04/10] Part III --- src/cli.rs | 9 +++- src/electrumx.rs | 2 +- src/engine/rust.rs | 117 +++++++++++---------------------------------- src/util.rs | 8 ++-- 4 files changed, 41 insertions(+), 95 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index c83ce6f..ea5c419 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -37,6 +37,11 @@ pub struct Cli { /// Need to provide a path to the atomicals-js repository's directory. #[arg(long, value_name = "PATH", group = "engine")] js_engine: Option, + /// Thread count. + /// + /// This adjusts the number of threads utilized by the Rust engine miner. + #[arg(long, value_name = "NUM", default_value_t = num_cpus::get() as u16)] + thread: u16, /// Network type. #[arg(value_enum, long, value_name = "NETWORK", default_value_t = Network_::Mainnet)] network: Network_, @@ -63,13 +68,13 @@ pub struct Cli { } impl Cli { pub async fn run(self) -> Result<()> { - let Cli { rust_engine, js_engine, network, max_fee, electrumx, ticker } = self; + let Cli { rust_engine, js_engine, thread, network, max_fee, electrumx, ticker } = self; let ticker = ticker.to_lowercase(); if let Some(d) = js_engine { js::run(network.as_atomical_js_network(), &electrumx, &d, &ticker, max_fee).await?; } else if let Some(d) = rust_engine { - rust::run(network.into(), &electrumx, &d, &ticker, max_fee).await?; + rust::run(thread, network.into(), &electrumx, &d, &ticker, max_fee).await?; } Ok(()) diff --git a/src/electrumx.rs b/src/electrumx.rs index 5632249..b0cc268 100644 --- a/src/electrumx.rs +++ b/src/electrumx.rs @@ -198,7 +198,7 @@ pub struct ElectrumXBuilder { impl ElectrumXBuilder { #[cfg(test)] pub fn testnet() -> Self { - Self { network: Network::Testnet, base_uri: "https://eptestnet.atomicals.xyz/proxy".into() } + Self::default().network(Network::Testnet).base_uri("https://eptestnet.atomicals.xyz/proxy") } pub fn timeout(mut self, timeout: Duration) -> Self { diff --git a/src/engine/rust.rs b/src/engine/rust.rs index 4aa81f3..e6eec20 100644 --- a/src/engine/rust.rs +++ b/src/engine/rust.rs @@ -8,7 +8,6 @@ use std::{ Arc, Mutex, }, thread::{self, JoinHandle}, - time::{SystemTime, UNIX_EPOCH}, }; // crates.io use bitcoin::{ @@ -34,13 +33,14 @@ use crate::{ }; pub async fn run( + thread: u16, network: Network, electrumx: &str, wallet_dir: &Path, ticker: &str, max_fee: u64, ) -> Result<()> { - let m = MinerBuilder { network, electrumx, wallet_dir, ticker, max_fee }.build()?; + let m = MinerBuilder { thread, network, electrumx, wallet_dir, ticker, max_fee }.build()?; #[allow(clippy::never_loop)] loop { @@ -55,6 +55,7 @@ pub async fn run( #[derive(Debug)] struct Miner { + thread: u16, network: Network, api: ElectrumX, wallets: Vec, @@ -64,24 +65,16 @@ struct Miner { impl Miner { const BASE_BYTES: f64 = 10.5; const INPUT_BYTES_BASE: f64 = 57.5; - const MAX_SEQUENCE: u32 = u32::MAX; - // OP_RETURN size - // 8-bytes value(roughly estimate), a one-byte script’s size - // actual value size depends precisely on final nonce + // Estimated 8-byte value, with a script size of one byte. + // The actual size of the value is determined by the final nonce. const OP_RETURN_BYTES: f64 = 21. + 8. + 1.; const OUTPUT_BYTES_BASE: f64 = 43.; const REVEAL_INPUT_BYTES_BASE: f64 = 66.; - const SEQ_RANGE_BUCKET: u32 = 100_000_000; async fn mine(&self, wallet: &Wallet) -> Result<()> { - let concurrency: u32 = num_cpus::get() as u32; - let seq_range_per_revealer: u32 = Self::SEQ_RANGE_BUCKET / concurrency; - let d = self.prepare_data(wallet).await?; tracing::info!("attempt to find a solution based on {d:#?}"); - tracing::info!("\nStarting commit stage mining now...\n"); - tracing::info!("Concurrency set to: {concurrency}"); let Data { secp, @@ -136,8 +129,8 @@ impl Miner { let solution_found = Arc::new(AtomicBool::new(false)); let maybe_commit_tx = Arc::new(Mutex::new(None)); - Self::sequence_ranges().into_iter().enumerate().for_each(|(i, r)| { - tracing::info!("spawning commit worker thread {i} for sequence range {r:?}"); + Self::sequence_ranges(self.thread).into_iter().enumerate().for_each(|(i, r)| { + tracing::info!("spawning commit thread {i} for sequence range {r:?}"); let secp = secp.clone(); let bitworkc = bitworkc.clone(); @@ -193,16 +186,11 @@ impl Miner { ..Default::default() }; - tracing::trace!("{psbt:#?}"); - let tx = psbt.extract_tx_unchecked_fee_rate(); let txid = tx.txid(); if txid.to_string().trim_start_matches("0x").starts_with(&bitworkc) { - tracing::info!("solution found for commit step"); - tracing::info!("commit sequence {s}"); - tracing::info!("commit txid {txid}"); - tracing::info!("commit tx {tx:#?}"); + tracing::info!("solution found for commit"); solution_found.store(true, Ordering::Relaxed); *maybe_tx.lock().unwrap() = Some(tx); @@ -215,7 +203,6 @@ impl Miner { })); }); - tracing::info!("\nStay calm and grab a drink! Commit workers have started mining...\n"); for t in ts { t.join().unwrap()?; } @@ -247,9 +234,6 @@ impl Miner { let reveal_hty = TapSighashType::SinglePlusAnyoneCanPay; let reveal_lh = reveal_script.tapscript_leaf_hash(); let reveal_tx = if let Some(bitworkr) = bitworkr { - // exists bitworkr - tracing::info!("\nStarting reveal stage mining now...\n"); - tracing::info!("Concurrency set to: {concurrency}"); let psbt = Psbt::from_unsigned_tx(Transaction { version: Version::ONE, lock_time: LockTime::ZERO, @@ -260,14 +244,14 @@ impl Miner { }], output: additional_outputs, })?; + let time = util::time(); let mut ts = >>>::new(); let solution_found = Arc::new(AtomicBool::new(false)); let must_tx = Arc::new(Mutex::new(None)); - let solution_time = Arc::new(Mutex::::new(0)); - let solution_nonce = Arc::new(Mutex::::new(0)); - for i in 0..concurrency { - tracing::info!("spawning reveal worker thread {i} for bitworkr"); + // TODO: Update time after attempting all sequences. + Self::sequence_ranges(self.thread).into_iter().enumerate().for_each(|(i, r)| { + tracing::info!("spawning reveal thread {i} for sequence range {r:?}"); let secp = secp.clone(); let bitworkr = bitworkr.clone(); @@ -278,51 +262,18 @@ impl Miner { let psbt = psbt.clone(); let solution_found = solution_found.clone(); let must_tx = must_tx.clone(); - let solution_time = solution_time.clone(); - let solution_nonce = solution_nonce.clone(); ts.push(thread::spawn(move || { - let mut seq_start = i * seq_range_per_revealer; - let mut seq = seq_start; - let mut seq_end = seq_start + seq_range_per_revealer - 1; - if i == (concurrency - 1) { - seq_end = Self::SEQ_RANGE_BUCKET - 1; - } - - let mut unixtime = - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); - let mut nonces_generated: u32 = 0; - - loop { - if seq > seq_end { - if seq_end <= Self::MAX_SEQUENCE - Self::SEQ_RANGE_BUCKET { - seq_start += Self::SEQ_RANGE_BUCKET; - seq_end += Self::SEQ_RANGE_BUCKET; - seq = seq_start; - } else { - // reveal worker thread stop mining w/o soluton found - tracing::info!("reveal worker thread {i} traversed its range w/o solution found."); - } - } - if seq % 10000 == 0 { - tracing::trace!( - "started reveal mining for sequence: {seq} - {}", - (seq + 10000).min(seq_end) - ); - } + for s in r { if solution_found.load(Ordering::Relaxed) { return Ok(()); } - if nonces_generated % 10000 == 0 { - unixtime = - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); - } let mut psbt = psbt.clone(); psbt.unsigned_tx.output.push(TxOut { value: Amount::ZERO, - script_pubkey: util::time_nonce_script(unixtime, seq), + script_pubkey: util::time_nonce_script(time, s), }); psbt.outputs.push(Default::default()); @@ -371,37 +322,28 @@ impl Miner { let txid = tx.txid(); if txid.to_string().trim_start_matches("0x").starts_with(&bitworkr) { - tracing::info!("solution found for reveal step"); - tracing::info!("reveal sequence {seq}"); - tracing::info!("solution at time: {unixtime}, solution nonce: {seq}"); + tracing::info!("solution found for reveal"); + solution_found.store(true, Ordering::Relaxed); *must_tx.lock().unwrap() = Some(tx); - *solution_time.lock().unwrap() = unixtime; - *solution_nonce.lock().unwrap() = seq; - - tracing::info!("\nReveal workers have completed their tasks for the reveal transaction.\n"); return Ok(()); } - - seq += 1; - nonces_generated += 1; } + + Ok(()) })); - } + }); - tracing::info!( - "\nDon't despair, it still takes some time! Reveal workers have started mining...\n" - ); for t in ts { t.join().unwrap()?; } + // TODO: If no solution found. let tx = must_tx.lock().unwrap().take().unwrap(); tx } else { - // No bitworkr let mut psbt = Psbt::from_unsigned_tx(Transaction { version: Version::ONE, lock_time: LockTime::ZERO, @@ -496,7 +438,6 @@ impl Miner { let payload = PayloadWrapper { args: { let (time, nonce) = util::time_nonce(); - tracing::info!("payload time: {time}, payload nonce: {nonce}"); Payload { bitworkc: ft.mint_bitworkc.clone(), @@ -514,12 +455,11 @@ impl Miner { .add_leaf(0, reveal_script.clone())? .finalize(&secp, wallet.funding.x_only_public_key) .unwrap(); - let perform_bitworkr = if ft.mint_bitworkr.is_some() { true } else { false }; let fees = Self::fees_of( satsbyte, reveal_script.as_bytes().len(), &additional_outputs, - perform_bitworkr, + ft.mint_bitworkr.is_some(), ); let funding_utxo = self .api @@ -543,14 +483,13 @@ impl Miner { satsbyte: u64, reveal_script_len: usize, additional_outputs: &[TxOut], - perform_bitworkr: bool, + has_bitworkr: bool, ) -> Fees { let satsbyte = satsbyte as f64; let commit = { (satsbyte * (Self::BASE_BYTES + Self::INPUT_BYTES_BASE + Self::OUTPUT_BYTES_BASE)) .ceil() as u64 }; - let op_return_size_bytes = if perform_bitworkr { Self::OP_RETURN_BYTES } else { 0. }; let reveal = { let compact_input_bytes = if reveal_script_len <= 252 { 1. @@ -561,14 +500,15 @@ impl Miner { } else { 9. }; + let op_return_bytes = if has_bitworkr { Self::OP_RETURN_BYTES } else { 0. }; (satsbyte * (Self::BASE_BYTES + Self::REVEAL_INPUT_BYTES_BASE + (compact_input_bytes + reveal_script_len as f64) / 4. // + utxos.len() as f64 * Self::INPUT_BYTES_BASE - + op_return_size_bytes - + additional_outputs.len() as f64 * Self::OUTPUT_BYTES_BASE)) + + additional_outputs.len() as f64 * Self::OUTPUT_BYTES_BASE + + op_return_bytes)) .ceil() as u64 }; let outputs = additional_outputs.iter().map(|o| o.value.to_sat()).sum::(); @@ -592,9 +532,8 @@ impl Miner { } } - fn sequence_ranges() -> Vec> { - let concurrency: u32 = num_cpus::get() as u32; - let step = (Sequence::MAX.0 as f64 / concurrency as f64).ceil() as u32; + fn sequence_ranges(range_count: u16) -> Vec> { + let step = (Sequence::MAX.0 as f32 / range_count as f32).ceil() as u32; let mut ranges = Vec::new(); let mut start = 0; @@ -611,6 +550,7 @@ impl Miner { } #[derive(Debug)] struct MinerBuilder<'a> { + thread: u16, network: Network, electrumx: &'a str, wallet_dir: &'a Path, @@ -627,6 +567,7 @@ impl<'a> MinerBuilder<'a> { .collect::>()?; Ok(Miner { + thread: self.thread, network: self.network, api, wallets, diff --git a/src/util.rs b/src/util.rs index a9e8c68..f08f62b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -30,11 +30,11 @@ pub async fn query_fee() -> Result { .fastest_fee) } +pub fn time() -> u64 { + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() +} pub fn time_nonce() -> (u64, u64) { - ( - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), - rand::thread_rng().gen_range(1..10_000_000), - ) + (time(), rand::thread_rng().gen_range(1..10_000_000)) } pub fn time_nonce_script(time: u64, nonce: u32) -> ScriptBuf { Script::builder() From 3a1e95815b39107d0e462448ce93145dbc8c546d Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Mon, 22 Jan 2024 16:00:20 +0800 Subject: [PATCH 05/10] Part IV --- Cargo.lock | 18 +- Cargo.toml | 37 +- atomicals-electrumx/Cargo.toml | 25 ++ atomicals-electrumx/src/lib.rs | 339 ++++++++++++++++++ .../src}/test.rs | 2 +- .../src}/type.rs | 22 +- atomicals-electrumx/src/util.rs | 38 ++ src/electrumx.rs | 281 --------------- src/engine/rust.rs | 8 +- src/main.rs | 1 - src/util.rs | 33 +- 11 files changed, 448 insertions(+), 356 deletions(-) create mode 100644 atomicals-electrumx/Cargo.toml create mode 100644 atomicals-electrumx/src/lib.rs rename {src/electrumx => atomicals-electrumx/src}/test.rs (97%) rename {src/electrumx => atomicals-electrumx/src}/type.rs (94%) create mode 100644 atomicals-electrumx/src/util.rs delete mode 100644 src/electrumx.rs diff --git a/Cargo.lock b/Cargo.lock index 8b95e88..1e0ac62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,12 +86,29 @@ version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f840fb7195bcfc5e17ea40c26e5ce6d5b9ce5d584466e17703209657e459ae0" +[[package]] +name = "atomicals-electrumx" +version = "0.1.9" +dependencies = [ + "anyhow", + "array-bytes", + "bitcoin", + "reqwest", + "serde", + "serde_json", + "sha2", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "atomicalsir" version = "0.1.9" dependencies = [ "anyhow", "array-bytes", + "atomicals-electrumx", "bitcoin", "ciborium", "clap", @@ -101,7 +118,6 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2", "tokio", "tracing", "tracing-subscriber", diff --git a/Cargo.toml b/Cargo.toml index dd5ba5c..6fad116 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,18 +24,25 @@ vergen = { version = "8.3", features = ["build", "cargo", "git", "gitcl"] } [dependencies] # crates.io -anyhow = { version = "1.0" } -array-bytes = { version = "6.2" } -bitcoin = { version = "0.31", features = ["rand-std"] } -ciborium = { version = "0.2" } -clap = { version = "4.4", features = ["color", "derive"] } -color-eyre = { version = "0.6" } -num_cpus = { version = "1.16" } -rand = { version = "0.8" } -reqwest = { version = "0.11", features = ["json", "rustls-tls"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0" } -sha2 = { version = "0.10" } -tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] } -tracing = { version = "0.1" } -tracing-subscriber = { version = "0.3" } +anyhow = { version = "1.0" } +atomicals-electrumx = { version = "0.1.9", path = "atomicals-electrumx" } +bitcoin = { version = "0.31", features = ["rand-std"] } +ciborium = { version = "0.2" } +clap = { version = "4.4", features = ["color", "derive"] } +color-eyre = { version = "0.6" } +num_cpus = { version = "1.16" } +rand = { version = "0.8" } +reqwest = { version = "0.11", features = ["json", "rustls-tls"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0" } +tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] } +tracing = { version = "0.1" } +tracing-subscriber = { version = "0.3" } + +[dev-dependencies] +array-bytes = { version = "6.2" } + +[workspace] +members = [ + "atomicals-electrumx", +] diff --git a/atomicals-electrumx/Cargo.toml b/atomicals-electrumx/Cargo.toml new file mode 100644 index 0000000..d6c12d3 --- /dev/null +++ b/atomicals-electrumx/Cargo.toml @@ -0,0 +1,25 @@ +[package] +authors = ["Xavier Lau "] +description = "Atomicals electrumx APIs." +edition = "2021" +homepage = "https://hack.ink/atomicalsir" +license = "GPL-3.0" +name = "atomicals-electrumx" +readme = "README.md" +repository = "https://github.com/hack-ink/atomicalsir" +version = "0.1.9" + +[dependencies] +# crates.io +anyhow = { version = "1.0" } +array-bytes = { version = "6.2" } +bitcoin = { version = "0.31", features = ["rand-std"] } +reqwest = { version = "0.11", features = ["json", "rustls-tls"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1.0" } +sha2 = { version = "0.10" } +tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] } +tracing = { version = "0.1" } + +[dev-dependencies] +tracing-subscriber = { version = "0.3" } diff --git a/atomicals-electrumx/src/lib.rs b/atomicals-electrumx/src/lib.rs new file mode 100644 index 0000000..7b0c12c --- /dev/null +++ b/atomicals-electrumx/src/lib.rs @@ -0,0 +1,339 @@ +#![deny(missing_docs, unused_crate_dependencies)] + +//! Atomicals electrumx APIs. + +#[cfg(test)] mod test; + +pub mod r#type; +use r#type::*; + +pub mod util; + +mod prelude { + pub use anyhow::Result; +} +use prelude::*; + +// std +use std::{future::Future, str::FromStr, time::Duration}; +// crates.io +use bitcoin::{Address, Amount, Network}; +use reqwest::{Client as ReqwestClient, ClientBuilder as ReqwestClientBuilder}; +use serde::{de::DeserializeOwned, Serialize}; +use tokio::time; + +/// Necessary configurations of the client to transform it into an API client. +pub trait Config { + /// Network type. + fn network(&self) -> &Network; + /// Base URI. + fn base_uri(&self) -> &str; +} + +/// Necessary HTTP methods of the client to transform it into an API client. +pub trait Http { + /// Send a POST request. + fn post(&self, uri: U, params: P) -> impl Future> + Send + where + U: Send + Sync + AsRef, + P: Send + Sync + Serialize, + R: DeserializeOwned; +} + +/// Atomicals electrumx APIs. +pub trait Api: Send + Sync + Config + Http { + /// Construct the API's URI. + fn uri_of(&self, uri: S) -> String + where + S: AsRef, + { + format!("{}/{}", self.base_uri(), uri.as_ref()) + } + + /// Make a request at `blockchain.atomicals.get_by_ticker`. + fn get_by_ticker(&self, ticker: S) -> impl Future> + Send + where + S: Send + Sync + AsRef, + { + async move { + Ok(self + .post::<_, _, Response>>( + self.uri_of("blockchain.atomicals.get_by_ticker"), + [ticker.as_ref()], + ) + .await? + .response + .result) + } + } + + /// Make a request at `blockchain.atomicals.get_by_id`. + fn get_ft_info( + &self, + atomical_id: S, + ) -> impl Future>> + Send + where + S: Send + Sync + AsRef, + { + async move { + Ok(self + .post::<_, _, Response>>( + self.uri_of("blockchain.atomicals.get_ft_info"), + [atomical_id.as_ref()], + ) + .await? + .response) + } + } + + /// Make a request at `blockchain.atomicals.get_by_id`. + fn get_unspent_address(&self, address: S) -> impl Future>> + Send + where + S: Send + Sync + AsRef, + { + async move { + self.get_unspent_scripthash(util::address2scripthash( + &Address::from_str(address.as_ref()).unwrap().require_network(*self.network())?, + )?) + .await + } + } + + /// Make a request at `blockchain.scripthash.listunspent`. + fn get_unspent_scripthash( + &self, + scripthash: S, + ) -> impl Future>> + Send + where + S: Send + Sync + AsRef, + { + async move { + let mut utxos = self + .post::<_, _, Response>>( + self.uri_of("blockchain.scripthash.listunspent"), + [scripthash.as_ref()], + ) + .await? + .response + .into_iter() + .map(|u| u.into()) + .collect::>(); + + utxos.sort_by(|a, b| a.value.cmp(&b.value)); + + Ok(utxos) + } + } + + /// Wait until a matching UTXO is found. + fn wait_until_utxo( + &self, + address: S, + satoshis: u64, + ) -> impl Future> + Send + where + S: Send + Sync + AsRef, + { + async move { + let a = address.as_ref(); + let ba = Amount::from_sat(satoshis); + + loop { + if let Some(u) = self + .get_unspent_address(a) + .await? + .into_iter() + .find(|u| u.atomicals.is_empty() && u.value >= satoshis) + { + tracing::info!( + "funding UTXO detected {}:{} with a value of {} for funding purposes", + u.txid, + u.vout, + u.value + ); + return Ok(u); + } + + tracing::info!( + "awaiting UTXO confirmation until {ba} BTC is received at address {a}" + ); + + time::sleep(Duration::from_secs(5)).await; + } + } + } + + // TODO: Return type. + /// Make a request at `blockchain.scripthash.get_balance`. + fn broadcast(&self, tx: S) -> impl Future> + Send + where + S: Send + Sync + AsRef, + { + async move { + self.post::<_, _, serde_json::Value>( + self.uri_of("blockchain.transaction.broadcast"), + [tx.as_ref()], + ) + .await + } + } +} +impl Api for T where T: Send + Sync + Config + Http {} + +/// Atomicals electrumx client. +#[derive(Debug)] +pub struct ElectrumX { + /// HTTP client. + pub client: ReqwestClient, + /// Retry period. + pub retry_period: Duration, + /// Maximum number of retry attempts. + pub max_retries: MaxRetries, + /// Network type. + pub network: Network, + /// Base URI. + pub base_uri: String, +} +impl Config for ElectrumX { + fn network(&self) -> &Network { + &self.network + } + + fn base_uri(&self) -> &str { + &self.base_uri + } +} +impl Http for ElectrumX { + async fn post(&self, uri: U, params: P) -> Result + where + U: Send + Sync + AsRef, + P: Send + Sync + Serialize, + R: DeserializeOwned, + { + let u = uri.as_ref(); + + for _ in self.max_retries.clone() { + match self.client.post(u).json(¶ms).send().await { + Ok(r) => match r.json().await { + Ok(r) => return Ok(r), + Err(e) => { + tracing::error!("failed to parse response into JSON due to {e}"); + }, + }, + Err(e) => { + tracing::error!("the request to {u} failed due to {e}"); + }, + } + + time::sleep(self.retry_period).await; + } + + Err(anyhow::anyhow!("exceeded maximum retries")) + } +} + +/// Builder for [`ElectrumX`]. +#[derive(Debug)] +pub struct ElectrumXBuilder { + /// Request timeout. + pub timeout: Duration, + /// Retry period. + pub retry_period: Duration, + /// Maximum number of retry attempts. + pub max_retries: MaxRetries, + /// Network type. + pub network: Network, + /// Base URI. + pub base_uri: String, +} +impl ElectrumXBuilder { + #[cfg(test)] + fn testnet() -> Self { + Self::default().network(Network::Testnet).base_uri("https://eptestnet.atomicals.xyz/proxy") + } + + /// Set request timeout. + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = timeout; + + self + } + + /// Set retry period. + pub fn retry_period(mut self, retry_period: Duration) -> Self { + self.retry_period = retry_period; + + self + } + + /// Set maximum number of retry attempts. + pub fn max_retries(mut self, max_retries: MaxRetries) -> Self { + self.max_retries = max_retries; + + self + } + + /// Set network type. + pub fn network(mut self, network: Network) -> Self { + self.network = network; + + self + } + + /// Set base URI. + pub fn base_uri(mut self, base_uri: S) -> Self + where + S: Into, + { + self.base_uri = base_uri.into(); + + self + } + + /// Build the [`ElectrumX`] client. + pub fn build(self) -> Result { + Ok(ElectrumX { + client: ReqwestClientBuilder::new().timeout(self.timeout).build()?, + retry_period: self.retry_period, + max_retries: self.max_retries, + network: self.network, + base_uri: self.base_uri, + }) + } +} +impl Default for ElectrumXBuilder { + fn default() -> Self { + Self { + timeout: Duration::from_secs(30), + retry_period: Duration::from_secs(5), + max_retries: MaxRetries::Finite(5), + network: Network::Bitcoin, + base_uri: "https://ep.atomicals.xyz/proxy".into(), + } + } +} +/// Maximum number of retry attempts. +#[derive(Debug, Clone)] +pub enum MaxRetries { + /// Unlimited number of retries. + Infinite, + /// Limited number of retries. + Finite(u8), +} +impl Iterator for MaxRetries { + type Item = (); + + fn next(&mut self) -> Option { + match self { + Self::Infinite => Some(()), + Self::Finite(n) => + if *n > 0 { + *n -= 1; + + Some(()) + } else { + None + }, + } + } +} diff --git a/src/electrumx/test.rs b/atomicals-electrumx/src/test.rs similarity index 97% rename from src/electrumx/test.rs rename to atomicals-electrumx/src/test.rs index 43ace08..c1f7a8e 100644 --- a/src/electrumx/test.rs +++ b/atomicals-electrumx/src/test.rs @@ -2,7 +2,7 @@ use std::future::Future; // crates.io use tokio::runtime::Runtime; -// atomicalsir +// atomicals-electrumx use super::*; fn test(f: F) diff --git a/src/electrumx/type.rs b/atomicals-electrumx/src/type.rs similarity index 94% rename from src/electrumx/type.rs rename to atomicals-electrumx/src/type.rs index 77e53ca..e8356fd 100644 --- a/src/electrumx/type.rs +++ b/atomicals-electrumx/src/type.rs @@ -1,25 +1,9 @@ +#![allow(missing_docs)] + // std use std::collections::HashMap; // crates.io -use serde::{Deserialize, Serialize}; - -// TODO: We can remove the unused parts as much as possible. - -#[derive(Debug, Serialize)] -pub struct Params

-where - P: Serialize, -{ - pub params: P, -} -impl

Params

-where - P: Serialize, -{ - pub fn new(params: P) -> Self { - Self { params } - } -} +use serde::Deserialize; // TODO: Handle errors. #[derive(Debug, Deserialize)] diff --git a/atomicals-electrumx/src/util.rs b/atomicals-electrumx/src/util.rs new file mode 100644 index 0000000..0beaf95 --- /dev/null +++ b/atomicals-electrumx/src/util.rs @@ -0,0 +1,38 @@ +//! Atomicals electrumx utilities. + +// crates.io +use bitcoin::Address; +use sha2::{Digest, Sha256}; +// atomicals-electrumx +use crate::prelude::*; + +/// Convert an address to a scripthash. +pub fn address2scripthash(address: &Address) -> Result { + let mut hasher = Sha256::new(); + + hasher.update(address.script_pubkey()); + + let mut hash = hasher.finalize(); + + hash.reverse(); + + Ok(array_bytes::bytes2hex("", hash)) +} +#[test] +fn address2scripthash_should_work() { + // std + use std::str::FromStr; + // crates.io + use bitcoin::Network; + + assert_eq!( + address2scripthash( + &Address::from_str("bc1pqkq0rg5yjrx6u08nhmc652s33g96jmdz4gjp9d46ew6ahun7xuvqaerzsp") + .unwrap() + .require_network(Network::Bitcoin) + .unwrap() + ) + .unwrap(), + "2ae9d6353b5f9b05073e3a4def3b47ab05033d8340ffa6959917c21779f956cf" + ) +} diff --git a/src/electrumx.rs b/src/electrumx.rs deleted file mode 100644 index b0cc268..0000000 --- a/src/electrumx.rs +++ /dev/null @@ -1,281 +0,0 @@ -// TODO: Make this a single library. -// TODO: Use thiserror. - -#[cfg(test)] mod test; - -pub mod r#type; -use r#type::*; - -// std -use std::{str::FromStr, time::Duration}; -// crates.io -use bitcoin::{Address, Amount, Network}; -use reqwest::{Client as ReqwestClient, ClientBuilder as ReqwestClientBuilder}; -use serde::{de::DeserializeOwned, Serialize}; -use tokio::time; -// atomicalsir -use crate::{prelude::*, util}; - -pub trait Config { - fn network(&self) -> &Network; - fn base_uri(&self) -> &str; -} - -pub trait Http { - async fn post(&self, uri: U, params: P) -> Result - where - U: AsRef, - P: Serialize, - R: DeserializeOwned; -} - -pub trait Api: Config + Http { - fn uri_of(&self, uri: S) -> String - where - S: AsRef, - { - format!("{}/{}", self.base_uri(), uri.as_ref()) - } - - async fn get_by_ticker(&self, ticker: S) -> Result - where - S: AsRef, - { - Ok(self - .post::<_, _, Response>>( - self.uri_of("blockchain.atomicals.get_by_ticker"), - Params::new([ticker.as_ref()]), - ) - .await? - .response - .result) - } - - async fn get_ft_info(&self, atomical_id: S) -> Result> - where - S: AsRef, - { - Ok(self - .post::<_, _, Response>>( - self.uri_of("blockchain.atomicals.get_ft_info"), - Params::new([atomical_id.as_ref()]), - ) - .await? - .response) - } - - async fn get_unspent_address(&self, address: S) -> Result> - where - S: AsRef, - { - self.get_unspent_scripthash(util::address2scripthash( - &Address::from_str(address.as_ref()).unwrap().require_network(*self.network())?, - )?) - .await - } - - async fn get_unspent_scripthash(&self, scripthash: S) -> Result> - where - S: AsRef, - { - let mut utxos = self - .post::<_, _, Response>>( - self.uri_of("blockchain.scripthash.listunspent"), - Params::new([scripthash.as_ref()]), - ) - .await? - .response - .into_iter() - .map(|u| u.into()) - .collect::>(); - - utxos.sort_by(|a, b| a.value.cmp(&b.value)); - - Ok(utxos) - } - - async fn wait_until_utxo(&self, address: S, satoshis: u64) -> Result - where - S: AsRef, - { - let addr = address.as_ref(); - let sat = Amount::from_sat(satoshis); - - loop { - if let Some(u) = self - .get_unspent_address(addr) - .await? - .into_iter() - .find(|u| u.atomicals.is_empty() && u.value >= satoshis) - { - tracing::info!( - "funding UTXO detected {}:{} with a value of {} for funding purposes", - u.txid, - u.vout, - u.value - ); - return Ok(u); - } - - tracing::info!( - "awaiting UTXO confirmation until {sat} BTC is received at address {addr}" - ); - - time::sleep(Duration::from_secs(5)).await; - } - } - - // TODO: Return type. - async fn broadcast(&self, tx: S) -> Result - where - S: AsRef, - { - self.post::<_, _, serde_json::Value>( - self.uri_of("blockchain.transaction.broadcast"), - Params::new([tx.as_ref()]), - ) - .await - } -} -impl Api for T where T: Config + Http {} - -#[derive(Debug)] -pub struct ElectrumX { - pub client: ReqwestClient, - pub retry_period: Duration, - pub max_retries: MaxRetries, - pub network: Network, - pub base_uri: String, -} -impl Config for ElectrumX { - fn network(&self) -> &Network { - &self.network - } - - fn base_uri(&self) -> &str { - &self.base_uri - } -} -impl Http for ElectrumX { - async fn post(&self, uri: U, params: P) -> Result - where - U: AsRef, - P: Serialize, - R: DeserializeOwned, - { - let u = uri.as_ref(); - - for _ in self.max_retries.clone() { - match self.client.post(u).json(¶ms).send().await { - Ok(r) => match r.json().await { - Ok(r) => return Ok(r), - Err(e) => { - tracing::error!("failed to parse response into JSON due to {e}"); - }, - }, - Err(e) => { - tracing::error!("the request to {u} failed due to {e}"); - }, - } - - time::sleep(self.retry_period).await; - } - - Err(anyhow::anyhow!("exceeded maximum retries")) - } -} - -#[derive(Debug)] -pub struct ElectrumXBuilder { - pub timeout: Duration, - pub retry_period: Duration, - pub max_retries: MaxRetries, - pub network: Network, - pub base_uri: String, -} -// TODO: Remove this cfg. -#[allow(unused)] -impl ElectrumXBuilder { - #[cfg(test)] - pub fn testnet() -> Self { - Self::default().network(Network::Testnet).base_uri("https://eptestnet.atomicals.xyz/proxy") - } - - pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = timeout; - - self - } - - pub fn retry_period(mut self, retry_period: Duration) -> Self { - self.retry_period = retry_period; - - self - } - - pub fn max_retries(mut self, max_retries: MaxRetries) -> Self { - self.max_retries = max_retries; - - self - } - - pub fn network(mut self, network: Network) -> Self { - self.network = network; - - self - } - - pub fn base_uri(mut self, base_uri: S) -> Self - where - S: Into, - { - self.base_uri = base_uri.into(); - - self - } - - pub fn build(self) -> Result { - Ok(ElectrumX { - client: ReqwestClientBuilder::new().timeout(self.timeout).build()?, - retry_period: self.retry_period, - max_retries: self.max_retries, - network: self.network, - base_uri: self.base_uri, - }) - } -} -impl Default for ElectrumXBuilder { - fn default() -> Self { - Self { - timeout: Duration::from_secs(30), - retry_period: Duration::from_secs(5), - max_retries: MaxRetries::Finite(5), - network: Network::Bitcoin, - base_uri: "https://ep.atomicals.xyz/proxy".into(), - } - } -} -// TODO: Remove this cfg. -#[allow(unused)] -#[derive(Debug, Clone)] -pub enum MaxRetries { - Infinite, - Finite(u8), -} -impl Iterator for MaxRetries { - type Item = (); - - fn next(&mut self) -> Option { - match self { - Self::Infinite => Some(()), - Self::Finite(n) => - if *n > 0 { - *n -= 1; - - Some(()) - } else { - None - }, - } - } -} diff --git a/src/engine/rust.rs b/src/engine/rust.rs index e6eec20..d364da0 100644 --- a/src/engine/rust.rs +++ b/src/engine/rust.rs @@ -25,12 +25,8 @@ use bitcoin::{ }; use serde::Serialize; // atomicalsir -use crate::{ - electrumx::{r#type::Utxo, Api, ElectrumX, ElectrumXBuilder}, - prelude::*, - util, - wallet::Wallet as RawWallet, -}; +use crate::{prelude::*, util, wallet::Wallet as RawWallet}; +use atomicals_electrumx::{r#type::Utxo, Api, ElectrumX, ElectrumXBuilder}; pub async fn run( thread: u16, diff --git a/src/main.rs b/src/main.rs index 723e863..e3799c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,6 @@ mod cli; use cli::Cli; -mod electrumx; mod engine; mod util; mod wallet; diff --git a/src/util.rs b/src/util.rs index f08f62b..68cec39 100644 --- a/src/util.rs +++ b/src/util.rs @@ -8,11 +8,10 @@ use bitcoin::{ }, script::PushBytes, secp256k1::Keypair, - Address, PrivateKey, Script, ScriptBuf, XOnlyPublicKey, + PrivateKey, Script, ScriptBuf, XOnlyPublicKey, }; use rand::Rng; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; // atomicalsir use crate::prelude::*; @@ -144,33 +143,3 @@ fn build_reval_script_should_work() { "207e41d0ce6e41328e17ec13076603fc9d7a1d41fb1b497af09cdfbf9b648f7480ac00630461746f6d03646d743ea16461726773a468626974776f726b63666161626263636b6d696e745f7469636b657265717561726b656e6f6e63651a0098967f6474696d651a6591da5368" ); } - -pub fn address2scripthash(address: &Address) -> Result { - let mut hasher = Sha256::new(); - - hasher.update(address.script_pubkey()); - - let mut hash = hasher.finalize(); - - hash.reverse(); - - Ok(array_bytes::bytes2hex("", hash)) -} -#[test] -fn address2scripthash_should_work() { - // std - use std::str::FromStr; - // crates.io - use bitcoin::Network; - - assert_eq!( - address2scripthash( - &Address::from_str("bc1pqkq0rg5yjrx6u08nhmc652s33g96jmdz4gjp9d46ew6ahun7xuvqaerzsp") - .unwrap() - .require_network(Network::Bitcoin) - .unwrap() - ) - .unwrap(), - "2ae9d6353b5f9b05073e3a4def3b47ab05033d8340ffa6959917c21779f956cf" - ) -} From 0c9b3bfdba3c4b1c0efd26fcb99cc68b71553fea Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Mon, 22 Jan 2024 16:51:39 +0800 Subject: [PATCH 06/10] Part V --- src/cli.rs | 17 +++++++---------- src/engine/js.rs | 22 ++++++++++++++++------ src/engine/rust.rs | 18 +++++++++++------- src/util.rs | 20 ++++++++++++++++++++ 4 files changed, 54 insertions(+), 23 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index ea5c419..545182a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,7 @@ use clap::{ ArgGroup, Parser, ValueEnum, }; // atomicalsir -use crate::{engine::*, prelude::*}; +use crate::{engine::*, prelude::*, util::FeeBound}; #[derive(Debug, Parser)] #[command( @@ -45,12 +45,9 @@ pub struct Cli { /// Network type. #[arg(value_enum, long, value_name = "NETWORK", default_value_t = Network_::Mainnet)] network: Network_, - /// Maximum acceptable fee. - /// - /// This value will be passed to atomicals-js's `--satsbyte` flag if the current network's - /// priority fee is larger then this value. - #[arg(long, value_name = "VALUE", default_value_t = 150)] - max_fee: u64, + /// Set the fee rate range to sat/vB. + #[arg(long, value_name = "MIN,MAX", value_parser = FeeBound::from_str)] + fee_bound: FeeBound, /// Specify the URI of the electrumx. /// /// Example: @@ -68,13 +65,13 @@ pub struct Cli { } impl Cli { pub async fn run(self) -> Result<()> { - let Cli { rust_engine, js_engine, thread, network, max_fee, electrumx, ticker } = self; + let Cli { rust_engine, js_engine, thread, network, fee_bound, electrumx, ticker } = self; let ticker = ticker.to_lowercase(); if let Some(d) = js_engine { - js::run(network.as_atomical_js_network(), &electrumx, &d, &ticker, max_fee).await?; + js::run(network.as_atomical_js_network(), &fee_bound, &electrumx, &d, &ticker).await?; } else if let Some(d) = rust_engine { - rust::run(thread, network.into(), &electrumx, &d, &ticker, max_fee).await?; + rust::run(thread, network.into(), &fee_bound, &electrumx, &d, &ticker).await?; } Ok(()) diff --git a/src/engine/js.rs b/src/engine/js.rs index 2ad0af4..8a826ed 100644 --- a/src/engine/js.rs +++ b/src/engine/js.rs @@ -15,14 +15,18 @@ use std::{ // crates.io use tokio::time; // atomicalsir -use crate::{prelude::*, util, wallet::Wallet}; +use crate::{ + prelude::*, + util::{self, FeeBound}, + wallet::Wallet, +}; pub async fn run( network: &str, + fee_bound: &FeeBound, electrumx: &str, atomicals_js_dir: &Path, ticker: &str, - max_fee: u64, ) -> Result<()> { let ws = Wallet::load_wallets(atomicals_js_dir.join("wallets")); @@ -31,13 +35,19 @@ pub async fn run( tracing::info!(""); tracing::info!(""); - w.mine(network, electrumx, ticker, max_fee).await?; + w.mine(network, fee_bound, electrumx, ticker).await?; } } } impl Wallet { - async fn mine(&self, network: &str, electrumx: &str, ticker: &str, max_fee: u64) -> Result<()> { + async fn mine( + &self, + network: &str, + fee_bound: &FeeBound, + electrumx: &str, + ticker: &str, + ) -> Result<()> { tracing::info!("stash: {}", self.stash.key.address); tracing::info!("funding: {}", self.funding.address); @@ -47,13 +57,13 @@ impl Wallet { tracing::info!("current priority fee: {f} sat/vB"); // Add 5 more to increase the speed. - let f = (f + 5).min(max_fee); + let f = fee_bound.apply(f + 5); tracing::info!("selected: {f} sat/vB"); f } else { - 1 + 2 }; let dir = self.path.parent().unwrap().parent().unwrap(); diff --git a/src/engine/rust.rs b/src/engine/rust.rs index d364da0..5e82837 100644 --- a/src/engine/rust.rs +++ b/src/engine/rust.rs @@ -25,18 +25,22 @@ use bitcoin::{ }; use serde::Serialize; // atomicalsir -use crate::{prelude::*, util, wallet::Wallet as RawWallet}; +use crate::{ + prelude::*, + util::{self, FeeBound}, + wallet::Wallet as RawWallet, +}; use atomicals_electrumx::{r#type::Utxo, Api, ElectrumX, ElectrumXBuilder}; pub async fn run( thread: u16, network: Network, + fee_bound: &FeeBound, electrumx: &str, wallet_dir: &Path, ticker: &str, - max_fee: u64, ) -> Result<()> { - let m = MinerBuilder { thread, network, electrumx, wallet_dir, ticker, max_fee }.build()?; + let m = MinerBuilder { thread, network, fee_bound, electrumx, wallet_dir, ticker }.build()?; #[allow(clippy::never_loop)] loop { @@ -53,10 +57,10 @@ pub async fn run( struct Miner { thread: u16, network: Network, + fee_bound: FeeBound, api: ElectrumX, wallets: Vec, ticker: String, - max_fee: u64, } impl Miner { const BASE_BYTES: f64 = 10.5; @@ -423,7 +427,7 @@ impl Miner { let secp = Secp256k1::new(); let satsbyte = if self.network == Network::Bitcoin { - (util::query_fee().await? + 5).min(self.max_fee) + self.fee_bound.apply(util::query_fee().await? + 5) } else { 2 }; @@ -548,10 +552,10 @@ impl Miner { struct MinerBuilder<'a> { thread: u16, network: Network, + fee_bound: &'a FeeBound, electrumx: &'a str, wallet_dir: &'a Path, ticker: &'a str, - max_fee: u64, } impl<'a> MinerBuilder<'a> { fn build(self) -> Result { @@ -565,10 +569,10 @@ impl<'a> MinerBuilder<'a> { Ok(Miner { thread: self.thread, network: self.network, + fee_bound: self.fee_bound.to_owned(), api, wallets, ticker: self.ticker.into(), - max_fee: self.max_fee, }) } } diff --git a/src/util.rs b/src/util.rs index 68cec39..036e661 100644 --- a/src/util.rs +++ b/src/util.rs @@ -15,6 +15,26 @@ use serde::{Deserialize, Serialize}; // atomicalsir use crate::prelude::*; +#[derive(Clone, Debug)] +pub struct FeeBound { + pub min: u64, + pub max: u64, +} +impl FeeBound { + pub fn from_str(s: &str) -> Result { + let mut s_ = s.split(','); + + let min = s_.next().ok_or(anyhow::anyhow!("expected , found {s}"))?.parse()?; + let max = s_.next().ok_or(anyhow::anyhow!("expected , found {s}"))?.parse()?; + + Ok(Self { min, max }) + } + + pub fn apply(&self, value: u64) -> u64 { + value.min(self.max).max(self.min) + } +} + pub async fn query_fee() -> Result { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] From b5b0466341f593c8db3656c52f7b071a72d8c78a Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Mon, 22 Jan 2024 23:05:44 +0800 Subject: [PATCH 07/10] Part VI --- Cargo.lock | 2 +- atomicals-electrumx/Cargo.toml | 2 +- atomicals-electrumx/src/error.rs | 17 ++ atomicals-electrumx/src/lib.rs | 19 +- atomicals-electrumx/src/test.rs | 2 +- src/engine/rust.rs | 463 +++++++++++++++---------------- src/main.rs | 4 +- 7 files changed, 262 insertions(+), 247 deletions(-) create mode 100644 atomicals-electrumx/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 1e0ac62..a06601f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,13 +90,13 @@ checksum = "6f840fb7195bcfc5e17ea40c26e5ce6d5b9ce5d584466e17703209657e459ae0" name = "atomicals-electrumx" version = "0.1.9" dependencies = [ - "anyhow", "array-bytes", "bitcoin", "reqwest", "serde", "serde_json", "sha2", + "thiserror", "tokio", "tracing", "tracing-subscriber", diff --git a/atomicals-electrumx/Cargo.toml b/atomicals-electrumx/Cargo.toml index d6c12d3..91badbf 100644 --- a/atomicals-electrumx/Cargo.toml +++ b/atomicals-electrumx/Cargo.toml @@ -11,13 +11,13 @@ version = "0.1.9" [dependencies] # crates.io -anyhow = { version = "1.0" } array-bytes = { version = "6.2" } bitcoin = { version = "0.31", features = ["rand-std"] } reqwest = { version = "0.11", features = ["json", "rustls-tls"] } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } sha2 = { version = "0.10" } +thiserror = { version = "1.0" } tokio = { version = "1.35", features = ["macros", "rt-multi-thread"] } tracing = { version = "0.1" } diff --git a/atomicals-electrumx/src/error.rs b/atomicals-electrumx/src/error.rs new file mode 100644 index 0000000..2f6a62d --- /dev/null +++ b/atomicals-electrumx/src/error.rs @@ -0,0 +1,17 @@ +//! atomicals-electrumx error collections. + +#![allow(missing_docs)] + +// crates.io +use thiserror::Error as ThisError; + +#[derive(Debug, ThisError)] +pub enum Error { + #[error("exceeded maximum retries")] + ExceededMaximumRetries, + + #[error(transparent)] + Bitcoin(#[from] bitcoin::address::Error), + #[error(transparent)] + Reqwest(#[from] reqwest::Error), +} diff --git a/atomicals-electrumx/src/lib.rs b/atomicals-electrumx/src/lib.rs index 7b0c12c..a2d2ca2 100644 --- a/atomicals-electrumx/src/lib.rs +++ b/atomicals-electrumx/src/lib.rs @@ -1,16 +1,25 @@ -#![deny(missing_docs, unused_crate_dependencies)] - //! Atomicals electrumx APIs. +#![deny(missing_docs, unused_crate_dependencies)] + #[cfg(test)] mod test; +pub mod error; + pub mod r#type; use r#type::*; pub mod util; -mod prelude { - pub use anyhow::Result; +pub mod prelude { + //! atomicals-electrumx prelude. + + pub use std::result::Result as StdResult; + + pub use super::error::{self, Error}; + + /// atomicals-electrumx `Result` type. + pub type Result = StdResult; } use prelude::*; @@ -228,7 +237,7 @@ impl Http for ElectrumX { time::sleep(self.retry_period).await; } - Err(anyhow::anyhow!("exceeded maximum retries")) + Err(Error::ExceededMaximumRetries) } } diff --git a/atomicals-electrumx/src/test.rs b/atomicals-electrumx/src/test.rs index c1f7a8e..da6c6dc 100644 --- a/atomicals-electrumx/src/test.rs +++ b/atomicals-electrumx/src/test.rs @@ -3,7 +3,7 @@ use std::future::Future; // crates.io use tokio::runtime::Runtime; // atomicals-electrumx -use super::*; +use crate::*; fn test(f: F) where diff --git a/src/engine/rust.rs b/src/engine/rust.rs index 5e82837..55ad131 100644 --- a/src/engine/rust.rs +++ b/src/engine/rust.rs @@ -18,7 +18,7 @@ use bitcoin::{ psbt::Input, secp256k1::{All, Keypair, Message, Secp256k1, XOnlyPublicKey}, sighash::{Prevouts, SighashCache}, - taproot::{LeafVersion, Signature, TaprootBuilder, TaprootSpendInfo}, + taproot::{LeafVersion, Signature, TapLeafHash, TaprootBuilder, TaprootSpendInfo}, transaction::Version, Address, Amount, Network, OutPoint, Psbt, ScriptBuf, Sequence, TapSighashType, Transaction, TxIn, TxOut, Witness, @@ -65,11 +65,13 @@ struct Miner { impl Miner { const BASE_BYTES: f64 = 10.5; const INPUT_BYTES_BASE: f64 = 57.5; + const LOCK_TIME: LockTime = LockTime::ZERO; // Estimated 8-byte value, with a script size of one byte. // The actual size of the value is determined by the final nonce. const OP_RETURN_BYTES: f64 = 21. + 8. + 1.; const OUTPUT_BYTES_BASE: f64 = 43.; const REVEAL_INPUT_BYTES_BASE: f64 = 66.; + const VERSION: Version = Version::ONE; async fn mine(&self, wallet: &Wallet) -> Result<()> { let d = self.prepare_data(wallet).await?; @@ -124,91 +126,36 @@ impl Miner { value: Amount::from_sat(funding_utxo.value), script_pubkey: funding_spk.clone(), }]; - let commit_hty = TapSighashType::Default; - let mut ts = >>>::new(); - let solution_found = Arc::new(AtomicBool::new(false)); - let maybe_commit_tx = Arc::new(Mutex::new(None)); - - Self::sequence_ranges(self.thread).into_iter().enumerate().for_each(|(i, r)| { - tracing::info!("spawning commit thread {i} for sequence range {r:?}"); - - let secp = secp.clone(); - let bitworkc = bitworkc.clone(); - let funding_kp = wallet.funding.pair.tap_tweak(&secp, None).to_inner(); - let funding_xpk = wallet.funding.x_only_public_key; - let input = commit_input.clone(); - let output = commit_output.clone(); - let prevouts = commit_prevouts.clone(); - let solution_found = solution_found.clone(); - let maybe_tx = maybe_commit_tx.clone(); - - ts.push(thread::spawn(move || { - for s in r { - if solution_found.load(Ordering::Relaxed) { - return Ok(()); - } - + let commit_tx = WorkerPool::new("commit", bitworkc, self.thread) + .activate( + ( + secp.clone(), + wallet.funding.pair.tap_tweak(&secp, None).to_inner(), + wallet.funding.x_only_public_key, + commit_input.clone(), + commit_output.clone(), + commit_prevouts.clone(), + ), + |(secp, signer, signer_xpk, input, output, prevouts), s| { let mut psbt = Psbt::from_unsigned_tx(Transaction { - version: Version::ONE, - lock_time: LockTime::ZERO, + version: Self::VERSION, + lock_time: Self::LOCK_TIME, input: { - let mut i = input.clone(); + let mut i = input.to_owned(); i[0].sequence = Sequence(s); i }, - output: output.clone(), + output: output.to_owned(), })?; - let tap_key_sig = { - let h = SighashCache::new(&psbt.unsigned_tx) - .taproot_key_spend_signature_hash( - 0, - &Prevouts::All(&prevouts), - commit_hty, - )?; - let m = Message::from_digest(h.to_byte_array()); - - Signature { sig: secp.sign_schnorr(&m, &funding_kp), hash_ty: commit_hty } - }; - - psbt.inputs[0] = Input { - witness_utxo: Some(prevouts[0].clone()), - final_script_witness: { - let mut w = Witness::new(); - - w.push(tap_key_sig.to_vec()); - - Some(w) - }, - tap_key_sig: Some(tap_key_sig), - tap_internal_key: Some(funding_xpk), - ..Default::default() - }; - - let tx = psbt.extract_tx_unchecked_fee_rate(); - let txid = tx.txid(); - - if txid.to_string().trim_start_matches("0x").starts_with(&bitworkc) { - tracing::info!("solution found for commit"); - - solution_found.store(true, Ordering::Relaxed); - *maybe_tx.lock().unwrap() = Some(tx); - - return Ok(()); - } - } - Ok(()) - })); - }); + sign_commit_psbt(secp, signer, signer_xpk, &mut psbt, prevouts)?; - for t in ts { - t.join().unwrap()?; - } - - // TODO: If no solution found. - let commit_tx = maybe_commit_tx.lock().unwrap().take().unwrap(); + Ok(psbt.extract_tx_unchecked_fee_rate()) + }, + )? + .result(); let commit_txid = commit_tx.txid(); let commit_tx_hex = encode::serialize_hex(&commit_tx); @@ -230,46 +177,33 @@ impl Miner { assert_eq!(commit_txid, commit_txid_.parse()?); - // TODO: Move common code to a single function. - let reveal_hty = TapSighashType::SinglePlusAnyoneCanPay; + let mut reveal_psbt = Psbt::from_unsigned_tx(Transaction { + version: Self::VERSION, + lock_time: Self::LOCK_TIME, + input: vec![TxIn { + previous_output: OutPoint::new(commit_txid, 0), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }], + output: additional_outputs, + })?; let reveal_lh = reveal_script.tapscript_leaf_hash(); let reveal_tx = if let Some(bitworkr) = bitworkr { - let psbt = Psbt::from_unsigned_tx(Transaction { - version: Version::ONE, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint::new(commit_txid, 0), - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - }], - output: additional_outputs, - })?; let time = util::time(); - let mut ts = >>>::new(); - let solution_found = Arc::new(AtomicBool::new(false)); - let must_tx = Arc::new(Mutex::new(None)); // TODO: Update time after attempting all sequences. - Self::sequence_ranges(self.thread).into_iter().enumerate().for_each(|(i, r)| { - tracing::info!("spawning reveal thread {i} for sequence range {r:?}"); - - let secp = secp.clone(); - let bitworkr = bitworkr.clone(); - let funding_kp = wallet.funding.pair; - let reveal_script = reveal_script.clone(); - let reveal_spend_info = reveal_spend_info.clone(); - let commit_output = commit_output.clone(); - let psbt = psbt.clone(); - let solution_found = solution_found.clone(); - let must_tx = must_tx.clone(); - - ts.push(thread::spawn(move || { - for s in r { - if solution_found.load(Ordering::Relaxed) { - return Ok(()); - } - - let mut psbt = psbt.clone(); + WorkerPool::new("reveal", bitworkr, self.thread) + .activate( + ( + secp.clone(), + wallet.funding.pair, + reveal_script.clone(), + reveal_spend_info.clone(), + commit_output[0].clone(), + reveal_psbt.clone(), + ), + move |(secp, signer, script, spend_info, output, psbt), s| { + let mut psbt = psbt.to_owned(); psbt.unsigned_tx.output.push(TxOut { value: Amount::ZERO, @@ -277,118 +211,26 @@ impl Miner { }); psbt.outputs.push(Default::default()); - let tap_key_sig = { - let h = SighashCache::new(&psbt.unsigned_tx) - .taproot_script_spend_signature_hash( - 0, - &Prevouts::One(0, commit_output[0].clone()), - reveal_lh, - reveal_hty, - )?; - let m = Message::from_digest(h.to_byte_array()); - - Signature { - sig: secp.sign_schnorr(&m, &funding_kp), - hash_ty: reveal_hty, - } - }; - - psbt.inputs[0] = Input { - // TODO: Check. - witness_utxo: Some(commit_output[0].clone()), - tap_internal_key: Some(reveal_spend_info.internal_key()), - tap_merkle_root: reveal_spend_info.merkle_root(), - final_script_witness: { - let mut w = Witness::new(); - - w.push(tap_key_sig.to_vec()); - w.push(reveal_script.as_bytes()); - w.push( - reveal_spend_info - .control_block(&( - reveal_script.clone(), - LeafVersion::TapScript, - )) - .unwrap() - .serialize(), - ); - - Some(w) - }, - ..Default::default() - }; - - let tx = psbt.extract_tx_unchecked_fee_rate(); - let txid = tx.txid(); - - if txid.to_string().trim_start_matches("0x").starts_with(&bitworkr) { - tracing::info!("solution found for reveal"); - - solution_found.store(true, Ordering::Relaxed); - *must_tx.lock().unwrap() = Some(tx); - - return Ok(()); - } - } - - Ok(()) - })); - }); + sign_reveal_psbt( + secp, signer, &mut psbt, output, &reveal_lh, spend_info, script, + )?; - for t in ts { - t.join().unwrap()?; - } - - // TODO: If no solution found. - let tx = must_tx.lock().unwrap().take().unwrap(); - - tx + Ok(psbt.extract_tx_unchecked_fee_rate()) + }, + )? + .result() } else { - let mut psbt = Psbt::from_unsigned_tx(Transaction { - version: Version::ONE, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint::new(commit_txid, 0), - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - }], - output: additional_outputs, - })?; - let tap_key_sig = { - let h = SighashCache::new(&psbt.unsigned_tx).taproot_script_spend_signature_hash( - 0, - &Prevouts::One(0, commit_output[0].clone()), - reveal_lh, - reveal_hty, - )?; - let m = Message::from_digest(h.to_byte_array()); - - Signature { sig: secp.sign_schnorr(&m, &wallet.funding.pair), hash_ty: reveal_hty } - }; - - psbt.inputs[0] = Input { - // TODO: Check. - witness_utxo: Some(commit_output[0].clone()), - tap_internal_key: Some(reveal_spend_info.internal_key()), - tap_merkle_root: reveal_spend_info.merkle_root(), - final_script_witness: { - let mut w = Witness::new(); - - w.push(tap_key_sig.to_vec()); - w.push(reveal_script.as_bytes()); - w.push( - reveal_spend_info - .control_block(&(reveal_script, LeafVersion::TapScript)) - .unwrap() - .serialize(), - ); - - Some(w) - }, - ..Default::default() - }; - - psbt.extract_tx_unchecked_fee_rate() + sign_reveal_psbt( + &secp, + &wallet.funding.pair, + &mut reveal_psbt, + &commit_output[0], + &reveal_lh, + &reveal_spend_info, + &reveal_script, + )?; + + reveal_psbt.extract_tx_unchecked_fee_rate() }; let reveal_txid = reveal_tx.txid(); let reveal_tx_hex = encode::serialize_hex(&reveal_tx); @@ -531,22 +373,6 @@ impl Miner { reveal_and_outputs: reveal + outputs, } } - - fn sequence_ranges(range_count: u16) -> Vec> { - let step = (Sequence::MAX.0 as f32 / range_count as f32).ceil() as u32; - let mut ranges = Vec::new(); - let mut start = 0; - - while start < Sequence::MAX.0 { - let end = start.checked_add(step).unwrap_or(Sequence::MAX.0); - - ranges.push(start..end); - - start = end; - } - - ranges - } } #[derive(Debug)] struct MinerBuilder<'a> { @@ -645,3 +471,166 @@ struct Fees { // reveal: u64, reveal_and_outputs: u64, } + +struct WorkerPool { + task: &'static str, + thread: u16, + difficulty: String, + result: Arc>>, +} +impl WorkerPool { + fn new(task: &'static str, difficulty: String, thread: u16) -> Self { + Self { task, difficulty, thread, result: Default::default() } + } + + fn sequence_ranges(&self) -> Vec> { + let step = (Sequence::MAX.0 as f32 / self.thread as f32).ceil() as u32; + let mut ranges = Vec::new(); + let mut start = 0; + + while start < Sequence::MAX.0 { + let end = start.checked_add(step).unwrap_or(Sequence::MAX.0); + + ranges.push(start..end); + + start = end; + } + + ranges + } + + fn activate(&self, p: P, f: F) -> Result<&Self> + where + P: 'static + Clone + Send, + F: 'static + Clone + Send + Fn(&P, u32) -> Result, + { + let task = self.task; + let mut ts = >>>::new(); + let exit = Arc::new(AtomicBool::new(false)); + + self.sequence_ranges().into_iter().enumerate().for_each(|(i, r)| { + tracing::info!("spawning {task} worker thread {i} for sequence range {r:?}"); + + let p = p.clone(); + let f = f.clone(); + let difficulty = self.difficulty.clone(); + let exit = exit.clone(); + let result = self.result.clone(); + + ts.push(thread::spawn(move || { + for s in r { + if exit.load(Ordering::Relaxed) { + return Ok(()); + } + + let tx = f(&p, s)?; + + if tx.txid().to_string().trim_start_matches("0x").starts_with(&difficulty) { + tracing::info!("solution found for {task}"); + + exit.store(true, Ordering::Relaxed); + *result.lock().unwrap() = Some(tx); + + return Ok(()); + } + } + + Ok(()) + })); + }); + + for t in ts { + t.join().unwrap()?; + } + + Ok(self) + } + + // TODO: If no solution found. + fn result(&self) -> Transaction { + self.result.lock().unwrap().take().unwrap() + } +} + +fn sign_commit_psbt( + secp: &Secp256k1, + signer: &Keypair, + signer_xpk: &XOnlyPublicKey, + psbt: &mut Psbt, + prevouts: &[TxOut], +) -> Result<()> { + let commit_hty = TapSighashType::Default; + let tap_key_sig = { + let h = SighashCache::new(&psbt.unsigned_tx).taproot_key_spend_signature_hash( + 0, + &Prevouts::All(prevouts), + commit_hty, + )?; + let m = Message::from_digest(h.to_byte_array()); + + Signature { sig: secp.sign_schnorr(&m, signer), hash_ty: commit_hty } + }; + + psbt.inputs[0] = Input { + witness_utxo: Some(prevouts[0].clone()), + final_script_witness: { + let mut w = Witness::new(); + + w.push(tap_key_sig.to_vec()); + + Some(w) + }, + tap_key_sig: Some(tap_key_sig), + tap_internal_key: Some(*signer_xpk), + ..Default::default() + }; + + Ok(()) +} + +fn sign_reveal_psbt( + secp: &Secp256k1, + signer: &Keypair, + psbt: &mut Psbt, + commit_output: &TxOut, + reveal_left_hash: &TapLeafHash, + reveal_spend_info: &TaprootSpendInfo, + reveal_script: &ScriptBuf, +) -> Result<()> { + let reveal_hty = TapSighashType::SinglePlusAnyoneCanPay; + let tap_key_sig = { + let h = SighashCache::new(&psbt.unsigned_tx).taproot_script_spend_signature_hash( + 0, + &Prevouts::One(0, commit_output.to_owned()), + *reveal_left_hash, + reveal_hty, + )?; + let m = Message::from_digest(h.to_byte_array()); + + Signature { sig: secp.sign_schnorr(&m, signer), hash_ty: reveal_hty } + }; + + psbt.inputs[0] = Input { + // TODO: Check. + witness_utxo: Some(commit_output.to_owned()), + tap_internal_key: Some(reveal_spend_info.internal_key()), + tap_merkle_root: reveal_spend_info.merkle_root(), + final_script_witness: { + let mut w = Witness::new(); + + w.push(tap_key_sig.to_vec()); + w.push(reveal_script.as_bytes()); + w.push( + reveal_spend_info + .control_block(&(reveal_script.to_owned(), LeafVersion::TapScript)) + .unwrap() + .serialize(), + ); + + Some(w) + }, + ..Default::default() + }; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index e3799c7..74e4b65 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ -#![deny(missing_docs, unused_crate_dependencies)] - //! Atomicals mining manager. +#![deny(missing_docs, unused_crate_dependencies)] + mod cli; use cli::Cli; From 4760bafde4219bae04139b5456f297066fb96803 Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Mon, 22 Jan 2024 23:28:43 +0800 Subject: [PATCH 08/10] Part VII --- src/engine/rust.rs | 13 ++++++++++--- src/util.rs | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/engine/rust.rs b/src/engine/rust.rs index 55ad131..e3ad34e 100644 --- a/src/engine/rust.rs +++ b/src/engine/rust.rs @@ -230,7 +230,8 @@ impl Miner { &reveal_script, )?; - reveal_psbt.extract_tx_unchecked_fee_rate() + // Remove this clone if not needed in the future. + reveal_psbt.clone().extract_tx_unchecked_fee_rate() }; let reveal_txid = reveal_tx.txid(); let reveal_tx_hex = encode::serialize_hex(&reveal_tx); @@ -239,8 +240,14 @@ impl Miner { tracing::debug!("{reveal_tx:#?}"); tracing::info!("{reveal_tx_hex}"); - // TODO?: Handle result. - self.api.broadcast(reveal_tx_hex).await?; + if let Err(e) = self.api.broadcast(&reveal_tx_hex).await { + tracing::error!("failed to broadcast reveal transaction due to {e}"); + + util::cache( + reveal_txid.to_string(), + format!("{reveal_tx_hex}\n{reveal_psbt:?}\n{reveal_tx:?}"), + )?; + } Ok(()) } diff --git a/src/util.rs b/src/util.rs index 036e661..1e3b78d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,5 +1,9 @@ // std -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + fs, + path::Path, + time::{SystemTime, UNIX_EPOCH}, +}; // crates.io use bitcoin::{ opcodes::{ @@ -163,3 +167,17 @@ fn build_reval_script_should_work() { "207e41d0ce6e41328e17ec13076603fc9d7a1d41fb1b497af09cdfbf9b648f7480ac00630461746f6d03646d743ea16461726773a468626974776f726b63666161626263636b6d696e745f7469636b657265717561726b656e6f6e63651a0098967f6474696d651a6591da5368" ); } + +pub fn cache(txid: S1, tx: S2) -> Result<()> +where + S1: AsRef, + S2: AsRef<[u8]>, +{ + if !Path::new("cache").is_dir() { + fs::create_dir("cache")?; + } + + fs::write(format!("cache/{}", txid.as_ref()), tx)?; + + Ok(()) +} From 9f42aacb3d6f47241e3fc44d2b204abfecf13d27 Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Mon, 22 Jan 2024 23:35:16 +0800 Subject: [PATCH 09/10] README --- README.md | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index c1b8355..a0c6fbf 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ``` Atomicals mining manager. -Usage: atomicalsir [OPTIONS] --ticker <--rust-engine |--js-engine > +Usage: atomicalsir [OPTIONS] --fee-bound --ticker <--rust-engine |--js-engine > Options: --rust-engine @@ -28,18 +28,21 @@ Options: Need to provide a path to the atomicals-js repository's directory. + --thread + Thread count. + + This adjusts the number of threads utilized by the Rust engine miner. + + [default: 16] + --network Network type [default: mainnet] [possible values: mainnet, testnet] - --max-fee - Maximum acceptable fee. - - This value will be passed to atomicals-js's `--satsbyte` flag if the current network's priority fee is larger then this value. - - [default: 150] + --fee-bound + Set the fee rate range to sat/vB --electrumx Specify the URI of the electrumx. @@ -61,11 +64,10 @@ Options: ### Warning The Rust mining engine is not fully tested; use at your own risk. -Additionally, it does not support `bitworkr`. #### Example ```sh -RUST_LOG=atomicalsir=debug cargo r -r -- --rust-engine .maintain/atomicals-js/wallets --network testnet --electrumx https://eptestnet.atomicals.xyz/proxy --ticker atomicalsir4 +RUST_LOG=atomicalsir=debug cargo r -r -- --rust-engine .maintain/atomicals-js/wallets --network testnet --fee-bound 50,150 --electrumx https://eptestnet.atomicals.xyz/proxy --ticker atomicalsir4 ``` #### [Bitcoin testnet result](https://mempool.space/testnet/tx/aabbcc683171c11c3513f88f0c601e2657982e07d4e9259c8cfa4d909eb397bc) @@ -245,16 +247,20 @@ To build from the source code, use the following commands: ```sh git clone https://hack-ink/atomicalsir cd atomicalsir -cargo build --release +cargo b -r ``` -#### Step-by-step setup +#### Step-by-step setup (js-engine) 1. Follow the installation steps for [`atomicals-js`](https://github.com/atomicals/atomicals-js#install). 2. Follow the installation steps for [`atomicalsir`](#installation). -3. Run the following command: `atomicalsir --max-fee 150 ` +3. Run the following command: `atomicalsir --js-engine --fee-bound 50,150 --ticker quark` + +#### Step-by-step setup (rust-engine) +1. Follow the installation steps for [`atomicalsir`](#installation). +2. Run the following command: `atomicalsir --rust-engine --fee-bound 50,150 --ticker quark` ### Q&A -- **Where can I find the mining log?** +- **Where can I find the js-engine mining log?** You'll find the information in `stdout.log` and `stderr.log`, which are located in the current working directory. @@ -296,6 +302,7 @@ cargo build --release - The `wallet-first` strategy mines indefinitely, switching wallets until the current wallet has more than 12 unconfirmed transactions. ## Future plan -- [ ] Update and rebuild `atomicals-js` automatically. -- [ ] Implement wallet balance detection. -- [x] Implement a mining worker in pure Rust. +- [ ] Remove js-engine. +- [x] Implement wallet balance detection. +- [x] Implement mining worker in Rust. +- [ ] Implement GPU mining worker. From f60b5b1d211fbfb287212fe9bbd4a53f1360d8ca Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Mon, 22 Jan 2024 23:35:59 +0800 Subject: [PATCH 10/10] Release `v0.2.0-rc1` --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- atomicals-electrumx/Cargo.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a06601f..2533be3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ checksum = "6f840fb7195bcfc5e17ea40c26e5ce6d5b9ce5d584466e17703209657e459ae0" [[package]] name = "atomicals-electrumx" -version = "0.1.9" +version = "0.2.0" dependencies = [ "array-bytes", "bitcoin", @@ -104,7 +104,7 @@ dependencies = [ [[package]] name = "atomicalsir" -version = "0.1.9" +version = "0.2.0" dependencies = [ "anyhow", "array-bytes", diff --git a/Cargo.toml b/Cargo.toml index 6fad116..d535447 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "GPL-3.0" name = "atomicalsir" readme = "README.md" repository = "https://github.com/hack-ink/atomicalsir" -version = "0.1.9" +version = "0.2.0" [profile.ci-dev] incremental = false @@ -25,7 +25,7 @@ vergen = { version = "8.3", features = ["build", "cargo", "git", "gitcl"] } [dependencies] # crates.io anyhow = { version = "1.0" } -atomicals-electrumx = { version = "0.1.9", path = "atomicals-electrumx" } +atomicals-electrumx = { version = "0.2.0", path = "atomicals-electrumx" } bitcoin = { version = "0.31", features = ["rand-std"] } ciborium = { version = "0.2" } clap = { version = "4.4", features = ["color", "derive"] } diff --git a/atomicals-electrumx/Cargo.toml b/atomicals-electrumx/Cargo.toml index 91badbf..e891375 100644 --- a/atomicals-electrumx/Cargo.toml +++ b/atomicals-electrumx/Cargo.toml @@ -7,7 +7,7 @@ license = "GPL-3.0" name = "atomicals-electrumx" readme = "README.md" repository = "https://github.com/hack-ink/atomicalsir" -version = "0.1.9" +version = "0.2.0" [dependencies] # crates.io