Skip to content

Commit

Permalink
Partial lightning network node implementation #1045 (#1103)
Browse files Browse the repository at this point in the history
* impl FeeEstimator, Logger, BroadcasterInterface

* impl Filter for ElectrumClient

* lightning conf + ChainMonitor for electrum

* fix wasm build

* impl keys_manager and channel_manager

* NetGraphMsgHandler, PeerManager, spawn network

* update best block for chainmon and channel manager

* Mock LN events, Persist CM, Background Processing

* 1st version of enable_lightning rpc for test

* refactoring + Error handling

* remove hardcoded lightning conf paramaters

* move LN trait impls for Electrum to seperate file

* enable_lightning test

* move enable_lightning to dispacher_v2

* announce external ip address to LN

* Add node color to configs and request
  • Loading branch information
shamardy authored Oct 18, 2021
1 parent 09c54f8 commit 637e247
Show file tree
Hide file tree
Showing 23 changed files with 1,363 additions and 199 deletions.
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) {
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

0 comments on commit 637e247

Please sign in to comment.