Skip to content

Commit

Permalink
Merge pull request #1806 from get10101/feat/collab-close-channel
Browse files Browse the repository at this point in the history
feat: allow collab and force close DLC channel
  • Loading branch information
bonomat authored Jan 9, 2024
2 parents a94cc1b + 61e5ed4 commit 1fbcff9
Show file tree
Hide file tree
Showing 15 changed files with 341 additions and 189 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Chore: move telegram link into toplevel of settings so that it can be found easier
- Feat: update coordinator API to show more details on pending channel balance
- Feat: show dlc-channel balance instead of ln-balance in app and in coordinator's API
- Feat: allow collaboratively close a channel from coordinator and the app
- Chore: don't allow multiple dlc-channels per user
- Feat: show dlc-channel opening transaction in transaction history
- Feat: allow force-close a DLC channel

## [1.7.4] - 2023-12-20

Expand Down
6 changes: 4 additions & 2 deletions coordinator/src/admin.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::collaborative_revert;
use crate::db;
use crate::parse_channel_id;
use crate::parse_dlc_channel_id;
use crate::routes::AppState;
use crate::AppError;
use anyhow::Context;
Expand Down Expand Up @@ -492,15 +493,16 @@ pub async fn close_channel(
Query(params): Query<CloseChannelParams>,
State(state): State<Arc<AppState>>,
) -> Result<(), AppError> {
let channel_id = parse_channel_id(&channel_id_string)
let channel_id = parse_dlc_channel_id(&channel_id_string)
.map_err(|_| AppError::BadRequest("Provided channel ID was invalid".to_string()))?;

tracing::info!(channel_id = %channel_id_string, "Attempting to close channel");

state
.node
.inner
.close_channel(channel_id, params.force.unwrap_or_default())
.close_dlc_channel(channel_id, params.force.unwrap_or_default())
.await
.map_err(|e| AppError::InternalServerError(format!("{e:#}")))?;

Ok(())
Expand Down
6 changes: 6 additions & 0 deletions coordinator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use diesel::PgConnection;
use diesel_migrations::embed_migrations;
use diesel_migrations::EmbeddedMigrations;
use diesel_migrations::MigrationHarness;
use dlc_manager::DlcChannelId;
use hex::FromHex;
use lightning::ln::ChannelId;
use rust_decimal::prelude::FromPrimitive;
use rust_decimal::prelude::ToPrimitive;
Expand Down Expand Up @@ -91,6 +93,10 @@ pub fn parse_channel_id(channel_id: &str) -> Result<ChannelId> {
Ok(ChannelId(channel_id))
}

pub fn parse_dlc_channel_id(channel_id: &str) -> Result<DlcChannelId> {
Ok(DlcChannelId::from_hex(channel_id)?)
}

pub fn compute_relative_contracts(contracts: Decimal, direction: &::trade::Direction) -> Decimal {
match direction {
::trade::Direction::Long => contracts,
Expand Down
13 changes: 13 additions & 0 deletions coordinator/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,19 @@ impl Node {
}
}

if let Message::Channel(ChannelMessage::CollaborativeCloseOffer(close_offer)) = &msg {
let channel_id_hex_string = close_offer.channel_id.to_hex();
tracing::info!(
channel_id = channel_id_hex_string,
node_id = node_id.to_string(),
"Received an offer to collaboratively close a channel"
);

// TODO(bonomat): we should verify that the proposed amount is acceptable
self.inner
.accept_dlc_channel_collaborative_close(close_offer.channel_id)?;
}

if let Message::SubChannel(SubChannelMessage::CloseFinalize(msg)) = &msg {
let mut connection = self.pool.get()?;
match db::positions::Position::get_position_by_trader(
Expand Down
2 changes: 1 addition & 1 deletion crates/fund/examples/fund.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async fn fund_everything(faucet: &str, coordinator: &str) -> Result<()> {
let coordinator_balance = coordinator.get_balance().await?;
tracing::info!(
onchain = %Amount::from_sat(coordinator_balance.onchain),
offchain = %Amount::from_sat(coordinator_balance.offchain),
offchain = %Amount::from_sat(coordinator_balance.dlc_channel),
"Coordinator balance",
);

Expand Down
92 changes: 91 additions & 1 deletion crates/ln-dlc-node/src/node/dlc_channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,85 @@ impl<S: TenTenOneStorage + 'static, N: LnDlcStorage + Sync + Send + 'static> Nod
Ok(())
}

pub async fn close_dlc_channel(&self, channel_id: DlcChannelId, force: bool) -> Result<()> {
let channel_id_hex = hex::encode(channel_id);
tracing::info!(channel_id = channel_id_hex, "Closing DLC channel");

let channel = self
.get_dlc_channel(|channel| channel.channel_id == channel_id)?
.context("DLC channel to close not found")?;

if force {
self.force_close_dlc_channel(&channel_id)?;
} else {
self.propose_dlc_channel_collaborative_close(channel)
.await?
}

Ok(())
}

fn force_close_dlc_channel(&self, channel_id: &DlcChannelId) -> Result<()> {
let channel_id_hex = hex::encode(channel_id);

tracing::info!(
channel_id = %channel_id_hex,
"Force closing DLC channel"
);

self.dlc_manager.force_close_channel(channel_id)?;
Ok(())
}

/// Collaboratively close a DLC channel on-chain if there is no open position
async fn propose_dlc_channel_collaborative_close(&self, channel: SignedChannel) -> Result<()> {
let channel_id_hex = hex::encode(channel.channel_id);

tracing::info!(
channel_id = %channel_id_hex,
"Closing DLC channel collaboratively"
);

let counterparty = channel.counter_party;

match channel.state {
SignedChannelState::Settled { .. } | SignedChannelState::RenewFinalized { .. } => {
spawn_blocking({
let dlc_manager = self.dlc_manager.clone();
let dlc_message_handler = self.dlc_message_handler.clone();
let peer_manager = self.peer_manager.clone();
move || {
let settle_offer = dlc_manager
.offer_collaborative_close(
&channel.channel_id,
channel.counter_params.collateral,
)
.context(
"Could not propose to collaboratively close the dlc channel.",
)?;

send_dlc_message(
&dlc_message_handler,
&peer_manager,
counterparty,
Message::Channel(ChannelMessage::CollaborativeCloseOffer(settle_offer)),
);

anyhow::Ok(())
}
})
.await??;
}
_ => {
tracing::error!( state = %channel.state, "Can't collaboratively close a channel with an open position.");
bail!("Can't collaboratively close a channel with an open position");
}
}

Ok(())
}

/// Collaboratively close a position within a DLC Channel
pub async fn propose_dlc_channel_collaborative_settlement(
&self,
channel_id: DlcChannelId,
Expand All @@ -168,7 +247,7 @@ impl<S: TenTenOneStorage + 'static, N: LnDlcStorage + Sync + Send + 'static> Nod
tracing::info!(
channel_id = %channel_id_hex,
%accept_settlement_amount,
"Settling DLC channel collaboratively"
"Settling DLC in channel collaboratively"
);

spawn_blocking({
Expand All @@ -192,6 +271,17 @@ impl<S: TenTenOneStorage + 'static, N: LnDlcStorage + Sync + Send + 'static> Nod
.await?
}

pub fn accept_dlc_channel_collaborative_close(&self, channel_id: DlcChannelId) -> Result<()> {
let channel_id_hex = hex::encode(channel_id);

tracing::info!(channel_id = %channel_id_hex, "Accepting DLC channel collaborative close offer");

let dlc_manager = self.dlc_manager.clone();
dlc_manager.accept_collaborative_close(&channel_id)?;

Ok(())
}

pub fn accept_dlc_channel_collaborative_settlement(
&self,
channel_id: DlcChannelId,
Expand Down
141 changes: 140 additions & 1 deletion crates/ln-dlc-node/src/tests/dlc_channel.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,154 @@
use crate::node::InMemoryStore;
use crate::node::Node;
use crate::storage::TenTenOneInMemoryStorage;
use crate::tests::bitcoind::mine;
use crate::tests::dummy_contract_input;
use crate::tests::init_tracing;
use crate::tests::wait_until;
use bitcoin::Amount;
use dlc_manager::channel::signed_channel::SignedChannel;
use dlc_manager::channel::signed_channel::SignedChannelStateType;
use dlc_manager::contract::Contract;
use dlc_manager::Storage;
use std::sync::Arc;
use std::time::Duration;

#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn vanilla_dlc_channel() {
async fn can_open_and_collaboratively_close_channel() {
init_tracing();

// Arrange
let (app, coordinator, coordinator_signed_channel, app_signed_channel) =
setup_channel_with_position().await;

let app_on_chain_balance_before_close = app.get_on_chain_balance().unwrap();
let coordinator_on_chain_balance_before_close = coordinator.get_on_chain_balance().unwrap();

tracing::debug!("Proposing to close dlc channel collaboratively");

coordinator
.close_dlc_channel(app_signed_channel.channel_id, false)
.await
.unwrap();

wait_until(Duration::from_secs(10), || async {
app.process_incoming_messages()?;

let dlc_channels = app
.dlc_manager
.get_store()
.get_signed_channels(Some(SignedChannelStateType::CollaborativeCloseOffered))?;

Ok(dlc_channels
.iter()
.find(|dlc_channel| dlc_channel.counter_party == coordinator.info.pubkey)
.cloned())
})
.await
.unwrap();

tracing::debug!("Accepting collaborative close offer");

app.accept_dlc_channel_collaborative_close(coordinator_signed_channel.channel_id)
.unwrap();

wait_until(Duration::from_secs(10), || async {
mine(1).await.unwrap();
coordinator.sync_wallets().await?;

let coordinator_on_chain_balances_after_close = coordinator.get_on_chain_balance()?;

let coordinator_balance_changed = coordinator_on_chain_balances_after_close.confirmed
> coordinator_on_chain_balance_before_close.confirmed;

if coordinator_balance_changed {
tracing::debug!(
old_balance = coordinator_on_chain_balance_before_close.confirmed,
new_balance = coordinator_on_chain_balances_after_close.confirmed,
"Balance updated"
)
}

Ok(coordinator_balance_changed.then_some(true))
})
.await
.unwrap();

wait_until(Duration::from_secs(10), || async {
mine(1).await.unwrap();
app.sync_wallets().await?;

let app_on_chain_balances_after_close = app.get_on_chain_balance()?;

let app_balance_changed = app_on_chain_balances_after_close.confirmed
> app_on_chain_balance_before_close.confirmed;
if app_balance_changed {
tracing::debug!(
old_balance = app_on_chain_balance_before_close.confirmed,
new_balance = app_on_chain_balances_after_close.confirmed,
"Balance updated"
)
}

Ok(app_balance_changed.then_some(()))
})
.await
.unwrap();
}

#[tokio::test(flavor = "multi_thread")]
#[ignore]
async fn can_open_and_force_close_channel() {
init_tracing();

// Arrange
let (app, coordinator, coordinator_signed_channel, _) = setup_channel_with_position().await;

tracing::debug!("Force closing dlc channel");

wait_until(Duration::from_secs(10), || async {
mine(1).await.unwrap();

let dlc_channels = coordinator
.dlc_manager
.get_store()
.get_signed_channels(None)?;
Ok(dlc_channels
.iter()
.find(|dlc_channel| dlc_channel.counter_party == app.info.pubkey)
.cloned())
})
.await
.unwrap();

coordinator
.close_dlc_channel(coordinator_signed_channel.channel_id, true)
.await
.unwrap();

wait_until(Duration::from_secs(10), || async {
mine(1).await.unwrap();

let dlc_channels = coordinator
.dlc_manager
.get_store()
.get_signed_channels(None)?;
Ok(dlc_channels.is_empty().then_some(()))
})
.await
.unwrap();

// TODO: we could also test that the DLCs are being spent, but for that we would need a TARDIS
// or similar
}

async fn setup_channel_with_position() -> (
Arc<Node<TenTenOneInMemoryStorage, InMemoryStore>>,
Arc<Node<TenTenOneInMemoryStorage, InMemoryStore>>,
SignedChannel,
SignedChannel,
) {
let app_dlc_collateral = 10_000;
let coordinator_dlc_collateral = 10_000;

Expand Down Expand Up @@ -245,4 +378,10 @@ async fn vanilla_dlc_channel() {
})
.await
.unwrap();
(
app,
coordinator,
coordinator_signed_channel,
app_signed_channel,
)
}
Loading

0 comments on commit 1fbcff9

Please sign in to comment.