Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial lightning network node implementation #1103

Merged
merged 17 commits into from
Oct 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 262 additions & 17 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ futures = { version = "0.3.1", package = "futures", features = ["compat", "async
gstuff = { version = "0.7", features = ["nightly"] }
hash256-std-hasher = "0.15.2"
hash-db = "0.15.2"
hex = "0.3.2"
hex = "0.4.2"
hex-literal = "0.3.1"
http = "0.2"
itertools = "0.9"
Expand Down
Empty file modified etomic_build/client/enable_USDF
100755 → 100644
Empty file.
Empty file modified etomic_build/client/enable_tBCH
100755 → 100644
Empty file.
Empty file modified etomic_build/client/validate_address
100755 → 100644
Empty file.
9 changes: 8 additions & 1 deletion mm2src/coins/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ async-std = { version = "1.5", features = ["unstable"] }
async-trait = "0.1"
base64 = "0.10.0"
bigdecimal = { version = "0.1.0", features = ["serde"] }
bitcoin = "0.27.1"
bitcoin-cash-slp = "0.3.1"
bitcoin_hashes = "0.10.0"
bitcrypto = { path = "../mm2_bitcoin/crypto" }
byteorder = "1.3"
bytes = "0.4"
Expand All @@ -34,13 +37,14 @@ futures01 = { version = "0.1", package = "futures" }
# using select macro requires the crate to be named futures, compilation failed with futures03 name
futures = { version = "0.3", package = "futures", features = ["compat", "async-await"] }
gstuff = { version = "0.7", features = ["nightly"] }
hex = "0.3.2"
hex = "0.4.2"
http = "0.2"
itertools = "0.9"
jsonrpc-core = "8.0.1"
keys = { path = "../mm2_bitcoin/keys" }
lazy_static = "1.4"
libc = "0.2"
lightning = "0.0.101"
metrics = "0.12"
mocktopus = "0.7.0"
num-traits = "0.2"
Expand Down Expand Up @@ -75,6 +79,9 @@ web-sys = { version = "0.3.4", features = ["console", "Headers", "Request", "Req

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
dirs = { version = "1" }
lightning-background-processor = "0.0.101"
lightning-persister = "0.0.101"
lightning-net-tokio = "0.0.101"
rusqlite = { version = "0.24.2", features = ["bundled"], optional = true }
rust-ini = { version = "0.13" }
rustls = { version = "0.19", features = ["dangerous_configuration"] }
Expand Down
100 changes: 100 additions & 0 deletions mm2src/coins/lightning.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#[cfg(not(target_arch = "wasm32"))]
use crate::utxo::rpc_clients::UtxoRpcClientEnum;
#[cfg(not(target_arch = "wasm32"))]
use common::ip_addr::myipaddr;
use common::mm_ctx::MmArc;
use common::mm_error::prelude::*;
use ln_errors::{EnableLightningError, EnableLightningResult};
#[cfg(not(target_arch = "wasm32"))]
use ln_utils::{network_from_string, start_lightning, LightningConf};

#[cfg(not(target_arch = "wasm32"))]
use super::{lp_coinfind_or_err, MmCoinEnum};

mod ln_errors;
mod ln_rpc;
#[cfg(not(target_arch = "wasm32"))] mod ln_utils;

#[derive(Deserialize)]
pub struct EnableLightningRequest {
pub coin: String,
pub port: Option<u16>,
pub name: String,
pub color: Option<String>,
}

#[cfg(target_arch = "wasm32")]
pub async fn enable_lightning(_ctx: MmArc, _req: EnableLightningRequest) -> EnableLightningResult<String> {
MmError::err(EnableLightningError::UnsupportedMode(
"'enable_lightning'".into(),
"native".into(),
))
}

/// Start a BTC lightning node (LTC should be added later).
#[cfg(not(target_arch = "wasm32"))]
pub async fn enable_lightning(ctx: MmArc, req: EnableLightningRequest) -> EnableLightningResult<String> {
// coin has to be enabled in electrum to start a lightning node
let coin = lp_coinfind_or_err(&ctx, &req.coin).await?;

let utxo_coin = match coin {
MmCoinEnum::UtxoCoin(utxo) => utxo,
_ => {
return MmError::err(EnableLightningError::UnsupportedCoin(
req.coin,
"Only utxo coins are supported in lightning".into(),
))
},
};

if !utxo_coin.as_ref().conf.lightning {
return MmError::err(EnableLightningError::UnsupportedCoin(
req.coin,
"'lightning' field not found in coin config".into(),
));
}

let client = match &utxo_coin.as_ref().rpc_client {
UtxoRpcClientEnum::Electrum(c) => c,
UtxoRpcClientEnum::Native(_) => {
return MmError::err(EnableLightningError::UnsupportedMode(
"Lightning network".into(),
"electrum".into(),
))
},
};

let network = match &utxo_coin.as_ref().conf.network {
Some(n) => network_from_string(n.clone())?,
None => {
return MmError::err(EnableLightningError::UnsupportedCoin(
req.coin,
"'network' field not found in coin config".into(),
))
},
};

if req.name.len() > 32 {
return MmError::err(EnableLightningError::InvalidRequest(
"Node name length can't be more than 32 characters".into(),
));
}
let node_name = format!("{}{:width$}", req.name, " ", width = 32 - req.name.len());

let mut node_color = [0u8; 3];
hex::decode_to_slice(
req.color.unwrap_or_else(|| "000000".into()),
&mut node_color as &mut [u8],
)
.map_to_mm(|_| EnableLightningError::InvalidRequest("Invalid Hex Color".into()))?;

let listen_addr = myipaddr(ctx.clone())
.await
.map_to_mm(EnableLightningError::InvalidAddress)?;
let port = req.port.unwrap_or(9735);

let conf = LightningConf::new(client.clone(), network, listen_addr, port, node_name, node_color);
start_lightning(&ctx, conf).await?;

Ok("success".into())
}
61 changes: 61 additions & 0 deletions mm2src/coins/lightning/ln_errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use crate::CoinFindError;
use common::mm_error::prelude::*;
use common::HttpStatusCode;
use derive_more::Display;
use http::StatusCode;

pub type EnableLightningResult<T> = Result<T, MmError<EnableLightningError>>;

#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)]
#[serde(tag = "error_type", content = "error_data")]
pub enum EnableLightningError {
#[display(fmt = "Invalid request: {}", _0)]
InvalidRequest(String),
#[display(fmt = "Invalid address: {}", _0)]
InvalidAddress(String),
#[display(fmt = "Invalid path: {}", _0)]
InvalidPath(String),
#[display(fmt = "Lightning node already running")]
AlreadyRunning,
#[display(fmt = "{} is only supported in {} mode", _0, _1)]
UnsupportedMode(String, String),
#[display(fmt = "Lightning network is not supported for {}: {}", _0, _1)]
UnsupportedCoin(String, String),
#[display(fmt = "No such coin {}", _0)]
NoSuchCoin(String),
#[display(fmt = "System time error {}", _0)]
SystemTimeError(String),
#[display(fmt = "I/O error {}", _0)]
IOError(String),
#[display(fmt = "Hash error {}", _0)]
HashError(String),
#[display(fmt = "RPC error {}", _0)]
RpcError(String),
}

impl HttpStatusCode for EnableLightningError {
fn status_code(&self) -> StatusCode {
match self {
EnableLightningError::InvalidRequest(_)
| EnableLightningError::RpcError(_)
| EnableLightningError::UnsupportedCoin(_, _) => StatusCode::BAD_REQUEST,
EnableLightningError::AlreadyRunning | EnableLightningError::UnsupportedMode(_, _) => {
StatusCode::METHOD_NOT_ALLOWED
},
EnableLightningError::InvalidAddress(_)
| EnableLightningError::InvalidPath(_)
| EnableLightningError::SystemTimeError(_)
| EnableLightningError::IOError(_)
| EnableLightningError::HashError(_) => StatusCode::INTERNAL_SERVER_ERROR,
EnableLightningError::NoSuchCoin(_) => StatusCode::PRECONDITION_REQUIRED,
}
}
}

impl From<CoinFindError> for EnableLightningError {
fn from(e: CoinFindError) -> Self {
match e {
CoinFindError::NoSuchCoin { coin } => EnableLightningError::NoSuchCoin(coin),
}
}
}
73 changes: 73 additions & 0 deletions mm2src/coins/lightning/ln_rpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use crate::utxo::rpc_clients::{electrum_script_hash, ElectrumClient, UtxoRpcClientOps, UtxoRpcError};
use bitcoin::blockdata::script::Script;
use bitcoin::blockdata::transaction::Transaction;
use bitcoin::consensus::encode;
use bitcoin::hash_types::Txid;
use common::block_on;
use common::mm_error::prelude::MapToMmFutureExt;
use futures::compat::Future01CompatExt;
use lightning::chain::{chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator},
Filter, WatchedOutput};
use rpc::v1::types::Bytes as BytesJson;

impl FeeEstimator for ElectrumClient {
// Gets estimated satoshis of fee required per 1000 Weight-Units.
// TODO: use fn estimate_fee instead of fixed number when starting work on opening channels
fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 {
match confirmation_target {
// fetch background feerate
ConfirmationTarget::Background => 253,
// fetch normal feerate (~6 blocks)
ConfirmationTarget::Normal => 2000,
// fetch high priority feerate
ConfirmationTarget::HighPriority => 5000,
}
}
}

impl BroadcasterInterface for ElectrumClient {
fn broadcast_transaction(&self, tx: &Transaction) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit sad that they assume a synchronous interface for this 🙁

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is intended as they leave you the choice to run these functions in asynchronous code through other interfaces. For the broadcast_transaction function for instance, when a new best block is updated for the channel manager https://github.com/KomodoPlatform/atomicDEX-API/blob/ad87f483cc5199851a781c5b99a1bda341f75ecb/mm2src/coins/lightning.rs#L384 if it's required to claim on-chain channel funds for some reason as this case https://github.com/rust-bitcoin/rust-lightning/blob/fe8c10db95124e3238b7469bdabb00afc7c5bdd6/lightning/src/chain/onchaintx.rs#L486, broadcast_transaction is run in the best_block_update thread away from the p2p networking and node announcement. The only problem I see is if for the same new best block more than one on-chain transaction needs to be broadcasted then they will be broadcasted synchronously inside the best_block_update thread but this is very rare but would have been solved if broadcast_transaction was async.

let tx_bytes = BytesJson::from(encode::serialize_hex(tx).as_bytes());
let _ = Box::new(
self.blockchain_transaction_broadcast(tx_bytes)
.map_to_mm_fut(UtxoRpcError::from),
);
}
}

impl Filter for ElectrumClient {
// Watches for this transaction on-chain
fn register_tx(&self, _txid: &Txid, _script_pubkey: &Script) { unimplemented!() }

// Watches for any transactions that spend this output on-chain
fn register_output(&self, output: WatchedOutput) -> Option<(usize, Transaction)> {
let selfi = self.clone();
let script_hash = hex::encode(electrum_script_hash(output.script_pubkey.as_ref()));
let history = block_on(selfi.scripthash_get_history(&script_hash).compat()).unwrap_or_default();

if history.len() < 2 {
return None;
}

for item in history.iter() {
let transaction = match block_on(selfi.get_transaction_bytes(item.tx_hash.clone()).compat()) {
Ok(tx) => tx,
Err(_) => continue,
};

let maybe_spend_tx: Transaction = match encode::deserialize(transaction.as_slice()) {
Ok(tx) => tx,
Err(_) => continue,
};

for (index, input) in maybe_spend_tx.input.iter().enumerate() {
if input.previous_output.txid == output.outpoint.txid
&& input.previous_output.vout == output.outpoint.index as u32
{
return Some((index, maybe_spend_tx));
}
}
}
None
}
}
Loading