From b5b0466341f593c8db3656c52f7b071a72d8c78a Mon Sep 17 00:00:00 2001 From: Xavier Lau Date: Mon, 22 Jan 2024 23:05:44 +0800 Subject: [PATCH] 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;