From 8ed71ebb5a0408276d2fd9976684ebb022374cb1 Mon Sep 17 00:00:00 2001 From: Carlos Alejandro Gutierrez Sandoval Date: Mon, 23 Oct 2023 14:54:33 -0600 Subject: [PATCH 1/6] shared mnemonics --- romeo/src/bitcoin_client.rs | 6 +++--- romeo/src/lib.rs | 1 + romeo/src/test/mod.rs | 3 +++ 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 romeo/src/test/mod.rs diff --git a/romeo/src/bitcoin_client.rs b/romeo/src/bitcoin_client.rs index f5bc13be..961d335d 100644 --- a/romeo/src/bitcoin_client.rs +++ b/romeo/src/bitcoin_client.rs @@ -242,7 +242,6 @@ impl Client { } #[cfg(test)] -// test that wallet returns correct address mod tests { use std::path::Path; @@ -252,11 +251,12 @@ mod tests { use stacks_core::{wallet::Wallet, Network}; use super::Client; - use crate::config::Config; + use crate::{config::Config, test::MNEMONIC}; #[test] + // test that wallet returns correct address fn test_wallet_address() { - let wallet = Wallet::new("twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw").unwrap(); + let wallet = Wallet::new(MNEMONIC[0]).unwrap(); let stacks_network = Network::Testnet; let stacks_credentials = wallet.credentials(stacks_network, 0).unwrap(); diff --git a/romeo/src/lib.rs b/romeo/src/lib.rs index 8230107e..9d112046 100644 --- a/romeo/src/lib.rs +++ b/romeo/src/lib.rs @@ -14,3 +14,4 @@ pub mod stacks_client; pub mod state; pub mod system; pub mod task; +pub mod test; diff --git a/romeo/src/test/mod.rs b/romeo/src/test/mod.rs new file mode 100644 index 00000000..08084e2d --- /dev/null +++ b/romeo/src/test/mod.rs @@ -0,0 +1,3 @@ +#![cfg(test)] + +pub const MNEMONIC: [&str;1] = ["twice kind fence tip hidden tilt action fragile skin nothing glory cousin green tomorrow spring wrist shed math olympic multiply hip blue scout claw"]; From 3187149142da1939abf25cb4e128a99fe7d169a2 Mon Sep 17 00:00:00 2001 From: Carlos Alejandro Gutierrez Sandoval Date: Mon, 23 Oct 2023 14:55:03 -0600 Subject: [PATCH 2/6] clean workspace dependencies --- Cargo.toml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 814863ff..c16baf41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,13 +4,10 @@ resolver = "2" [workspace.dependencies] anyhow = "1.0" -array-bytes = "6.1.0" backoff = "0.4.0" bdk = "0.28.1" -bitcoin = "0.29.2" clap = "4.1.1" derivative = "2.2.0" -dirs = "5.0.1" futures = "0.3.28" hex = "0.4.3" log = "0.4.19" @@ -19,18 +16,14 @@ p256k1 = "5.1" rand = "0.8.5" regex = "~1.8.4" reqwest = "0.11.20" -ring = "0.16.20" ripemd = "0.1.3" rs_merkle = "1.4.1" -secp256k1 = "0.27.0" serde = "1.0" serde_json = "1.0" sha2 = "0.10.7" -stacks-core = { version = "0.1.0", path = "./stacks-core" } strum = "0.25.0" thiserror = "1.0.43" tokio = "1.32.0" -toml = "0.8.0" tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } url = "2.4.1" From 8410fa3653d27d19d483a276fdb8de187bb24404 Mon Sep 17 00:00:00 2001 From: Carlos Alejandro Gutierrez Sandoval Date: Mon, 23 Oct 2023 16:56:30 -0600 Subject: [PATCH 3/6] generic electrum blockchain --- romeo/src/bitcoin_client.rs | 134 +++++++++++++++++++++++++----------- romeo/src/system.rs | 31 +++++++-- 2 files changed, 121 insertions(+), 44 deletions(-) diff --git a/romeo/src/bitcoin_client.rs b/romeo/src/bitcoin_client.rs index 961d335d..0de0c459 100644 --- a/romeo/src/bitcoin_client.rs +++ b/romeo/src/bitcoin_client.rs @@ -1,6 +1,7 @@ //! RPC Bitcoin client use std::{ + fmt::Debug, sync::{Arc, Mutex}, time::Duration, }; @@ -9,48 +10,51 @@ use anyhow::anyhow; use bdk::{ bitcoin::{Block, PrivateKey, Script, Transaction, Txid}, bitcoincore_rpc::{self, Auth, Client as RPCClient, RpcApi}, - blockchain::{ - ConfigurableBlockchain, ElectrumBlockchain, ElectrumBlockchainConfig, - }, + blockchain::{ElectrumBlockchain, GetHeight, WalletSync}, database::MemoryDatabase, template::P2TR, SignOptions, SyncOptions, Wallet, }; +use derivative::Derivative; use sbtc_core::operations::op_return::utils::reorder_outputs; +use stacks_core::wallet::BitcoinCredentials; use tokio::{task::spawn_blocking, time::sleep}; use tracing::trace; +use url::Url; -use crate::{config::Config, event::TransactionStatus}; +use crate::event::TransactionStatus; const BLOCK_POLLING_INTERVAL: Duration = Duration::from_secs(5); +/// [Client] +pub type BitcoinClient = Client; + /// Bitcoin RPC client -#[derive(Clone)] -pub struct Client { - config: Config, - blockchain: Arc, +/// unless testing use [ElectrumBlockchain] for `ElectrumClient`. +#[derive(Derivative, Debug)] +#[derivative(Clone)] +pub struct Client { + bitcoin_url: Url, + #[derivative(Clone(bound = ""))] + blockchain: Arc, // required for fulfillment txs wallet: Arc>>, } -impl Client { +impl Client { /// Create a new RPC client - pub fn new(config: Config) -> anyhow::Result { - let url = config.electrum_node_url.as_str().to_string(); - let network = config.bitcoin_network; - let p2tr_private_key = PrivateKey::from_wif( - &config.bitcoin_credentials.wif_p2tr().to_string(), - )?; + pub fn new( + bitcoin_url: Url, + electrum_blockchain: B, + credentials: BitcoinCredentials, + ) -> anyhow::Result { + let network = credentials.network(); + let p2tr_private_key = PrivateKey::new( + credentials.private_key_p2tr(), + credentials.network(), + ); - let blockchain = - ElectrumBlockchain::from_config(&ElectrumBlockchainConfig { - url, - socks5: None, - retry: 3, - timeout: Some(10), - stop_gap: 10, - validate_domain: false, - })?; + let blockchain = electrum_blockchain; let wallet = Wallet::new( P2TR(p2tr_private_key), @@ -59,13 +63,24 @@ impl Client { MemoryDatabase::default(), )?; + if bitcoin_url.username().is_empty() { + return Err(anyhow::anyhow!("Username in {bitcoin_url} is empty")); + } + + if bitcoin_url.password().is_none() { + return Err(anyhow::anyhow!("Password in {bitcoin_url} is empty")); + } + Ok(Self { - config, + bitcoin_url, blockchain: Arc::new(blockchain), wallet: Arc::new(Mutex::new(wallet)), }) } +} +impl Client { + /// Create a new RPC client async fn execute( &self, f: F, @@ -74,19 +89,11 @@ impl Client { F: FnOnce(RPCClient) -> bitcoincore_rpc::Result + Send + 'static, T: Send + 'static, { - let mut url = self.config.bitcoin_node_url.clone(); + let mut url = self.bitcoin_url.clone(); let username = url.username().to_string(); let password = url.password().unwrap_or_default().to_string(); - if username.is_empty() { - return Err(anyhow::anyhow!("Username is empty")); - } - - if password.is_empty() { - return Err(anyhow::anyhow!("Password is empty")); - } - url.set_username("").unwrap(); url.set_password(None).unwrap(); @@ -197,7 +204,12 @@ impl Client { Ok(info.blocks as u32) } +} +impl Client +where + Arc: Send, +{ /// Sign and broadcast a transaction pub async fn sign_and_broadcast( &self, @@ -243,14 +255,16 @@ impl Client { #[cfg(test)] mod tests { - use std::path::Path; - use bdk::bitcoin::Network as BitcoinNetwork; + use bdk::{ + bitcoin::Network as BitcoinNetwork, + blockchain::{ConfigurableBlockchain, ElectrumBlockchainConfig}, + }; use blockstack_lib::vm::ContractName; use stacks_core::{wallet::Wallet, Network}; - use super::Client; + use super::*; use crate::{config::Config, test::MNEMONIC}; #[test] @@ -267,7 +281,9 @@ mod tests { let conf = Config { state_directory: Path::new("/tmp/romeo").to_path_buf(), bitcoin_credentials, - bitcoin_node_url: "http://localhost:18443".parse().unwrap(), + bitcoin_node_url: "http://user:pwd@localhost:18443" + .parse() + .unwrap(), electrum_node_url: "ssl://blockstream.info:993".parse().unwrap(), bitcoin_network: "testnet".parse().unwrap(), contract_name: ContractName::from("asset"), @@ -278,7 +294,23 @@ mod tests { strict: true, }; - let client = Client::new(conf.clone()).unwrap(); + let electrum_blockchain = + ElectrumBlockchain::from_config(&ElectrumBlockchainConfig { + url: conf.electrum_node_url.to_string(), + socks5: None, + retry: 3, + timeout: Some(10), + stop_gap: 10, + validate_domain: false, + }) + .unwrap(); + + let client = Client::new( + conf.bitcoin_node_url.clone(), + electrum_blockchain, + conf.bitcoin_credentials.clone(), + ) + .unwrap(); let client_sbtc_wallet = client .wallet @@ -299,4 +331,28 @@ mod tests { expected_sbtc_wallet ); } + + fn client( + url: &str, + ) -> anyhow::Result> { + let wallet = Wallet::new(MNEMONIC[WALLET_INDEX]).unwrap(); + let credentials = wallet + .bitcoin_credentials(BitcoinNetwork::Testnet, 0) + .unwrap(); + + Client::new(url.parse().unwrap(), (), credentials) + } + + #[test] + fn no_password() { + let broken_client = + |url: &str| client::<0>(url).expect_err("missing password"); + + let err_string = "Password in http://user@host/ is empty"; + assert_eq!(broken_client("http://user:@host").to_string(), err_string,); + assert_eq!(broken_client("http://user@host").to_string(), err_string,); + let err_string = "Username in http://host/ is empty"; + assert_eq!(broken_client("http://@host").to_string(), err_string); + assert_eq!(broken_client("http://host").to_string(), err_string,); + } } diff --git a/romeo/src/system.rs b/romeo/src/system.rs index 30d27b25..8a2d182b 100644 --- a/romeo/src/system.rs +++ b/romeo/src/system.rs @@ -2,7 +2,12 @@ use std::{fs::create_dir_all, io::Cursor}; -use bdk::bitcoin::Txid as BitcoinTxId; +use bdk::{ + bitcoin::Txid as BitcoinTxId, + blockchain::{ + ConfigurableBlockchain, ElectrumBlockchain, ElectrumBlockchainConfig, + }, +}; use blockstack_lib::{ burnchains::Txid as StacksTxId, chainstate::stacks::{ @@ -24,7 +29,7 @@ use tokio::{ use tracing::{debug, info, trace}; use crate::{ - bitcoin_client::Client as BitcoinClient, + bitcoin_client::BitcoinClient, config::Config, event::Event, proof_data::{ProofData, ProofDataClarityValues}, @@ -46,8 +51,23 @@ const DUMMY_STACKS_ID: StacksTxId = StacksTxId([ /// The system is bootstrapped by emitting the CreateAssetContract task. pub async fn run(config: Config) { let (tx, mut rx) = mpsc::channel::(128); // TODO: Make capacity configurable - let bitcoin_client = BitcoinClient::new(config.clone()) - .expect("Failed to instantiate bitcoin client"); + let electrum_blockchain = + ElectrumBlockchain::from_config(&ElectrumBlockchainConfig { + url: config.electrum_node_url.to_string(), + socks5: None, + retry: 3, + timeout: Some(10), + stop_gap: 10, + validate_domain: false, + }) + .unwrap(); + + let bitcoin_client = BitcoinClient::new( + config.electrum_node_url.clone(), + electrum_blockchain, + config.bitcoin_credentials.clone(), + ) + .expect("Failed to instantiate bitcoin client"); let stacks_client: LockedClient = StacksClient::new(config.clone(), reqwest::Client::new()).into(); @@ -62,9 +82,10 @@ pub async fn run(config: Config) { // Bootstrap for task in bootstrap_tasks { + let bitcoin_client = bitcoin_client.clone(); spawn( config.clone(), - bitcoin_client.clone(), + bitcoin_client, stacks_client.clone(), task, tx.clone(), From adf621c87a8784c3cc49f4fde7f7fa993afff9bf Mon Sep 17 00:00:00 2001 From: Carlos Alejandro Gutierrez Sandoval Date: Mon, 23 Oct 2023 19:41:23 -0600 Subject: [PATCH 4/6] move url parsing to constructor --- romeo/src/bitcoin_client.rs | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/romeo/src/bitcoin_client.rs b/romeo/src/bitcoin_client.rs index 0de0c459..71bb1af8 100644 --- a/romeo/src/bitcoin_client.rs +++ b/romeo/src/bitcoin_client.rs @@ -35,6 +35,7 @@ pub type BitcoinClient = Client; #[derivative(Clone)] pub struct Client { bitcoin_url: Url, + bitcoin_auth: Auth, #[derivative(Clone(bound = ""))] blockchain: Arc, // required for fulfillment txs @@ -71,8 +72,16 @@ impl Client { return Err(anyhow::anyhow!("Password in {bitcoin_url} is empty")); } + let username = bitcoin_url.username().to_string(); + let password = bitcoin_url.password().unwrap_or_default().to_string(); + + let mut bitcoin_url = bitcoin_url; + bitcoin_url.set_username("").unwrap(); + bitcoin_url.set_password(None).unwrap(); + Ok(Self { bitcoin_url, + bitcoin_auth: Auth::UserPass(username, password), blockchain: Arc::new(blockchain), wallet: Arc::new(Mutex::new(wallet)), }) @@ -89,16 +98,10 @@ impl Client { F: FnOnce(RPCClient) -> bitcoincore_rpc::Result + Send + 'static, T: Send + 'static, { - let mut url = self.bitcoin_url.clone(); - - let username = url.username().to_string(); - let password = url.password().unwrap_or_default().to_string(); - - url.set_username("").unwrap(); - url.set_password(None).unwrap(); - - let client = - RPCClient::new(url.as_ref(), Auth::UserPass(username, password))?; + let client = RPCClient::new( + self.bitcoin_url.as_ref(), + self.bitcoin_auth.clone(), + )?; Ok(spawn_blocking(move || f(client)).await?) } @@ -355,4 +358,14 @@ mod tests { assert_eq!(broken_client("http://@host").to_string(), err_string); assert_eq!(broken_client("http://host").to_string(), err_string,); } + + #[test] + fn stripped_url_auth_is_field() { + let client = client::<0>("http://user:pass@host").unwrap(); + assert_eq!(client.bitcoin_url, "http://host".parse().unwrap()); + assert_eq!( + client.bitcoin_auth, + Auth::UserPass("user".into(), "pass".into()) + ); + } } From 00baa2de41268a8b04bbf5ba715246cad3c8d93c Mon Sep 17 00:00:00 2001 From: Carlos Alejandro Gutierrez Sandoval Date: Tue, 24 Oct 2023 00:16:26 -0600 Subject: [PATCH 5/6] simplify match pattern --- romeo/Cargo.toml | 4 +++ romeo/src/bitcoin_client.rs | 64 +++++++++++++++++++++++++++++-------- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/romeo/Cargo.toml b/romeo/Cargo.toml index 58483c26..7c40bab6 100644 --- a/romeo/Cargo.toml +++ b/romeo/Cargo.toml @@ -23,3 +23,7 @@ tracing-subscriber.workspace = true tracing.workspace = true url.workspace = true rs_merkle.workspace = true + +[dev-dependencies] +assert_matches = "1.5.0" +mockito = "1.2.0" diff --git a/romeo/src/bitcoin_client.rs b/romeo/src/bitcoin_client.rs index 71bb1af8..347678aa 100644 --- a/romeo/src/bitcoin_client.rs +++ b/romeo/src/bitcoin_client.rs @@ -152,13 +152,12 @@ impl Client { block_height: u32, ) -> anyhow::Result<(u32, Block)> { let block_hash = loop { - let res = self + match self .execute(move |client| { client.get_block_hash(block_height as u64) }) - .await?; - - match res { + .await? + { Ok(hash) => { trace!( "Got Bitcoin block hash at height {}: {}", @@ -169,15 +168,8 @@ impl Client { } Err(bitcoincore_rpc::Error::JsonRpc( bitcoincore_rpc::jsonrpc::Error::Rpc(err), - )) => { - if err.code == -8 { - trace!("Bitcoin block not found, retrying..."); - } else { - Err(anyhow!( - "Error fetching Bitcoin block: {:?}", - err - ))?; - } + )) if err.code == -8 => { + trace!("Bitcoin block not found, retrying..."); } Err(bitcoincore_rpc::Error::JsonRpc( bitcoincore_rpc::jsonrpc::Error::Transport(_), @@ -185,7 +177,7 @@ impl Client { trace!("Bitcoin client connection error, retrying..."); } Err(err) => { - Err(anyhow!("Error fetching Bitcoin block: {:?}", err))? + Err(anyhow!("Error fetching Bitcoin block: {:?}", err))?; } }; @@ -260,6 +252,7 @@ where mod tests { use std::path::Path; + use assert_matches::assert_matches; use bdk::{ bitcoin::Network as BitcoinNetwork, blockchain::{ConfigurableBlockchain, ElectrumBlockchainConfig}, @@ -368,4 +361,47 @@ mod tests { Auth::UserPass("user".into(), "pass".into()) ); } + + #[tokio::test] + async fn get_block() { + let mut server = mockito::Server::new(); + let host = format!("http://devnet:devnet@{}", server.host_with_port()); + let mock_hash = server + .mock("POST", "/") + .with_status(200) + .with_header("content-type", "application/json") + .match_body(mockito::Matcher::PartialJsonString( + r#"{"method": "getblockhash"}"#.to_string(), + )) + .with_body( + // Regardless of input. + r#"{ "result": "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206", "error": null, "id": 0 }"#, + ) + .create(); + + let mock_block = server + .mock("POST", "/") + .with_status(200) + .with_header("content-type", "application/json") + .match_body(mockito::Matcher::PartialJsonString( + r#"{"method": "getblock"}"#.to_string(), + )) + .with_body( + // Regardless of input. + r#"{ "result":"0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff7f20020000000101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000", "id": 0}"#, + ) + .create(); + + let client = client::<0>(host.as_str()).unwrap(); + + assert_matches!(client.get_block(0).await.unwrap(), (0, block) =>{ + // given the current devenv config; block hash 0 + assert_eq!("0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206", + block.header.block_hash().to_string()); + }); + + // endpoints where served. + mock_hash.assert(); + mock_block.assert() + } } From 221fe2c5b5624198128b36bdb8be0a5da3e5c20b Mon Sep 17 00:00:00 2001 From: Carlos Alejandro Gutierrez Sandoval Date: Fri, 27 Oct 2023 09:50:20 -0600 Subject: [PATCH 6/6] reintegrate delegated logic --- romeo/src/bitcoin_client.rs | 3 --- romeo/src/system.rs | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/romeo/src/bitcoin_client.rs b/romeo/src/bitcoin_client.rs index 347678aa..601f8611 100644 --- a/romeo/src/bitcoin_client.rs +++ b/romeo/src/bitcoin_client.rs @@ -210,8 +210,6 @@ where &self, outputs: Vec<(Script, u64)>, ) -> anyhow::Result { - sleep(Duration::from_secs(3)).await; - let blockchain = self.blockchain.clone(); let wallet = self.wallet.clone(); @@ -310,7 +308,6 @@ mod tests { let client_sbtc_wallet = client .wallet - .clone() .lock() .unwrap() .get_address(bdk::wallet::AddressIndex::Peek(0)) diff --git a/romeo/src/system.rs b/romeo/src/system.rs index 8a2d182b..30dc9c3b 100644 --- a/romeo/src/system.rs +++ b/romeo/src/system.rs @@ -1,6 +1,6 @@ //! System -use std::{fs::create_dir_all, io::Cursor}; +use std::{fs::create_dir_all, io::Cursor, time::Duration}; use bdk::{ bitcoin::Txid as BitcoinTxId, @@ -440,6 +440,7 @@ async fn fulfill_asset( ) .expect("Could not create withdrawal fulfillment outputs"); + tokio::time::sleep(Duration::from_secs(3)).await; let txid = bitcoin_client .sign_and_broadcast(outputs.to_vec()) .await