diff --git a/Cargo.toml b/Cargo.toml index 13b408a..df5004e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "senseid" path = "src/main.rs" [dependencies] -bitcoin = "0.28.1" +bitcoin = { version = "0.28.1" } bitcoincore-rpc = "0.15" futures = "0.3" chrono = "0.4" diff --git a/senseicore/Cargo.toml b/senseicore/Cargo.toml index e6395ea..5651fef 100644 --- a/senseicore/Cargo.toml +++ b/senseicore/Cargo.toml @@ -17,7 +17,7 @@ lightning-persister = { version = "0.0.110" } lightning-background-processor = { version = "0.0.110" } lightning-rapid-gossip-sync = { version = "0.0.110" } base64 = "0.13.0" -bitcoin = "0.28" +bitcoin = { version = "0.28.1" } bitcoin-bech32 = "0.12" bech32 = "0.8" futures = "0.3" diff --git a/senseicore/src/chain/mod.rs b/senseicore/src/chain/mod.rs index a7d39f1..39ef233 100644 --- a/senseicore/src/chain/mod.rs +++ b/senseicore/src/chain/mod.rs @@ -1,6 +1,90 @@ +use bitcoin::Network; +use lightning::chain::chaininterface::{FeeEstimator, ConfirmationTarget, BroadcasterInterface}; +use lightning_block_sync::BlockSource; +use tokio::runtime::Handle; + +use self::{bitcoind_client::BitcoindClient, remote::{fee_estimator::RemoteFeeEstimator, broadcaster::RemoteBroadcaster, block_source::RemoteBlockSource}}; +use std::sync::Arc; + pub mod bitcoind_client; pub mod broadcaster; pub mod database; pub mod fee_estimator; pub mod listener; pub mod manager; +pub mod remote; + + +pub enum AnyBlockSource { + Local(Arc), + Remote(remote::block_source::RemoteBlockSource) +} + +impl AnyBlockSource { + pub fn new_remote(network: Network, host: String, token: String) -> Self { + AnyBlockSource::Remote(RemoteBlockSource::new(network, host, token)) + } +} + +impl BlockSource for AnyBlockSource { + fn get_header<'a>(&'a self, header_hash: &'a bitcoin::BlockHash, height_hint: Option) -> lightning_block_sync::AsyncBlockSourceResult<'a, lightning_block_sync::BlockHeaderData> { + match self { + AnyBlockSource::Local(bitcoind_client) => bitcoind_client.get_header(header_hash, height_hint), + AnyBlockSource::Remote(remote) => remote.get_header(header_hash, height_hint) + } + } + + fn get_block<'a>(&'a self, header_hash: &'a bitcoin::BlockHash) -> lightning_block_sync::AsyncBlockSourceResult<'a, bitcoin::Block> { + match self { + AnyBlockSource::Local(bitcoind_client) => bitcoind_client.get_block(header_hash), + AnyBlockSource::Remote(remote) => remote.get_block(header_hash) + } + } + + fn get_best_block<'a>(&'a self) -> lightning_block_sync::AsyncBlockSourceResult<(bitcoin::BlockHash, Option)> { + match self { + AnyBlockSource::Local(bitcoind_client) => bitcoind_client.get_best_block(), + AnyBlockSource::Remote(remote) => remote.get_best_block() + } + } +} + +pub enum AnyFeeEstimator { + Local(Arc), + Remote(RemoteFeeEstimator) +} + +impl AnyFeeEstimator { + pub fn new_remote(host: String, token: String, handle: Handle) -> Self { + AnyFeeEstimator::Remote(RemoteFeeEstimator::new( host, token, handle)) + } +} + +impl FeeEstimator for AnyFeeEstimator { + fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 { + match self { + AnyFeeEstimator::Local(bitcoind_client) => bitcoind_client.get_est_sat_per_1000_weight(confirmation_target), + AnyFeeEstimator::Remote(remote) => remote.get_est_sat_per_1000_weight(confirmation_target) + } + } +} + +pub enum AnyBroadcaster { + Local(Arc), + Remote(RemoteBroadcaster) +} + +impl AnyBroadcaster { + pub fn new_remote(host: String, token: String, handle: Handle) -> Self { + AnyBroadcaster::Remote(RemoteBroadcaster::new( host, token, handle)) + } +} + +impl BroadcasterInterface for AnyBroadcaster { + fn broadcast_transaction(&self, tx: &bitcoin::Transaction) { + match self { + AnyBroadcaster::Local(bitcoind_client) => bitcoind_client.broadcast_transaction(tx), + AnyBroadcaster::Remote(remote) => remote.broadcast_transaction(tx) + } + } +} \ No newline at end of file diff --git a/senseicore/src/chain/remote/block_source.rs b/senseicore/src/chain/remote/block_source.rs new file mode 100644 index 0000000..5467e47 --- /dev/null +++ b/senseicore/src/chain/remote/block_source.rs @@ -0,0 +1,149 @@ +use lightning::chain::BestBlock; +use lightning_block_sync::{BlockSource, BlockSourceError, BlockHeaderData}; +use bitcoin::{hashes::hex::{ToHex, FromHex}, util::uint::Uint256, consensus::deserialize, BlockHeader, Block, BlockHash, Network}; +use crate::{hex_utils, p2p::router::RemoteSenseiInfo}; + +pub struct RemoteBlockSource { + network: Network, + remote_sensei: RemoteSenseiInfo +} + +impl RemoteBlockSource { + pub fn new(network: Network, host: String, token: String) -> Self { + Self { + network, + remote_sensei: RemoteSenseiInfo { host, token } + } + } + fn get_header_path(&self, header_hash: String) -> String { + format!("{}/v1/ldk/chain/header/{}", self.remote_sensei.host, header_hash) + } + fn get_block_path(&self, header_hash: String) -> String { + format!("{}/v1/ldk/chain/block/{}", self.remote_sensei.host, header_hash) + } + fn get_best_block_hash_path(&self) -> String { + format!("{}/v1/ldk/chain/best-block-hash", self.remote_sensei.host) + } + fn get_best_block_height_path(&self) -> String { + format!("{}/v1/ldk/chain/best-block-height", self.remote_sensei.host) + } + + pub async fn get_best_block_hash(&self) -> Option { + let client = reqwest::Client::new(); + match client.get(self.get_best_block_hash_path()) + .header("token", self.remote_sensei.token.clone()) + .send() + .await { + Ok(response) => { + match response.bytes().await { + Ok(serialized_hash) => { + Some(deserialize(&serialized_hash).unwrap()) + }, + Err(_) => None + } + }, + Err(_) => None + } + } + + pub async fn get_best_block_height(&self) -> Option { + let client = reqwest::Client::new(); + match client.get(self.get_best_block_height_path()) + .header("token", self.remote_sensei.token.clone()) + .send() + .await { + Ok(response) => { + match response.text().await { + Ok(height_as_string) => { + Some(height_as_string.parse().unwrap()) + }, + Err(_) => None + } + }, + Err(_) => None + } + } + + pub async fn get_best_block_async(&self) -> BestBlock { + let best_hash = self.get_best_block_hash().await; + let best_height = self.get_best_block_height().await; + if best_hash.is_none() || best_height.is_none() { + BestBlock::from_genesis(self.network) + } else { + BestBlock::new(best_hash.unwrap(), best_height.unwrap()) + } + } +} + +impl BlockSource for RemoteBlockSource { + fn get_header<'a>(&'a self, header_hash: &'a bitcoin::BlockHash, _height_hint: Option) -> lightning_block_sync::AsyncBlockSourceResult<'a, lightning_block_sync::BlockHeaderData> { + Box::pin(async move { + let client = reqwest::Client::new(); + let res = client.get(self.get_header_path(header_hash.to_hex())) + .header("token", self.remote_sensei.token.clone()) + .send() + .await; + + match res { + Ok(response) => { + match response.text().await { + Ok(header_data_string) => { + let header_parts: Vec<&str> = header_data_string.split(',').collect(); + let header: BlockHeader = + deserialize(&Vec::::from_hex(header_parts[0]).unwrap()).unwrap(); + let height: u32 = header_parts[1].to_string().parse().unwrap(); + let chainwork: Uint256 = + deserialize(&hex_utils::to_vec(header_parts[2]).unwrap()).unwrap(); + + Ok(BlockHeaderData { + header, + height, + chainwork, + }) + }, + Err(e) => { + Err(BlockSourceError::transient(e)) + } + } + }, + Err(e) => { + Err(BlockSourceError::transient(e)) + } + } + }) + } + + fn get_block<'a>(&'a self, header_hash: &'a bitcoin::BlockHash) -> lightning_block_sync::AsyncBlockSourceResult<'a, bitcoin::Block> { + Box::pin(async move { + let client = reqwest::Client::new(); + let res = client.get(self.get_block_path(header_hash.to_hex())) + .header("token", self.remote_sensei.token.clone()) + .send() + .await; + + match res { + Ok(response) => { + match response.bytes().await { + Ok(serialized_block_data) => { + let block: Block = deserialize(&serialized_block_data).unwrap(); + Ok(block) + }, + Err(e) => { + Err(BlockSourceError::transient(e)) + } + } + }, + Err(e) => { + Err(BlockSourceError::transient(e)) + } + } + }) + } + + fn get_best_block<'a>(&'a self) -> lightning_block_sync::AsyncBlockSourceResult<(bitcoin::BlockHash, Option)> { + Box::pin(async move { + let best_block = self.get_best_block_async().await; + Ok((best_block.block_hash(), Some(best_block.height()))) + }) + } +} \ No newline at end of file diff --git a/senseicore/src/chain/remote/broadcaster.rs b/senseicore/src/chain/remote/broadcaster.rs new file mode 100644 index 0000000..302577a --- /dev/null +++ b/senseicore/src/chain/remote/broadcaster.rs @@ -0,0 +1,44 @@ +use lightning::chain::chaininterface::BroadcasterInterface; +use tokio::runtime::Handle; +use lightning::util::ser::Writeable; +use crate::{p2p::router::RemoteSenseiInfo, hex_utils}; + +pub struct RemoteBroadcaster { + remote_sensei: RemoteSenseiInfo, + tokio_handle: Handle, +} + +impl RemoteBroadcaster { + + pub fn new(host: String, token: String, tokio_handle: Handle) -> Self { + Self { + remote_sensei: RemoteSenseiInfo { host, token }, + tokio_handle, + } + } + + fn broadcast_path(&self) -> String { + format!("{}/v1/ldk/chain/broadcast", self.remote_sensei.host) + } + + pub async fn broadcast_transaction_async(&self, tx: &bitcoin::Transaction) { + let client = reqwest::Client::new(); + let _res = client.post(self.broadcast_path()) + .header("token", self.remote_sensei.token.clone()) + .json(&serde_json::json!({ + "tx": hex_utils::hex_str(&tx.encode()) + })) + .send() + .await; + } +} + +impl BroadcasterInterface for RemoteBroadcaster { + fn broadcast_transaction(&self, tx: &bitcoin::Transaction) { + tokio::task::block_in_place(move || { + self.tokio_handle.clone().block_on(async move { + self.broadcast_transaction_async(tx).await + }) + }) + } +} diff --git a/senseicore/src/chain/remote/fee_estimator.rs b/senseicore/src/chain/remote/fee_estimator.rs new file mode 100644 index 0000000..75df2f2 --- /dev/null +++ b/senseicore/src/chain/remote/fee_estimator.rs @@ -0,0 +1,99 @@ +use lightning::chain::chaininterface::{ConfirmationTarget, FeeEstimator}; +use tokio::runtime::Handle; + +use crate::p2p::router::RemoteSenseiInfo; + +pub struct RemoteFeeEstimator { + remote_sensei: RemoteSenseiInfo, + tokio_handle: Handle, +} + +impl RemoteFeeEstimator { + + pub fn new(host: String, token: String, tokio_handle: Handle) -> Self { + Self { + remote_sensei: RemoteSenseiInfo { host, token }, + tokio_handle, + } + } + + fn fee_rate_normal_path(&self) -> String { + format!("{}/v1/ldk/chain/fee-rate-normal", self.remote_sensei.host) + } + + fn fee_rate_background_path(&self) -> String { + format!("{}/v1/ldk/chain/fee-rate-background", self.remote_sensei.host) + } + + fn fee_rate_high_priority_path(&self) -> String { + format!("{}/v1/ldk/chain/fee-rate-high-priority", self.remote_sensei.host) + } + + pub async fn get_fee_rate_normal(&self) -> u32 { + let client = reqwest::Client::new(); + match client.get(self.fee_rate_normal_path()) + .header("token", self.remote_sensei.token.clone()) + .send() + .await { + Ok(response) => { + match response.text().await { + Ok(fee_rate_string) => { + fee_rate_string.parse().unwrap_or(2000) + }, + Err(_) => 2000 + } + }, + Err(_) => 2000 + } + } + + pub async fn get_fee_rate_background(&self) -> u32 { + let client = reqwest::Client::new(); + match client.get(self.fee_rate_background_path()) + .header("token", self.remote_sensei.token.clone()) + .send() + .await { + Ok(response) => { + match response.text().await { + Ok(fee_rate_string) => { + fee_rate_string.parse().unwrap_or(253) + }, + Err(_) => 253 + } + }, + Err(_) => 253 + } + } + + pub async fn get_fee_rate_high_priority(&self) -> u32 { + let client = reqwest::Client::new(); + match client.get(self.fee_rate_high_priority_path()) + .header("token", self.remote_sensei.token.clone()) + .send() + .await { + Ok(response) => { + match response.text().await { + Ok(fee_rate_string) => { + fee_rate_string.parse().unwrap_or(5000) + }, + Err(_) => 5000 + } + }, + Err(_) => 5000 + } + } +} + +impl FeeEstimator for RemoteFeeEstimator { + fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 { + tokio::task::block_in_place(move || { + self.tokio_handle.clone().block_on(async move { + match confirmation_target { + ConfirmationTarget::Background => self.get_fee_rate_background().await, + ConfirmationTarget::Normal => self.get_fee_rate_normal().await, + ConfirmationTarget::HighPriority => self.get_fee_rate_high_priority().await, + } + }) + }) + } +} diff --git a/senseicore/src/chain/remote/mod.rs b/senseicore/src/chain/remote/mod.rs new file mode 100644 index 0000000..260a6c4 --- /dev/null +++ b/senseicore/src/chain/remote/mod.rs @@ -0,0 +1,3 @@ +pub mod block_source; +pub mod fee_estimator; +pub mod broadcaster; \ No newline at end of file diff --git a/senseicore/src/config.rs b/senseicore/src/config.rs index f664132..84e274f 100644 --- a/senseicore/src/config.rs +++ b/senseicore/src/config.rs @@ -30,6 +30,8 @@ pub struct SenseiConfig { pub database_url: String, pub remote_p2p_host: Option, pub remote_p2p_token: Option, + pub remote_chain_host: Option, + pub remote_chain_token: Option, pub gossip_peers: String, pub instance_name: String, } @@ -53,6 +55,8 @@ impl Default for SenseiConfig { database_url: String::from("sensei.db"), remote_p2p_host: None, remote_p2p_token: None, + remote_chain_host: None, + remote_chain_token: None, gossip_peers: String::from(""), instance_name: String::from("sensei"), } diff --git a/src/main.rs b/src/main.rs index abbbe94..7a4a688 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ mod http; mod hybrid; use senseicore::{ - chain::{bitcoind_client::BitcoindClient, manager::SenseiChainManager}, + chain::{bitcoind_client::BitcoindClient, manager::SenseiChainManager, AnyBlockSource, AnyFeeEstimator, AnyBroadcaster}, config::SenseiConfig, database::SenseiDatabase, events::SenseiEvent, @@ -98,6 +98,10 @@ struct SenseiArgs { remote_p2p_token: Option, #[clap(long, env = "INSTANCE_NAME")] instance_name: Option, + #[clap(long, env = "REMOTE_CHAIN_HOST")] + remote_chain_host: Option, + #[clap(long, env = "REMOTE_CHAIN_TOKEN")] + remote_chain_token: Option, } pub type AdminRequestResponse = (AdminRequest, Sender); @@ -175,6 +179,12 @@ fn main() { if let Some(instance_name) = args.instance_name { config.instance_name = instance_name; } + if let Some(remote_chain_host) = args.remote_chain_host { + config.remote_chain_host = Some(remote_chain_host); + } + if let Some(remote_chain_token) = args.remote_chain_token { + config.remote_chain_token = Some(remote_chain_token); + } if !config.database_url.starts_with("postgres:") && !config.database_url.starts_with("mysql:") { let sqlite_path = format!("{}/{}/{}", sensei_dir, config.network, config.database_url); @@ -217,24 +227,46 @@ fn main() { let addr = SocketAddr::from(([0, 0, 0, 0], config.api_port)); - let bitcoind_client = Arc::new( - BitcoindClient::new( - config.bitcoind_rpc_host.clone(), - config.bitcoind_rpc_port, - config.bitcoind_rpc_username.clone(), - config.bitcoind_rpc_password.clone(), - tokio::runtime::Handle::current(), - ) - .await - .expect("invalid bitcoind rpc config"), - ); + let (block_source, fee_estimator, broadcaster) = match ( + config.remote_chain_host.as_ref(), + config.remote_chain_token.as_ref(), + ) { + (Some(host), Some(token)) => { + ( + AnyBlockSource::new_remote(config.network, host.clone(), token.clone()), + AnyFeeEstimator::new_remote(host.clone(), token.clone(), tokio::runtime::Handle::current()), + AnyBroadcaster::new_remote(host.clone(), token.clone(), tokio::runtime::Handle::current()) + ) + }, + _ => { + let bitcoind_client = Arc::new( + BitcoindClient::new( + config.bitcoind_rpc_host.clone(), + config.bitcoind_rpc_port, + config.bitcoind_rpc_username.clone(), + config.bitcoind_rpc_password.clone(), + tokio::runtime::Handle::current(), + ) + .await + .expect("invalid bitcoind rpc config"), + ); + + ( + AnyBlockSource::Local(bitcoind_client.clone()), + AnyFeeEstimator::Local(bitcoind_client.clone()), + AnyBroadcaster::Local(bitcoind_client.clone()) + ) + }, + }; + + let chain_manager = Arc::new( SenseiChainManager::new( config.clone(), - bitcoind_client.clone(), - bitcoind_client.clone(), - bitcoind_client, + Arc::new(block_source), + Arc::new(fee_estimator), + Arc::new(broadcaster), ) .await .unwrap(),