From 37dd01e8cf296a67f58149c777d95e3ed2fc3232 Mon Sep 17 00:00:00 2001 From: harshit933 Date: Tue, 19 Mar 2024 21:48:29 +0530 Subject: [PATCH 1/4] feat(rpc) : implement the `close_channel` command Adds the ability to close channels from the lampo node. Changes done : Adds close_channel function inside lampod/src/ln/channel_manager.rs Adds request/response inside model/close_channel.rs Adds `close` command inside main.rs Some minor changes : Adds `channel_id` inside channels response. --- lampo-common/src/event/ln.rs | 6 +++ lampo-common/src/model.rs | 3 ++ lampo-common/src/model/close_channel.rs | 53 +++++++++++++++++++++++++ lampo-common/src/model/open_channel.rs | 4 +- lampo-testing/src/lib.rs | 2 + lampod-cli/src/main.rs | 2 + lampod/src/actions/handler.rs | 15 ++++++- lampod/src/jsonrpc/channels.rs | 19 +++++++++ lampod/src/ln/channel_manager.rs | 36 +++++++++++++++-- lampod/src/ln/events.rs | 5 ++- 10 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 lampo-common/src/model/close_channel.rs diff --git a/lampo-common/src/event/ln.rs b/lampo-common/src/event/ln.rs index 483b4492..8b3265b5 100644 --- a/lampo-common/src/event/ln.rs +++ b/lampo-common/src/event/ln.rs @@ -38,4 +38,10 @@ pub enum LightningEvent { state: ChannelState, message: String, }, + CloseChannelEvent { + channel_id: String, + message: String, + counterparty_node_id: String, + funding_utxo: String, + }, } diff --git a/lampo-common/src/model.rs b/lampo-common/src/model.rs index 39e75630..2c2dc603 100644 --- a/lampo-common/src/model.rs +++ b/lampo-common/src/model.rs @@ -1,3 +1,4 @@ +mod close_channel; mod connect; mod getinfo; mod invoice; @@ -10,6 +11,7 @@ pub use connect::Connect; pub use getinfo::GetInfo; pub mod request { + pub use crate::model::close_channel::request::*; pub use crate::model::connect::Connect; pub use crate::model::getinfo::GetInfo; pub use crate::model::invoice::request::*; @@ -21,6 +23,7 @@ pub mod request { } pub mod response { + pub use crate::model::close_channel::response::*; pub use crate::model::connect::Connect; pub use crate::model::getinfo::GetInfo; pub use crate::model::invoice::response::*; diff --git a/lampo-common/src/model/close_channel.rs b/lampo-common/src/model/close_channel.rs new file mode 100644 index 00000000..5f0daa28 --- /dev/null +++ b/lampo-common/src/model/close_channel.rs @@ -0,0 +1,53 @@ +pub mod request { + use std::num::ParseIntError; + use std::str::FromStr; + + use bitcoin::secp256k1::PublicKey; + use serde::{Deserialize, Serialize}; + + use crate::error; + use crate::types::*; + + #[derive(Clone, Serialize, Deserialize)] + pub struct CloseChannel { + pub counterpart_node_id: String, + // Hex of the channel + pub channel_id: String, + } + + impl CloseChannel { + pub fn counterpart_node_id(&self) -> error::Result { + let node_id = PublicKey::from_str(&self.counterpart_node_id)?; + Ok(node_id) + } + + // Returns ChannelId in byte format from hex of channelid + pub fn channel_id(&self) -> error::Result { + let result = self.decode_hex(&self.channel_id)?; + let mut result_array: [u8; 32] = [0; 32]; + for i in 0..32 { + result_array[i] = result[i] + } + Ok(ChannelId::from_bytes(result_array)) + } + + fn decode_hex(&self, s: &str) -> Result, ParseIntError> { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16)) + .collect() + } + } +} + +pub mod response { + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, Debug)] + pub struct CloseChannel { + pub channel_id: String, + pub message: String, + pub counterparty_node_id: String, + pub funding_utxo: String, + } +} diff --git a/lampo-common/src/model/open_channel.rs b/lampo-common/src/model/open_channel.rs index 7ef19f63..503c6b1c 100644 --- a/lampo-common/src/model/open_channel.rs +++ b/lampo-common/src/model/open_channel.rs @@ -34,7 +34,7 @@ pub mod response { use crate::error; use crate::types::NodeId; - #[derive(Serialize, Deserialize)] + #[derive(Serialize, Deserialize, Clone)] pub struct Channels { pub channels: Vec, } @@ -58,6 +58,8 @@ pub mod response { #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Channel { + // Channel_id needs to be string as it currently does not derive Serialize + pub channel_id: String, pub short_channel_id: Option, pub peer_id: String, pub peer_alias: Option, diff --git a/lampo-testing/src/lib.rs b/lampo-testing/src/lib.rs index 167cb2da..a3632249 100644 --- a/lampo-testing/src/lib.rs +++ b/lampo-testing/src/lib.rs @@ -15,6 +15,7 @@ use clightning_testing::prelude::*; use lampo_common::json; use lampo_common::model::response; use lampo_common::model::response::NewAddress; +use lampod::jsonrpc::channels::json_close_channel; use lampod::jsonrpc::offchain::json_keysend; use tempfile::TempDir; @@ -117,6 +118,7 @@ impl LampoTesting { server.add_rpc("pay", json_pay).unwrap(); server.add_rpc("keysend", json_keysend).unwrap(); + server.add_rpc("close", json_close_channel).unwrap(); let handler = server.handler(); let rpc_handler = Arc::new(CommandHandler::new(&lampo_conf)?); rpc_handler.set_handler(handler); diff --git a/lampod-cli/src/main.rs b/lampod-cli/src/main.rs index d88c44c9..074227b8 100644 --- a/lampod-cli/src/main.rs +++ b/lampod-cli/src/main.rs @@ -8,6 +8,7 @@ use std::str::FromStr; use std::sync::Arc; use std::thread::JoinHandle; +use lampod::jsonrpc::channels::json_close_channel; use lampod::jsonrpc::channels::json_list_channels; use lampo_bitcoind::BitcoinCore; @@ -171,6 +172,7 @@ fn run_jsonrpc( server.add_rpc("pay", json_pay).unwrap(); server.add_rpc("keysend", json_keysend).unwrap(); server.add_rpc("fees", json_estimate_fees).unwrap(); + server.add_rpc("close", json_close_channel).unwrap(); let handler = server.handler(); Ok((server.spawn(), handler)) } diff --git a/lampod/src/actions/handler.rs b/lampod/src/actions/handler.rs index 4f8da922..19808bfc 100644 --- a/lampod/src/actions/handler.rs +++ b/lampod/src/actions/handler.rs @@ -150,13 +150,26 @@ impl Handler for LampoHandler { channel_type, })); Ok(()) - } + }, ldk::events::Event::ChannelClosed { channel_id, user_channel_id, reason, + counterparty_node_id, + channel_funding_txo, .. } => { + let node_id = if let Some(id) = counterparty_node_id { + id.to_string() + } else { + "Node_id not found".to_string() + }; + let txo = if let Some(txo) = channel_funding_txo { + txo.to_string() + } else { + "Outpoint not found".to_string() + }; + self.emit(Event::Lightning(LightningEvent::CloseChannelEvent { channel_id: channel_id.to_string(), message: reason.to_string(), counterparty_node_id : node_id, funding_utxo : txo})); log::info!("channel `{user_channel_id}` closed with reason: `{reason}`"); Ok(()) } diff --git a/lampod/src/jsonrpc/channels.rs b/lampod/src/jsonrpc/channels.rs index 3f47d885..f7adaac3 100644 --- a/lampod/src/jsonrpc/channels.rs +++ b/lampod/src/jsonrpc/channels.rs @@ -1,5 +1,9 @@ use lampo_common::json; +use lampo_common::model::request; use lampo_jsonrpc::errors::Error; +use lampo_jsonrpc::errors::RpcError; + +use crate::ln::events::ChannelEvents; use crate::LampoDeamon; @@ -8,3 +12,18 @@ pub fn json_list_channels(ctx: &LampoDeamon, request: &json::Value) -> Result Result { + log::info!("call for `closechannel` with request {:?}", request); + let request: request::CloseChannel = json::from_value(request.clone())?; + let res = ctx.channel_manager().close_channel(request); + let resp = match res { + Ok(resp) => Ok(resp), + Err(err) => Err(Error::Rpc(RpcError { + code: -1, + message: format!("{err}"), + data: None, + })), + }; + Ok(json::to_value(resp?)?) +} diff --git a/lampod/src/ln/channel_manager.rs b/lampod/src/ln/channel_manager.rs index cebfeddf..d63c3b25 100644 --- a/lampod/src/ln/channel_manager.rs +++ b/lampod/src/ln/channel_manager.rs @@ -10,6 +10,7 @@ use lampo_common::bitcoin::absolute::Height; use lampo_common::bitcoin::{BlockHash, Transaction}; use lampo_common::conf::{LampoConf, UserConfig}; use lampo_common::error; +use lampo_common::event::ln::LightningEvent; use lampo_common::event::onchain::OnChainEvent; use lampo_common::event::Event; use lampo_common::handler::Handler; @@ -31,7 +32,7 @@ use lampo_common::ldk::util::config::{ChannelHandshakeConfig, ChannelHandshakeLi use lampo_common::ldk::util::persist::read_channel_monitors; use lampo_common::ldk::util::ser::ReadableArgs; use lampo_common::model::request; -use lampo_common::model::response::{self, Channel, Channels}; +use lampo_common::model::response::{self, Channel, Channels, CloseChannel}; use crate::actions::handler::LampoHandler; use crate::chain::{LampoChainManager, WalletManager}; @@ -191,6 +192,7 @@ impl LampoChannelManager { .list_channels() .into_iter() .map(|channel| Channel { + channel_id: channel.channel_id.to_string(), short_channel_id: channel.short_channel_id, peer_id: channel.counterparty.node_id.to_string(), peer_alias: None, @@ -448,10 +450,36 @@ impl ChannelEvents for LampoChannelManager { }) } - fn close_channel(&self) -> error::Result<()> { - unimplemented!() - } + fn close_channel( + &self, + channel: request::CloseChannel, + ) -> error::Result { + let channel_id = channel.channel_id()?; + let node_id = channel.counterpart_node_id()?; + self.manager() + .close_channel(&channel_id, &node_id) + .map_err(|err| error::anyhow!("{:?}", err))?; + let events = self.handler().events(); + let (message, channel_id, node_id, funding_utxo) = loop { + let event = events.recv_timeout(std::time::Duration::from_secs(30))?; + if let Event::Lightning(LightningEvent::CloseChannelEvent { + message, + channel_id, + counterparty_node_id, + funding_utxo, + }) = event + { + break (message, channel_id, counterparty_node_id, funding_utxo); + } + }; + Ok(CloseChannel { + channel_id, + message, + counterparty_node_id: node_id, + funding_utxo, + }) + } fn change_state_channel(&self, _: ChangeStateChannelEvent) -> error::Result<()> { unimplemented!() } diff --git a/lampod/src/ln/events.rs b/lampod/src/ln/events.rs index d5d616c8..45bd9103 100644 --- a/lampod/src/ln/events.rs +++ b/lampod/src/ln/events.rs @@ -31,7 +31,10 @@ pub trait ChannelEvents { ) -> error::Result; /// Close a channel - fn close_channel(&self) -> error::Result<()>; + fn close_channel( + &self, + channel: request::CloseChannel, + ) -> error::Result; fn change_state_channel(&self, event: ChangeStateChannelEvent) -> error::Result<()>; } From c012ed758b071eda0ce026980fcd02b3561de03a Mon Sep 17 00:00:00 2001 From: harshit933 Date: Tue, 19 Mar 2024 21:52:49 +0530 Subject: [PATCH 2/4] test : adds integration test for `close_channel` --- lampo-common/src/model/close_channel.rs | 14 +++- lampod/src/jsonrpc/channels.rs | 29 +++++++- lampod/src/ln/channel_manager.rs | 22 +----- lampod/src/ln/events.rs | 2 +- tests/tests/src/lampo_cln_tests.rs | 98 +++++++++++++++++++++++++ 5 files changed, 139 insertions(+), 26 deletions(-) diff --git a/lampo-common/src/model/close_channel.rs b/lampo-common/src/model/close_channel.rs index 5f0daa28..4c9a7007 100644 --- a/lampo-common/src/model/close_channel.rs +++ b/lampo-common/src/model/close_channel.rs @@ -10,20 +10,26 @@ pub mod request { #[derive(Clone, Serialize, Deserialize)] pub struct CloseChannel { - pub counterpart_node_id: String, + pub node_id: String, // Hex of the channel - pub channel_id: String, + pub channel_id: Option, } impl CloseChannel { pub fn counterpart_node_id(&self) -> error::Result { - let node_id = PublicKey::from_str(&self.counterpart_node_id)?; + let node_id = PublicKey::from_str(&self.node_id)?; Ok(node_id) } // Returns ChannelId in byte format from hex of channelid pub fn channel_id(&self) -> error::Result { - let result = self.decode_hex(&self.channel_id)?; + let id = if let Some(id) = &self.channel_id { + id + } else { + error::bail!("No node_id provided"); + }; + let result = self.decode_hex(&id)?; + // FIXME: We can do better here let mut result_array: [u8; 32] = [0; 32]; for i in 0..32 { result_array[i] = result[i] diff --git a/lampod/src/jsonrpc/channels.rs b/lampod/src/jsonrpc/channels.rs index f7adaac3..d456ecea 100644 --- a/lampod/src/jsonrpc/channels.rs +++ b/lampod/src/jsonrpc/channels.rs @@ -1,7 +1,10 @@ +use lampo_common::event::ln::LightningEvent; +use lampo_common::event::Event; use lampo_common::json; use lampo_common::model::request; use lampo_jsonrpc::errors::Error; use lampo_jsonrpc::errors::RpcError; +use lampo_common::handler::Handler; use crate::ln::events::ChannelEvents; @@ -16,9 +19,33 @@ pub fn json_list_channels(ctx: &LampoDeamon, request: &json::Value) -> Result Result { log::info!("call for `closechannel` with request {:?}", request); let request: request::CloseChannel = json::from_value(request.clone())?; + let events = ctx.handler().events(); let res = ctx.channel_manager().close_channel(request); + let (message, channel_id, node_id, funding_utxo) = loop { + let event = events.recv_timeout(std::time::Duration::from_secs(30)).map_err(|err| { + Error::Rpc(RpcError { + code: -1, + message: format!("{err}"), + data: None, + }) + })?; + if let Event::Lightning(LightningEvent::CloseChannelEvent { + message, + channel_id, + counterparty_node_id, + funding_utxo, + }) = event + { + break (message, channel_id, counterparty_node_id, funding_utxo); + } + }; let resp = match res { - Ok(resp) => Ok(resp), + Ok(_) => Ok(json::json!({ + "message" : message, + "channel_id" : channel_id, + "counterparty_node_id" : node_id, + "funding_utxo" : funding_utxo, + })), Err(err) => Err(Error::Rpc(RpcError { code: -1, message: format!("{err}"), diff --git a/lampod/src/ln/channel_manager.rs b/lampod/src/ln/channel_manager.rs index d63c3b25..0862adcc 100644 --- a/lampod/src/ln/channel_manager.rs +++ b/lampod/src/ln/channel_manager.rs @@ -453,32 +453,14 @@ impl ChannelEvents for LampoChannelManager { fn close_channel( &self, channel: request::CloseChannel, - ) -> error::Result { + ) -> error::Result<()> { let channel_id = channel.channel_id()?; let node_id = channel.counterpart_node_id()?; self.manager() .close_channel(&channel_id, &node_id) .map_err(|err| error::anyhow!("{:?}", err))?; - let events = self.handler().events(); - let (message, channel_id, node_id, funding_utxo) = loop { - let event = events.recv_timeout(std::time::Duration::from_secs(30))?; - if let Event::Lightning(LightningEvent::CloseChannelEvent { - message, - channel_id, - counterparty_node_id, - funding_utxo, - }) = event - { - break (message, channel_id, counterparty_node_id, funding_utxo); - } - }; - Ok(CloseChannel { - channel_id, - message, - counterparty_node_id: node_id, - funding_utxo, - }) + Ok(()) } fn change_state_channel(&self, _: ChangeStateChannelEvent) -> error::Result<()> { unimplemented!() diff --git a/lampod/src/ln/events.rs b/lampod/src/ln/events.rs index 45bd9103..91f602d6 100644 --- a/lampod/src/ln/events.rs +++ b/lampod/src/ln/events.rs @@ -34,7 +34,7 @@ pub trait ChannelEvents { fn close_channel( &self, channel: request::CloseChannel, - ) -> error::Result; + ) -> error::Result<()>; fn change_state_channel(&self, event: ChangeStateChannelEvent) -> error::Result<()>; } diff --git a/tests/tests/src/lampo_cln_tests.rs b/tests/tests/src/lampo_cln_tests.rs index 1c71dd5e..4da68b9d 100644 --- a/tests/tests/src/lampo_cln_tests.rs +++ b/tests/tests/src/lampo_cln_tests.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use std::time::Duration; +use lampo_common::bitcoin::hashes::hex; use lampo_common::error; use lampo_common::event::ln::LightningEvent; use lampo_common::event::onchain::OnChainEvent; @@ -539,3 +540,100 @@ fn be_able_to_kesend_payments() { assert!(result.is_ok(), "{:?}", result); async_run!(cln.stop()).unwrap(); } + +#[test] +fn close_channel() { + init(); + let cln = async_run!(cln::Node::with_params( + "--developer --dev-bitcoind-poll=1 --dev-fast-gossip --dev-allow-localhost", + "regtest" + )) + .unwrap(); + std::thread::sleep(Duration::from_secs(2)); + let btc = cln.btc(); + let lampo_manager = LampoTesting::new(btc.clone()).unwrap(); + let lampo = lampo_manager.lampod(); + let _info: response::GetInfo = lampo.call("getinfo", json::json!({})).unwrap(); + let info_cln = cln.rpc().getinfo().unwrap(); + let events = lampo.events(); + let address = lampo_manager.fund_wallet(101).unwrap(); + wait!(|| { + let Ok(Event::OnChain(OnChainEvent::NewBestBlock((_, height)))) = + events.recv_timeout(Duration::from_millis(100)) + else { + return Err(()); + }; + if height.to_consensus_u32() == 101 { + return Ok(()); + } + Err(()) + }); + let _: json::Value = lampo + .call( + "fundchannel", + request::OpenChannel { + node_id: cln.rpc().getinfo().unwrap().id, + port: Some(cln.port.into()), + amount: 1_500_000_000, + public: true, + addr: Some("127.0.0.1".to_owned()), + }, + ) + .unwrap(); + + std::thread::sleep(Duration::from_secs(2)); + + // Get the transaction confirmed + let _ = btc.rpc().generate_to_address(6, &address).unwrap(); + wait!(|| { + log::info!(target: "tests", "wait for confimetion"); + let _ = btc.rpc().generate_to_address(1, &address).unwrap(); + // Get the transaction confirmed + for _ in 0..100 { + let Ok(event) = events.recv_timeout(Duration::from_nanos(100)) else { + continue; + }; + log::info!(target: "tests", "lampo event: {:?}", event); + match event { + Event::Lightning(LightningEvent::ChannelReady { .. }) => return Ok(()), + _ => continue, + }; + } + Err(()) + }); + + wait!(|| { + let channels = cln.rpc().listfunds().unwrap().channels; + if channels.is_empty() { + return Err(()); + } + + let mut channels = cln.rpc().listfunds().unwrap().channels; + let origin_size = channels.len(); + channels.retain(|chan| chan.state == "CHANNELD_NORMAL"); + if channels.len() == origin_size { + return Ok(()); + } + + let channels: response::Channels = lampo.call("channels", json::json!({})).unwrap(); + if !channels.channels.first().unwrap().ready { + return Err(()); + } + let address = cln.rpc().newaddr(None).unwrap(); + fund_wallet(btc.clone(), &address.bech32.unwrap(), 1).unwrap(); + crate::wait_cln_sync!(cln); + Err(()) + }); + + // This should return the final channel_id as channel_id may differ from the time being in ChannelPending, ChannelClosed state. + let channels: response::Channels = lampo.call("channels", json::json!({})).unwrap(); + + let result: Result = lampo.call( + "close", + request::CloseChannel { + node_id: info_cln.id.to_string(), + channel_id: Some(channels.channels.first().unwrap().channel_id), + }, + ); + assert!(result.is_ok(), "{:?}", result); +} From 05574d94096b672c72a5343f1dec3e12f850f0a6 Mon Sep 17 00:00:00 2001 From: harshit933 Date: Wed, 20 Mar 2024 21:15:27 +0530 Subject: [PATCH 3/4] fix : make the `channel_id` inside `close` optional When there is only one channel open with the given `node_id` we can just skip the `channel_id` from the `close` request. --- lampo-common/src/event/ln.rs | 4 +- lampo-common/src/model/close_channel.rs | 5 +++ lampod/src/actions/handler.rs | 12 +---- lampod/src/jsonrpc/channels.rs | 57 +++++++++++++++++++++--- lampod/src/ln/channel_manager.rs | 8 +--- lampod/src/ln/events.rs | 5 +-- tests/tests/src/lampo_cln_tests.rs | 59 +++++++++++++++++++++++-- 7 files changed, 118 insertions(+), 32 deletions(-) diff --git a/lampo-common/src/event/ln.rs b/lampo-common/src/event/ln.rs index 8b3265b5..2b1180f1 100644 --- a/lampo-common/src/event/ln.rs +++ b/lampo-common/src/event/ln.rs @@ -41,7 +41,7 @@ pub enum LightningEvent { CloseChannelEvent { channel_id: String, message: String, - counterparty_node_id: String, - funding_utxo: String, + counterparty_node_id: Option, + funding_utxo: Option, }, } diff --git a/lampo-common/src/model/close_channel.rs b/lampo-common/src/model/close_channel.rs index 4c9a7007..2c224500 100644 --- a/lampo-common/src/model/close_channel.rs +++ b/lampo-common/src/model/close_channel.rs @@ -37,6 +37,11 @@ pub mod request { Ok(ChannelId::from_bytes(result_array)) } + /// This converts hex to bytes array. + /// Stolen from https://stackoverflow.com/a/52992629 + /// It takes two values every in each iteration from the hex + /// then convert the formed hexdecimal digit to u8, collects it in a vector + /// and return it (redix = 16 for hexadecimal) fn decode_hex(&self, s: &str) -> Result, ParseIntError> { (0..s.len()) .step_by(2) diff --git a/lampod/src/actions/handler.rs b/lampod/src/actions/handler.rs index 19808bfc..5673f340 100644 --- a/lampod/src/actions/handler.rs +++ b/lampod/src/actions/handler.rs @@ -159,16 +159,8 @@ impl Handler for LampoHandler { channel_funding_txo, .. } => { - let node_id = if let Some(id) = counterparty_node_id { - id.to_string() - } else { - "Node_id not found".to_string() - }; - let txo = if let Some(txo) = channel_funding_txo { - txo.to_string() - } else { - "Outpoint not found".to_string() - }; + let node_id = counterparty_node_id.map(|id| id.to_string()); + let txo = channel_funding_txo.map(|txo| txo.to_string()); self.emit(Event::Lightning(LightningEvent::CloseChannelEvent { channel_id: channel_id.to_string(), message: reason.to_string(), counterparty_node_id : node_id, funding_utxo : txo})); log::info!("channel `{user_channel_id}` closed with reason: `{reason}`"); Ok(()) diff --git a/lampod/src/jsonrpc/channels.rs b/lampod/src/jsonrpc/channels.rs index d456ecea..e0a967c2 100644 --- a/lampod/src/jsonrpc/channels.rs +++ b/lampod/src/jsonrpc/channels.rs @@ -1,10 +1,11 @@ use lampo_common::event::ln::LightningEvent; use lampo_common::event::Event; +use lampo_common::handler::Handler; use lampo_common::json; use lampo_common::model::request; +use lampo_common::model::response; use lampo_jsonrpc::errors::Error; use lampo_jsonrpc::errors::RpcError; -use lampo_common::handler::Handler; use crate::ln::events::ChannelEvents; @@ -18,17 +19,61 @@ pub fn json_list_channels(ctx: &LampoDeamon, request: &json::Value) -> Result Result { log::info!("call for `closechannel` with request {:?}", request); - let request: request::CloseChannel = json::from_value(request.clone())?; + let mut request: request::CloseChannel = json::from_value(request.clone())?; let events = ctx.handler().events(); - let res = ctx.channel_manager().close_channel(request); - let (message, channel_id, node_id, funding_utxo) = loop { - let event = events.recv_timeout(std::time::Duration::from_secs(30)).map_err(|err| { + let res; + // This gives all the channels with associated peer + let channels: response::Channels = ctx + .handler() + .call( + "channels", + json::json!({ + "peer_id": request.node_id, + }), + ) + .map_err(|err| { Error::Rpc(RpcError { code: -1, message: format!("{err}"), data: None, }) })?; + if channels.channels.len() > 1 { + // check the channel_id if it is not none, if it is return an error + // and if it is not none then we need to have the channel_id that needs to be shut + if request.channel_id.is_none() { + return Err(Error::Rpc(RpcError { + code: -1, + message: format!("Channels > 1, provide `channel_id`"), + data: None, + })); + } else { + res = ctx.channel_manager().close_channel(request.clone()); + }; + } else if !channels.channels.is_empty() { + // This is the case where channel with the given node_id = 1 + // SAFETY: it is safe to unwrap because the channels is not empty + let channel = channels.channels.first().unwrap(); + request.channel_id = Some(channel.channel_id.clone()); + res = ctx.channel_manager().close_channel(request); + } else { + // No channels with the given peer. + return Err(Error::Rpc(RpcError { + code: -1, + message: format!("No channels with associated peer"), + data: None, + })); + }; + let (message, channel_id, node_id, funding_utxo) = loop { + let event = events + .recv_timeout(std::time::Duration::from_secs(30)) + .map_err(|err| { + Error::Rpc(RpcError { + code: -1, + message: format!("{err}"), + data: None, + }) + })?; if let Event::Lightning(LightningEvent::CloseChannelEvent { message, channel_id, @@ -38,7 +83,7 @@ pub fn json_close_channel(ctx: &LampoDeamon, request: &json::Value) -> Result Ok(json::json!({ "message" : message, diff --git a/lampod/src/ln/channel_manager.rs b/lampod/src/ln/channel_manager.rs index 0862adcc..ac7d6f84 100644 --- a/lampod/src/ln/channel_manager.rs +++ b/lampod/src/ln/channel_manager.rs @@ -10,7 +10,6 @@ use lampo_common::bitcoin::absolute::Height; use lampo_common::bitcoin::{BlockHash, Transaction}; use lampo_common::conf::{LampoConf, UserConfig}; use lampo_common::error; -use lampo_common::event::ln::LightningEvent; use lampo_common::event::onchain::OnChainEvent; use lampo_common::event::Event; use lampo_common::handler::Handler; @@ -32,7 +31,7 @@ use lampo_common::ldk::util::config::{ChannelHandshakeConfig, ChannelHandshakeLi use lampo_common::ldk::util::persist::read_channel_monitors; use lampo_common::ldk::util::ser::ReadableArgs; use lampo_common::model::request; -use lampo_common::model::response::{self, Channel, Channels, CloseChannel}; +use lampo_common::model::response::{self, Channel, Channels}; use crate::actions::handler::LampoHandler; use crate::chain::{LampoChainManager, WalletManager}; @@ -450,10 +449,7 @@ impl ChannelEvents for LampoChannelManager { }) } - fn close_channel( - &self, - channel: request::CloseChannel, - ) -> error::Result<()> { + fn close_channel(&self, channel: request::CloseChannel) -> error::Result<()> { let channel_id = channel.channel_id()?; let node_id = channel.counterpart_node_id()?; diff --git a/lampod/src/ln/events.rs b/lampod/src/ln/events.rs index 91f602d6..8e7ddbda 100644 --- a/lampod/src/ln/events.rs +++ b/lampod/src/ln/events.rs @@ -31,10 +31,7 @@ pub trait ChannelEvents { ) -> error::Result; /// Close a channel - fn close_channel( - &self, - channel: request::CloseChannel, - ) -> error::Result<()>; + fn close_channel(&self, channel: request::CloseChannel) -> error::Result<()>; fn change_state_channel(&self, event: ChangeStateChannelEvent) -> error::Result<()>; } diff --git a/tests/tests/src/lampo_cln_tests.rs b/tests/tests/src/lampo_cln_tests.rs index 4da68b9d..2d26e3dc 100644 --- a/tests/tests/src/lampo_cln_tests.rs +++ b/tests/tests/src/lampo_cln_tests.rs @@ -1,7 +1,6 @@ use std::str::FromStr; use std::time::Duration; -use lampo_common::bitcoin::hashes::hex; use lampo_common::error; use lampo_common::event::ln::LightningEvent; use lampo_common::event::onchain::OnChainEvent; @@ -549,7 +548,6 @@ fn close_channel() { "regtest" )) .unwrap(); - std::thread::sleep(Duration::from_secs(2)); let btc = cln.btc(); let lampo_manager = LampoTesting::new(btc.clone()).unwrap(); let lampo = lampo_manager.lampod(); @@ -581,7 +579,19 @@ fn close_channel() { ) .unwrap(); - std::thread::sleep(Duration::from_secs(2)); + // This would be the second channel + let _: json::Value = lampo + .call( + "fundchannel", + request::OpenChannel { + node_id: cln.rpc().getinfo().unwrap().id, + port: Some(cln.port.into()), + amount: 1_000_000_000, + public: true, + addr: Some("127.0.0.1".to_owned()), + }, + ) + .unwrap(); // Get the transaction confirmed let _ = btc.rpc().generate_to_address(6, &address).unwrap(); @@ -628,12 +638,53 @@ fn close_channel() { // This should return the final channel_id as channel_id may differ from the time being in ChannelPending, ChannelClosed state. let channels: response::Channels = lampo.call("channels", json::json!({})).unwrap(); + // This should fail as there are two channels with the peer so we need to pass the specific `channel_id` + let result: Result = lampo.call( + "close", + request::CloseChannel { + node_id: info_cln.id.to_string(), + channel_id: None, + }, + ); + + assert!(result.is_err(), "{:?}", result); + + // Closing the first channel let result: Result = lampo.call( "close", request::CloseChannel { node_id: info_cln.id.to_string(), - channel_id: Some(channels.channels.first().unwrap().channel_id), + channel_id: Some(channels.channels.first().unwrap().channel_id.to_string()), }, ); assert!(result.is_ok(), "{:?}", result); + // assert_eq!(&result.unwrap().counterparty_node_id, &info_cln.id.to_string()); + assert_eq!( + &result.unwrap().channel_id, + &channels.channels.first().unwrap().channel_id.to_string() + ); + + // Closing the second channel - at this point there is only 1 channel with the peer + let result: Result = lampo.call( + "close", + request::CloseChannel { + node_id: info_cln.id.to_string(), + channel_id: None, + }, + ); + assert!(result.is_ok(), "{:?}", result); + assert_eq!( + result.unwrap().counterparty_node_id, + info_cln.id.to_string() + ); + + // Closing the third channel (this channel does not exist) + let result: Result = lampo.call( + "close", + request::CloseChannel { + node_id: info_cln.id.to_string(), + channel_id: Some(channels.channels.first().unwrap().channel_id.to_string()), + }, + ); + assert!(result.is_err(), "{:?}", result); } From ecdc494d3e2aa3a49f6a735f4c43def0be24203d Mon Sep 17 00:00:00 2001 From: harshit933 Date: Fri, 22 Mar 2024 18:23:37 +0530 Subject: [PATCH 4/4] fix : refactor `close` command tests This commits includes some refactoring of the `close` channel tests and adds a `channel_id` test. --- lampo-common/src/model/close_channel.rs | 16 +- lampod/src/jsonrpc/channels.rs | 39 ++-- tests/tests/src/lampo_cln_tests.rs | 250 ++++++++++++++++++++++-- 3 files changed, 262 insertions(+), 43 deletions(-) diff --git a/lampo-common/src/model/close_channel.rs b/lampo-common/src/model/close_channel.rs index 2c224500..337b0bea 100644 --- a/lampo-common/src/model/close_channel.rs +++ b/lampo-common/src/model/close_channel.rs @@ -23,17 +23,13 @@ pub mod request { // Returns ChannelId in byte format from hex of channelid pub fn channel_id(&self) -> error::Result { - let id = if let Some(id) = &self.channel_id { - id - } else { - error::bail!("No node_id provided"); - }; + let id = self + .channel_id + .as_ref() + .ok_or(error::anyhow!("`channel_id` not found"))?; let result = self.decode_hex(&id)?; - // FIXME: We can do better here let mut result_array: [u8; 32] = [0; 32]; - for i in 0..32 { - result_array[i] = result[i] - } + result_array.copy_from_slice(&result); Ok(ChannelId::from_bytes(result_array)) } @@ -58,7 +54,7 @@ pub mod response { pub struct CloseChannel { pub channel_id: String, pub message: String, - pub counterparty_node_id: String, + pub peer_id: String, pub funding_utxo: String, } } diff --git a/lampod/src/jsonrpc/channels.rs b/lampod/src/jsonrpc/channels.rs index e0a967c2..80d3539b 100644 --- a/lampod/src/jsonrpc/channels.rs +++ b/lampod/src/jsonrpc/channels.rs @@ -21,7 +21,6 @@ pub fn json_close_channel(ctx: &LampoDeamon, request: &json::Value) -> Result Result 1 { + let res = if channels.channels.len() > 1 { // check the channel_id if it is not none, if it is return an error // and if it is not none then we need to have the channel_id that needs to be shut if request.channel_id.is_none() { @@ -48,14 +47,14 @@ pub fn json_close_channel(ctx: &LampoDeamon, request: &json::Value) -> Result Result { + return Err(Error::Rpc(RpcError { + code: -1, + message: format!("{err}"), + data: None, + })) + } + Ok(_) => {} + }; let (message, channel_id, node_id, funding_utxo) = loop { let event = events .recv_timeout(std::time::Duration::from_secs(30)) @@ -84,18 +93,10 @@ pub fn json_close_channel(ctx: &LampoDeamon, request: &json::Value) -> Result Ok(json::json!({ - "message" : message, - "channel_id" : channel_id, - "counterparty_node_id" : node_id, - "funding_utxo" : funding_utxo, - })), - Err(err) => Err(Error::Rpc(RpcError { - code: -1, - message: format!("{err}"), - data: None, - })), - }; - Ok(json::to_value(resp?)?) + Ok(json::json!({ + "message" : message, + "channel_id" : channel_id, + "peer_id" : node_id, + "funding_utxo" : funding_utxo, + })) } diff --git a/tests/tests/src/lampo_cln_tests.rs b/tests/tests/src/lampo_cln_tests.rs index 2d26e3dc..de556fd2 100644 --- a/tests/tests/src/lampo_cln_tests.rs +++ b/tests/tests/src/lampo_cln_tests.rs @@ -8,6 +8,7 @@ use lampo_common::event::Event; use lampo_common::handler::Handler; use lampo_common::json; use lampo_common::model::request; +use lampo_common::model::request::CloseChannel; use lampo_common::model::response; use lampo_common::model::response::InvoiceInfo; use lampo_common::model::Connect; @@ -541,9 +542,9 @@ fn be_able_to_kesend_payments() { } #[test] -fn close_channel() { +fn test_closing_two_channels_without_channelid_success() { init(); - let cln = async_run!(cln::Node::with_params( + let mut cln = async_run!(cln::Node::with_params( "--developer --dev-bitcoind-poll=1 --dev-fast-gossip --dev-allow-localhost", "regtest" )) @@ -635,9 +636,6 @@ fn close_channel() { Err(()) }); - // This should return the final channel_id as channel_id may differ from the time being in ChannelPending, ChannelClosed state. - let channels: response::Channels = lampo.call("channels", json::json!({})).unwrap(); - // This should fail as there are two channels with the peer so we need to pass the specific `channel_id` let result: Result = lampo.call( "close", @@ -648,8 +646,92 @@ fn close_channel() { ); assert!(result.is_err(), "{:?}", result); + async_run!(cln.stop()).unwrap(); +} + +#[test] +fn test_lampo_to_cln_close_channel_with_channel_id_success() { + init(); + let mut cln = async_run!(cln::Node::with_params( + "--developer --dev-bitcoind-poll=1 --dev-fast-gossip --dev-allow-localhost", + "regtest" + )) + .unwrap(); + let btc = cln.btc(); + let lampo_manager = LampoTesting::new(btc.clone()).unwrap(); + let lampo = lampo_manager.lampod(); + let _info: response::GetInfo = lampo.call("getinfo", json::json!({})).unwrap(); + let info_cln = cln.rpc().getinfo().unwrap(); + let events = lampo.events(); + let address = lampo_manager.fund_wallet(101).unwrap(); + wait!(|| { + let Ok(Event::OnChain(OnChainEvent::NewBestBlock((_, height)))) = + events.recv_timeout(Duration::from_millis(100)) + else { + return Err(()); + }; + if height.to_consensus_u32() == 101 { + return Ok(()); + } + Err(()) + }); + let _: json::Value = lampo + .call( + "fundchannel", + request::OpenChannel { + node_id: cln.rpc().getinfo().unwrap().id, + port: Some(cln.port.into()), + amount: 1_500_000_000, + public: true, + addr: Some("127.0.0.1".to_owned()), + }, + ) + .unwrap(); + + // Get the transaction confirmed + let _ = btc.rpc().generate_to_address(6, &address).unwrap(); + wait!(|| { + log::info!(target: "tests", "wait for confimetion"); + let _ = btc.rpc().generate_to_address(1, &address).unwrap(); + // Get the transaction confirmed + for _ in 0..100 { + let Ok(event) = events.recv_timeout(Duration::from_nanos(100)) else { + continue; + }; + log::info!(target: "tests", "lampo event: {:?}", event); + match event { + Event::Lightning(LightningEvent::ChannelReady { .. }) => return Ok(()), + _ => continue, + }; + } + Err(()) + }); + + wait!(|| { + let channels = cln.rpc().listfunds().unwrap().channels; + if channels.is_empty() { + return Err(()); + } + + let mut channels = cln.rpc().listfunds().unwrap().channels; + let origin_size = channels.len(); + channels.retain(|chan| chan.state == "CHANNELD_NORMAL"); + if channels.len() == origin_size { + return Ok(()); + } + + let channels: response::Channels = lampo.call("channels", json::json!({})).unwrap(); + if !channels.channels.first().unwrap().ready { + return Err(()); + } + let address = cln.rpc().newaddr(None).unwrap(); + fund_wallet(btc.clone(), &address.bech32.unwrap(), 1).unwrap(); + crate::wait_cln_sync!(cln); + Err(()) + }); + + let channels: response::Channels = lampo.call("channels", json::json!({})).unwrap(); - // Closing the first channel let result: Result = lampo.call( "close", request::CloseChannel { @@ -658,13 +740,100 @@ fn close_channel() { }, ); assert!(result.is_ok(), "{:?}", result); - // assert_eq!(&result.unwrap().counterparty_node_id, &info_cln.id.to_string()); assert_eq!( - &result.unwrap().channel_id, - &channels.channels.first().unwrap().channel_id.to_string() + result.as_ref().unwrap().channel_id, + channels.channels.first().unwrap().channel_id.to_string() + ); + assert_eq!( + &result.unwrap().peer_id, + &channels.channels.first().unwrap().peer_id.to_string() ); + async_run!(cln.stop()).unwrap(); +} + +#[test] +fn test_lampo_to_cln_close_channel_without_channel_id_success() { + init(); + let mut cln = async_run!(cln::Node::with_params( + "--developer --dev-bitcoind-poll=1 --dev-fast-gossip --dev-allow-localhost", + "regtest" + )) + .unwrap(); + let btc = cln.btc(); + let lampo_manager = LampoTesting::new(btc.clone()).unwrap(); + let lampo = lampo_manager.lampod(); + let _info: response::GetInfo = lampo.call("getinfo", json::json!({})).unwrap(); + let info_cln = cln.rpc().getinfo().unwrap(); + let events = lampo.events(); + let address = lampo_manager.fund_wallet(101).unwrap(); + wait!(|| { + let Ok(Event::OnChain(OnChainEvent::NewBestBlock((_, height)))) = + events.recv_timeout(Duration::from_millis(100)) + else { + return Err(()); + }; + if height.to_consensus_u32() == 101 { + return Ok(()); + } + Err(()) + }); + let _: json::Value = lampo + .call( + "fundchannel", + request::OpenChannel { + node_id: cln.rpc().getinfo().unwrap().id, + port: Some(cln.port.into()), + amount: 1_500_000_000, + public: true, + addr: Some("127.0.0.1".to_owned()), + }, + ) + .unwrap(); + + // Get the transaction confirmed + let _ = btc.rpc().generate_to_address(6, &address).unwrap(); + wait!(|| { + log::info!(target: "tests", "wait for confimetion"); + let _ = btc.rpc().generate_to_address(1, &address).unwrap(); + // Get the transaction confirmed + for _ in 0..100 { + let Ok(event) = events.recv_timeout(Duration::from_nanos(100)) else { + continue; + }; + log::info!(target: "tests", "lampo event: {:?}", event); + match event { + Event::Lightning(LightningEvent::ChannelReady { .. }) => return Ok(()), + _ => continue, + }; + } + Err(()) + }); + + wait!(|| { + let channels = cln.rpc().listfunds().unwrap().channels; + if channels.is_empty() { + return Err(()); + } + + let mut channels = cln.rpc().listfunds().unwrap().channels; + let origin_size = channels.len(); + channels.retain(|chan| chan.state == "CHANNELD_NORMAL"); + if channels.len() == origin_size { + return Ok(()); + } + + let channels: response::Channels = lampo.call("channels", json::json!({})).unwrap(); + if !channels.channels.first().unwrap().ready { + return Err(()); + } + let address = cln.rpc().newaddr(None).unwrap(); + fund_wallet(btc.clone(), &address.bech32.unwrap(), 1).unwrap(); + crate::wait_cln_sync!(cln); + Err(()) + }); + + let channels: response::Channels = lampo.call("channels", json::json!({})).unwrap(); - // Closing the second channel - at this point there is only 1 channel with the peer let result: Result = lampo.call( "close", request::CloseChannel { @@ -674,17 +843,70 @@ fn close_channel() { ); assert!(result.is_ok(), "{:?}", result); assert_eq!( - result.unwrap().counterparty_node_id, - info_cln.id.to_string() + result.as_ref().unwrap().channel_id, + channels.channels.first().unwrap().channel_id.to_string() ); + assert_eq!( + result.as_ref().unwrap().peer_id, + channels.channels.first().unwrap().peer_id.to_string() + ); + async_run!(cln.stop()).unwrap(); +} + +#[test] +fn test_close_channel_without_opening_a_channel_success() { + init(); + let mut cln = async_run!(cln::Node::with_params( + "--developer --dev-bitcoind-poll=1 --dev-fast-gossip --dev-allow-localhost", + "regtest" + )) + .unwrap(); + let btc = cln.btc(); + let lampo_manager = LampoTesting::new(btc.clone()).unwrap(); + let lampo = lampo_manager.lampod(); + let _info: response::GetInfo = lampo.call("getinfo", json::json!({})).unwrap(); + let info_cln = cln.rpc().getinfo().unwrap(); + let events = lampo.events(); + lampo_manager.fund_wallet(101).unwrap(); + wait!(|| { + let Ok(Event::OnChain(OnChainEvent::NewBestBlock((_, height)))) = + events.recv_timeout(Duration::from_millis(100)) + else { + return Err(()); + }; + if height.to_consensus_u32() == 101 { + return Ok(()); + } + Err(()) + }); - // Closing the third channel (this channel does not exist) + // Closing an un opened channel let result: Result = lampo.call( "close", request::CloseChannel { node_id: info_cln.id.to_string(), - channel_id: Some(channels.channels.first().unwrap().channel_id.to_string()), + channel_id: None, }, ); assert!(result.is_err(), "{:?}", result); + async_run!(cln.stop()).unwrap(); +} + +#[test] +fn channel_id_tests() { + let node_id = "039c108cc6777e7d5066dfa33c611c32e6baa1c49de6d546b5b76686486d0360ac".to_string(); + + // This is a correct channel_hex of 32 bytes + let channel_hex = + Some("0a44677526ac8c607616bd91258d7e5df1d86fae9c32e23aa18703a650944c64".to_string()); + let req = CloseChannel { + node_id: node_id.clone(), + channel_id: channel_hex, + }; + let channel_bytes = [ + 10, 68, 103, 117, 38, 172, 140, 96, 118, 22, 189, 145, 37, 141, 126, 93, 241, 216, 111, + 174, 156, 50, 226, 58, 161, 135, 3, 166, 80, 148, 76, 100, + ]; + let channel_id_bytes = req.channel_id(); + assert_eq!(channel_bytes, channel_id_bytes.unwrap().0); }