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

feat: allow collab and force close DLC channel #1806

Merged
merged 5 commits into from
Jan 9, 2024
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
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),
luckysori marked this conversation as resolved.
Show resolved Hide resolved
"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
Loading