diff --git a/dlc-manager/src/chain_monitor.rs b/dlc-manager/src/chain_monitor.rs index ccdcd2f9..85d0d61b 100644 --- a/dlc-manager/src/chain_monitor.rs +++ b/dlc-manager/src/chain_monitor.rs @@ -47,6 +47,10 @@ pub(crate) enum TxType { SplitTx, SettleTx, Cet, + // Introduced new variant to maintain backwards-compatibility. + SettleTx2 { + is_offer: bool, + }, } impl_dlc_writeable_enum!(TxType,; @@ -55,6 +59,9 @@ impl_dlc_writeable_enum!(TxType,; (own_adaptor_signature, {cb_writeable, write_ecdsa_adaptor_signature, read_ecdsa_adaptor_signature}), (is_offer, writeable), (revoked_tx_type, writeable) + }), + (6, SettleTx2, { + (is_offer, writeable) });; (1, BufferTx), (2, CollaborativeClose), (3, SplitTx), (4, SettleTx), (5, Cet) ); @@ -180,6 +187,43 @@ impl ChainMonitor { } } + /// Heuristic to figure out if we sent the last settle offer. + pub(crate) fn did_we_offer_last_channel_settlement( + &self, + channel_id: &[u8; 32], + ) -> Option { + let mut watched_txs = self.watched_tx.iter(); + + watched_txs.find_map(|(_, state)| match state { + WatchState::Registered { + channel_info: + ChannelInfo { + channel_id: cid, + tx_type: + TxType::Revoked { + revoked_tx_type: RevokedTxType::Buffer, + is_offer, + .. + }, + }, + } + | WatchState::Confirmed { + channel_info: + ChannelInfo { + channel_id: cid, + tx_type: + TxType::Revoked { + revoked_tx_type: RevokedTxType::Buffer, + is_offer, + .. + }, + }, + .. + } if channel_id == cid => Some(*is_offer), + _ => None, + }) + } + /// All the currently watched transactions which have been confirmed. pub(crate) fn confirmed_txs(&self) -> Vec<(Transaction, ChannelInfo)> { (self.watched_tx.values()) diff --git a/dlc-manager/src/channel/mod.rs b/dlc-manager/src/channel/mod.rs index 7dcd30f5..947b02ed 100644 --- a/dlc-manager/src/channel/mod.rs +++ b/dlc-manager/src/channel/mod.rs @@ -31,6 +31,10 @@ pub enum Channel { /// has broadcast a buffer transaction and is waiting to finalize the /// closing of the channel by broadcasting a CET. Closing(ClosingChannel), + /// A [`Channel`] is in `SettledClosing` state when the local party + /// has broadcast a settle transaction and is waiting to finalize the + /// closing of the channel by claiming their output. + SettledClosing(SettledClosingChannel), /// A [`Channel`] is in `Closed` state when it was force closed by /// the local party. Closed(ClosedChannel), @@ -64,6 +68,7 @@ impl std::fmt::Debug for Channel { Channel::FailedAccept(_) => "failed accept", Channel::FailedSign(_) => "failed sign", Channel::Closing(_) => "closing", + Channel::SettledClosing(_) => "settled closing", Channel::Closed(_) => "closed", Channel::CounterClosed(_) => "counter closed", Channel::ClosedPunished(_) => "closed punished", @@ -84,6 +89,7 @@ impl Channel { Channel::FailedAccept(f) => f.counter_party, Channel::FailedSign(f) => f.counter_party, Channel::Closing(c) => c.counter_party, + Channel::SettledClosing(c) => c.counter_party, Channel::Closed(c) | Channel::CounterClosed(c) | Channel::CollaborativelyClosed(c) => { c.counter_party } @@ -101,6 +107,7 @@ impl Channel { Channel::FailedAccept(f) => f.reference_id, Channel::FailedSign(f) => f.reference_id, Channel::Closing(c) => c.reference_id, + Channel::SettledClosing(c) => c.reference_id, Channel::Closed(c) | Channel::CounterClosed(c) | Channel::CollaborativelyClosed(c) => { c.reference_id } @@ -167,6 +174,28 @@ pub struct ClosingChannel { pub reference_id: Option, } +#[derive(Clone)] +/// A channel is closing when its buffer transaction was broadcast or detected on chain. +pub struct SettledClosingChannel { + /// The [`secp256k1_zkp::PublicKey`] of the counter party. + pub counter_party: PublicKey, + /// The temporary [`DlcChannelId`] of the channel. + pub temporary_channel_id: DlcChannelId, + /// The [`DlcChannelId`] for the channel. + pub channel_id: DlcChannelId, + /// The previous state the channel was before being closed, if that state was the `Signed` one, + /// otherwise is `None`. + pub rollback_state: Option, + /// The settle transaction that was broadcast. + pub settle_transaction: Transaction, + /// The claim transaction that was broadcast. + pub claim_transaction: Transaction, + /// Whether the local party initiated the closing of the channel. + pub is_closer: bool, + /// The reference id set by the api user. + pub reference_id: Option, +} + #[derive(Clone)] /// A channel is closed when its buffer transaction has been spent. pub struct ClosedChannel { @@ -223,6 +252,7 @@ impl Channel { Channel::FailedAccept(f) => f.temporary_channel_id, Channel::FailedSign(f) => f.channel_id, Channel::Closing(c) => c.channel_id, + Channel::SettledClosing(c) => c.channel_id, Channel::Closed(c) | Channel::CounterClosed(c) | Channel::CollaborativelyClosed(c) => { c.channel_id } diff --git a/dlc-manager/src/channel/ser.rs b/dlc-manager/src/channel/ser.rs index 2144c0bf..c0746537 100644 --- a/dlc-manager/src/channel/ser.rs +++ b/dlc-manager/src/channel/ser.rs @@ -3,7 +3,7 @@ use super::accepted_channel::AcceptedChannel; use super::offered_channel::OfferedChannel; use super::party_points::PartyBasePoints; use super::signed_channel::{SignedChannel, SignedChannelState}; -use super::{ClosedChannel, ClosedPunishedChannel, ClosingChannel, FailedAccept, FailedSign}; +use super::{ClosedChannel, ClosedPunishedChannel, ClosingChannel, FailedAccept, FailedSign, SettledClosingChannel}; use dlc_messages::ser_impls::{ read_ecdsa_adaptor_signature, read_string, write_ecdsa_adaptor_signature, write_string, @@ -64,7 +64,8 @@ impl_dlc_writeable_enum!( (8, RenewConfirmed, {(contract_id, writeable), (offer_per_update_point, writeable), (accept_per_update_point, writeable), (buffer_transaction, writeable), (buffer_script_pubkey, writeable), (offer_buffer_adaptor_signature, {cb_writeable, write_ecdsa_adaptor_signature, read_ecdsa_adaptor_signature}), (timeout, writeable), (own_payout, writeable), (total_collateral, writeable)}), (10, RenewFinalized, {(contract_id, writeable), (prev_offer_per_update_point, writeable), (buffer_transaction, writeable), (buffer_script_pubkey, writeable), (offer_buffer_adaptor_signature, {cb_writeable, write_ecdsa_adaptor_signature, read_ecdsa_adaptor_signature}), (accept_buffer_adaptor_signature, {cb_writeable, write_ecdsa_adaptor_signature, read_ecdsa_adaptor_signature}), (timeout, writeable), (own_payout, writeable), (total_collateral, writeable)}), (9, Closing, {(buffer_transaction, writeable), (contract_id, writeable), (is_initiator, writeable)}), - (11, CollaborativeCloseOffered, { (counter_payout, writeable), (offer_signature, writeable), (close_tx, writeable), (timeout, writeable), (is_offer, writeable) }) + (11, CollaborativeCloseOffered, { (counter_payout, writeable), (offer_signature, writeable), (close_tx, writeable), (timeout, writeable), (is_offer, writeable) }), + (12, SettledClosing, {(settle_transaction, writeable), (is_offer, writeable), (is_initiator, writeable)}) ;; ); @@ -81,5 +82,15 @@ impl_dlc_writeable!(ClosingChannel, { (is_closer, writeable), (reference_id, option) }); +impl_dlc_writeable!(SettledClosingChannel, { + (channel_id, writeable), + (counter_party, writeable), + (temporary_channel_id, writeable), + (rollback_state, option), + (settle_transaction, writeable), + (claim_transaction, writeable), + (is_closer, writeable), + (reference_id, option) +}); impl_dlc_writeable!(ClosedChannel, {(channel_id, writeable), (counter_party, writeable), (temporary_channel_id, writeable), (reference_id, option), (closing_txid, writeable)}); impl_dlc_writeable!(ClosedPunishedChannel, {(channel_id, writeable), (counter_party, writeable), (temporary_channel_id, writeable), (punish_txid, writeable), (reference_id, option)}); diff --git a/dlc-manager/src/channel/signed_channel.rs b/dlc-manager/src/channel/signed_channel.rs index e2637dff..58add47a 100644 --- a/dlc-manager/src/channel/signed_channel.rs +++ b/dlc-manager/src/channel/signed_channel.rs @@ -297,6 +297,17 @@ typed_enum!( /// Whether the local party initiated the closing of the channel. is_initiator: bool, }, + /// A [`SignedChannel`] is in `SettledClosing` state when a settle transaction has been + /// broadcast, and we are waiting to finalize the closing of the channel by claiming one of + /// its outputs. + SettledClosing { + /// The settle transaction that was broadcast. + settle_transaction: Transaction, + /// Indicates whether the local party originally offered to settle the channel or not. + is_offer: bool, + /// Whether the local party initiated the closing of the channel. + is_initiator: bool, + }, /// A [`SignedChannel`] is in `CollaborativeCloseOffered` state when the local party /// has sent a [`dlc_messages::channel::CollaborativeCloseOffer`] message. CollaborativeCloseOffered { diff --git a/dlc-manager/src/channel_updater.rs b/dlc-manager/src/channel_updater.rs index 9783eb5f..662ae7e1 100644 --- a/dlc-manager/src/channel_updater.rs +++ b/dlc-manager/src/channel_updater.rs @@ -7,7 +7,7 @@ use crate::{chain_monitor::{ChainMonitor, ChannelInfo, TxType}, channel::{ offered_channel::OfferedChannel, party_points::PartyBasePoints, signed_channel::{SignedChannel, SignedChannelState}, - Channel, ClosedChannel, + Channel, ClosedChannel, SettledClosingChannel, }, contract::{ accepted_contract::AcceptedContract, contract_info::ContractInfo, contract_input::ContractInput, offered_contract::OfferedContract, @@ -15,8 +15,8 @@ use crate::{chain_monitor::{ChainMonitor, ChannelInfo, TxType}, channel::{ }, contract_updater::{ accept_contract_internal, verify_accepted_and_sign_contract_internal, verify_signed_contract_internal, -}, error::Error, subchannel::{ClosingSubChannel, SubChannel}, Blockchain, ContractId, DlcChannelId, Signer, Time, Wallet, ReferenceId}; -use bitcoin::{OutPoint, Script, Sequence, Transaction}; +}, error::Error, subchannel::{ClosingSubChannel, SubChannel}, Blockchain, ContractId, DlcChannelId, Signer, Time, Wallet, ReferenceId, manager::CET_NSEQUENCE}; +use bitcoin::{OutPoint, Script, Sequence, Transaction, Address}; use dlc::{ channel::{get_tx_adaptor_signature, verify_tx_adaptor_signature, DlcChannelTransactions}, util::dlc_channel_extra_fee, PartyParams }; @@ -1000,7 +1000,7 @@ where settle_tx.txid(), ChannelInfo { channel_id: channel.channel_id, - tx_type: TxType::SettleTx, + tx_type: TxType::SettleTx2 { is_offer: false }, }, ); @@ -1134,7 +1134,7 @@ where settle_tx.txid(), ChannelInfo { channel_id: channel.channel_id, - tx_type: TxType::SettleTx, + tx_type: TxType::SettleTx2 { is_offer: true }, }, ); @@ -2712,26 +2712,15 @@ where Ok((cet, channel)) } -/// Sign the settlement transaction and update the state of the channel. -pub fn close_settled_channel( +pub(crate) fn initiate_unilateral_close_settled_channel( secp: &Secp256k1, signed_channel: &mut SignedChannel, signer: &S, - is_initiator: bool, -) -> Result<(Transaction, Channel), Error> -where - S::Target: Signer, -{ - close_settled_channel_internal(secp, signed_channel, signer, None, is_initiator) -} - -pub(crate) fn close_settled_channel_internal( - secp: &Secp256k1, - signed_channel: &SignedChannel, - signer: &S, sub_channel: Option<(SubChannel, &ClosingSubChannel)>, + is_settle_offer: bool, is_initiator: bool, -) -> Result<(Transaction, Channel), Error> + reference_id: Option +) -> Result<(), Error> where S::Target: Signer, { @@ -2832,24 +2821,80 @@ where )?; } - let channel = if is_initiator { - Channel::Closed(ClosedChannel { - counter_party: signed_channel.counter_party, - temporary_channel_id: signed_channel.temporary_channel_id, - channel_id: signed_channel.channel_id, - reference_id: signed_channel.reference_id, - closing_txid: settle_tx.txid() - }) - } else { - Channel::CounterClosed(ClosedChannel { - counter_party: signed_channel.counter_party, - temporary_channel_id: signed_channel.temporary_channel_id, - channel_id: signed_channel.channel_id, - reference_id: signed_channel.reference_id, - closing_txid: settle_tx.txid() - }) + + + signed_channel.state = SignedChannelState::SettledClosing { + settle_transaction: settle_tx, + is_offer: is_settle_offer, + is_initiator, }; - Ok((settle_tx, channel)) + + signed_channel.reference_id = reference_id; + + Ok(()) +} + +/// Spend the settle transaction output owned by us. +pub fn finalize_unilateral_close_settled_channel( + secp: &Secp256k1, + signed_channel: &SignedChannel, + destination_address: &Address, + fee_rate_per_vb: u64, + signer: &S, + is_offer: bool, + is_initiator: bool, +) -> Result<(Transaction, Channel), Error> +where + S::Target: Signer, +{ + let settle_transaction = + get_signed_channel_state!(signed_channel, SettledClosing, settle_transaction)?; + + let own_revoke_params = signed_channel.own_points.get_revokable_params( + secp, + &signed_channel.counter_points.revocation_basepoint, + &signed_channel.own_per_update_point, + ); + + let counter_revoke_params = signed_channel.counter_points.get_revokable_params( + secp, + &signed_channel.own_points.revocation_basepoint, + &signed_channel.counter_per_update_point, + ); + + let own_basepoint = signed_channel.own_points.own_basepoint; + let own_per_update_point = signed_channel.own_per_update_point; + + let base_secret = signer.get_secret_key_for_pubkey(&own_basepoint)?; + let own_sk = derive_private_key(secp, &own_per_update_point, &base_secret); + + let claim_tx = dlc::channel::create_and_sign_claim_settle_transaction( + secp, + &own_revoke_params, + &counter_revoke_params, + &own_sk, + settle_transaction, + destination_address, + CET_NSEQUENCE, + 0, + fee_rate_per_vb, + is_offer, + )?; + + let closing_channel = SettledClosingChannel { + counter_party: signed_channel.counter_party, + temporary_channel_id: signed_channel.temporary_channel_id, + channel_id: signed_channel.channel_id, + rollback_state: Some(signed_channel.clone()), + settle_transaction: settle_transaction.clone(), + claim_transaction: claim_tx.clone(), + is_closer: is_initiator, + reference_id: signed_channel.reference_id, + }; + + let channel = Channel::SettledClosing(closing_channel); + + Ok((claim_tx, channel)) } /// Returns the current time as unix time (in seconds) diff --git a/dlc-manager/src/lib.rs b/dlc-manager/src/lib.rs index f6b98fd2..8ecc00eb 100644 --- a/dlc-manager/src/lib.rs +++ b/dlc-manager/src/lib.rs @@ -45,7 +45,7 @@ use bitcoin::{Address, Block, OutPoint, Script, Transaction, TxOut, Txid}; use chain_monitor::ChainMonitor; use channel::offered_channel::OfferedChannel; use channel::signed_channel::{SignedChannel, SignedChannelStateType}; -use channel::Channel; +use channel::{Channel, SettledClosingChannel}; use contract::PreClosedContract; use contract::{offered_contract::OfferedContract, signed_contract::SignedContract, Contract}; use dlc_messages::oracle_msgs::{OracleAnnouncement, OracleAttestation}; @@ -182,6 +182,8 @@ pub trait Storage { ) -> Result, Error>; /// Returns the set of channels in offer state. fn get_offered_channels(&self) -> Result, Error>; + /// Returns the set of channels in settled closing state. + fn get_settled_closing_channels(&self) -> Result, Error>; /// Writes the [`ChainMonitor`] data to the store. fn persist_chain_monitor(&self, monitor: &ChainMonitor) -> Result<(), Error>; /// Returns the latest [`ChainMonitor`] in the store if any. diff --git a/dlc-manager/src/manager.rs b/dlc-manager/src/manager.rs index d679b61c..06a31eb1 100644 --- a/dlc-manager/src/manager.rs +++ b/dlc-manager/src/manager.rs @@ -4,7 +4,7 @@ use super::{Blockchain, Oracle, Storage, Time, Wallet}; use crate::chain_monitor::{ChainMonitor, ChannelInfo, RevokedTxType, TxType}; use crate::channel::offered_channel::OfferedChannel; use crate::channel::signed_channel::{SignedChannel, SignedChannelState, SignedChannelStateType}; -use crate::channel::{Channel, ClosedChannel, ClosedPunishedChannel}; +use crate::channel::{Channel, ClosedChannel, ClosedPunishedChannel, SettledClosingChannel}; use crate::channel_updater::{get_unix_time_now, verify_signed_channel}; use crate::channel_updater::{self, get_signed_channel_state}; use crate::contract::{ @@ -32,7 +32,7 @@ use dlc_messages::oracle_msgs::{OracleAnnouncement, OracleAttestation}; use dlc_messages::{ AcceptDlc, ChannelMessage, Message as DlcMessage, OfferDlc, OnChainMessage, SignDlc, }; -use lightning::chain::chaininterface::FeeEstimator; +use lightning::chain::chaininterface::{FeeEstimator, ConfirmationTarget}; use lightning::ln::chan_utils::{ build_commitment_secret, derive_private_key, derive_private_revocation_key, }; @@ -1231,6 +1231,117 @@ where Ok(()) } + fn try_finalize_settled_closing_channel( + &self, + signed_channel: SignedChannel, + ) -> Result<(), Error> { + let (settle_tx, &is_offer, &is_initiator) = get_signed_channel_state!( + signed_channel, + SettledClosing, + settle_transaction, + is_offer, + is_initiator + )?; + + if self + .blockchain + .get_transaction_confirmations(&settle_tx.txid())? + >= CET_NSEQUENCE + { + log::info!( + "Settle transaction {} for channel {} has enough confirmations to spend from it", + settle_tx.txid(), + serialize_hex(&signed_channel.channel_id) + ); + + let fee_rate_per_vb: u64 = { + let fee_rate = self + .fee_estimator + .get_est_sat_per_1000_weight( + ConfirmationTarget::HighPriority, + ); + + let fee_rate = fee_rate / 250; + + fee_rate.into() + }; + + let (claim_tx, settled_closing_channel) = + channel_updater::finalize_unilateral_close_settled_channel( + &self.secp, + &signed_channel, + &self.wallet.get_new_address()?, + fee_rate_per_vb, + &self.wallet, + is_offer, + is_initiator, + )?; + + let confirmations = self + .blockchain + .get_transaction_confirmations(&claim_tx.txid())?; + + if confirmations < 1 { + self.blockchain.send_transaction(&claim_tx)?; + } + + self.store + .upsert_channel(settled_closing_channel, None)?; + } + + Ok(()) + } + + fn try_confirm_claim_tx(&self, channel: &SettledClosingChannel) -> Result<(), Error> { + let claim_tx = &channel.claim_transaction; + + let confirmations = self + .blockchain + .get_transaction_confirmations(&claim_tx.txid())?; + + // TODO(lucas): No need to send it again if it is in mempool, unless we want to bump the + // fee. + if confirmations < 1 { + self.blockchain.send_transaction(claim_tx)?; + } else if confirmations >= NB_CONFIRMATIONS { + self.chain_monitor + .lock() + .unwrap() + .cleanup_channel(channel.channel_id); + + let closed_channel = if channel.is_closer { + Channel::Closed(ClosedChannel { + counter_party: channel.counter_party, + temporary_channel_id: channel.temporary_channel_id, + channel_id: channel.channel_id, + reference_id: channel.reference_id, + // TODO(lucas): We are losing the context of the settle transaction here, but it + // can always be found based on the claim transaction. We could introduce a + // dedicated `SettledClosed` state to fix this. + closing_txid: channel.claim_transaction.txid() + }) + } else { + Channel::CounterClosed(ClosedChannel { + counter_party: channel.counter_party, + temporary_channel_id: channel.temporary_channel_id, + channel_id: channel.channel_id, + reference_id: channel.reference_id, + closing_txid: channel.claim_transaction.txid() + }) + }; + + self.chain_monitor + .lock() + .unwrap() + .cleanup_channel(channel.channel_id); + + self.store + .upsert_channel(closed_channel, None)?; + } + + Ok(()) + } + fn on_offer_channel( &self, offer_channel: &OfferChannel, @@ -2057,6 +2168,26 @@ where } } + let signed_settled_closing_channels = self + .store + .get_signed_channels(Some(SignedChannelStateType::SettledClosing))?; + + for channel in signed_settled_closing_channels { + if let Err(e) = self.try_finalize_settled_closing_channel(channel) { + error!("Error trying to close settled channel: {}", e); + } + } + + let settled_closing_channels = self + .store + .get_settled_closing_channels()?; + + for channel in settled_closing_channels { + if let Err(e) = self.try_confirm_claim_tx(&channel) { + error!("Error trying to confirm claim TX of settled channel: {}", e); + } + } + if let Err(e) = self.check_for_timed_out_channels() { error!("Error checking timed out channels {}", e); } @@ -2312,19 +2443,59 @@ where true } TxType::SettleTx => { - let closed_channel = Channel::CounterClosed(ClosedChannel { - counter_party: signed_channel.counter_party, - temporary_channel_id: signed_channel.temporary_channel_id, - channel_id: signed_channel.channel_id, - reference_id: None, - closing_txid: tx.txid(), - }); - self.chain_monitor - .lock() - .unwrap() - .cleanup_channel(signed_channel.channel_id); - self.store.upsert_channel(closed_channel, None)?; - true + // TODO(tibo): should only considered closed after some confirmations. + // Ideally should save previous state, and maybe restore in + // case of reorg, though if the counter party has sent the + // tx to close the channel it is unlikely that the tx will + // not be part of a future block. + + let is_settle_offer = { + let chain_monitor = self.chain_monitor.lock().unwrap(); + chain_monitor.did_we_offer_last_channel_settlement(&signed_channel.channel_id) + }; + + let is_settle_offer = match is_settle_offer { + Some(is_settle_offer) => is_settle_offer, + None => { + log::error!("Cannot force close settled channel without knowledge of who offered settlement"); + continue; + }, + }; + + let mut state = SignedChannelState::SettledClosing { + settle_transaction: tx.clone(), + is_offer: is_settle_offer, + is_initiator: false, + }; + std::mem::swap(&mut signed_channel.state, &mut state); + + signed_channel.roll_back_state = Some(state); + + self.store + .upsert_channel(Channel::Signed(signed_channel), None)?; + + false + } + TxType::SettleTx2 { is_offer } => { + // TODO(tibo): should only considered closed after some confirmations. + // Ideally should save previous state, and maybe restore in + // case of reorg, though if the counter party has sent the + // tx to close the channel it is unlikely that the tx will + // not be part of a future block. + + let mut state = SignedChannelState::SettledClosing { + settle_transaction: tx.clone(), + is_offer, + is_initiator: false, + }; + std::mem::swap(&mut signed_channel.state, &mut state); + + signed_channel.roll_back_state = Some(state); + + self.store + .upsert_channel(Channel::Signed(signed_channel), None)?; + + false } TxType::Cet => { let contract_id = signed_channel.get_contract_id(); @@ -2470,7 +2641,7 @@ where SignedChannelState::Settled { .. } => { warn!("Force closing settled channel with id: {}", channel.channel_id.to_hex()); - self.close_settled_channel(channel, sub_channel, is_initiator) + self.initiate_unilateral_close_settled_channel(channel, sub_channel, is_initiator, reference_id) } SignedChannelState::SettledOffered { .. } | SignedChannelState::SettledReceived { .. } @@ -2486,7 +2657,7 @@ where .expect("to have a rollback state"); self.force_close_channel_internal(channel, sub_channel, is_initiator, reference_id) } - SignedChannelState::Closing { .. } => Err(Error::InvalidState( + SignedChannelState::Closing { .. } | SignedChannelState::SettledClosing { .. } => Err(Error::InvalidState( "Channel is already closing.".to_string(), )), } @@ -2540,27 +2711,43 @@ where } /// Unilaterally close a channel that has been settled. - fn close_settled_channel( + fn initiate_unilateral_close_settled_channel( &self, - signed_channel: SignedChannel, + mut signed_channel: SignedChannel, sub_channel: Option<(SubChannel, &ClosingSubChannel)>, is_initiator: bool, + reference_id: Option ) -> Result<(), Error> { - let (settle_tx, closed_channel) = crate::channel_updater::close_settled_channel_internal( + let is_settle_offer = { + let chain_monitor = self.chain_monitor.lock().unwrap(); + chain_monitor.did_we_offer_last_channel_settlement(&signed_channel.channel_id) + }.ok_or_else( + || Error::InvalidState( + "Cannot force close settled channel without knowledge of who offered settlement" + .to_string() + ) + )?; + + crate::channel_updater::initiate_unilateral_close_settled_channel( &self.secp, - &signed_channel, + &mut signed_channel, &self.wallet, sub_channel, + is_settle_offer, is_initiator, + reference_id, )?; + let settle_tx = + get_signed_channel_state!(signed_channel, SettledClosing, ref settle_transaction)?; + if self .blockchain .get_transaction_confirmations(&settle_tx.txid()) .unwrap_or(0) == 0 { - self.blockchain.send_transaction(&settle_tx)?; + self.blockchain.send_transaction(settle_tx)?; } self.chain_monitor @@ -2568,7 +2755,7 @@ where .unwrap() .cleanup_channel(signed_channel.channel_id); - self.store.upsert_channel(closed_channel, None)?; + self.store.upsert_channel(Channel::Signed(signed_channel), None)?; Ok(()) } diff --git a/dlc-sled-storage-provider/src/lib.rs b/dlc-sled-storage-provider/src/lib.rs index dc519652..a33e45f3 100644 --- a/dlc-sled-storage-provider/src/lib.rs +++ b/dlc-sled-storage-provider/src/lib.rs @@ -21,7 +21,7 @@ use dlc_manager::channel::accepted_channel::AcceptedChannel; use dlc_manager::channel::offered_channel::OfferedChannel; use dlc_manager::channel::signed_channel::{SignedChannel, SignedChannelStateType}; use dlc_manager::channel::{ - Channel, ClosedChannel, ClosedPunishedChannel, ClosingChannel, FailedAccept, FailedSign, + Channel, ClosedChannel, ClosedPunishedChannel, ClosingChannel, FailedAccept, FailedSign, SettledClosingChannel, }; use dlc_manager::contract::accepted_contract::AcceptedContract; use dlc_manager::contract::offered_contract::OfferedContract; @@ -132,7 +132,8 @@ convertible_enum!( CollaborativelyClosed, FailedAccept, FailedSign, - Cancelled,; + Cancelled, + SettledClosing,; }, Channel ); @@ -151,6 +152,7 @@ convertible_enum!( RenewOffered, RenewConfirmed, RenewFinalized, + SettledClosing, }, SignedChannelStateType ); @@ -424,6 +426,14 @@ impl Storage for SledStorageProvider { ) } + fn get_settled_closing_channels(&self) -> Result, Error> { + self.get_data_with_prefix( + &self.channel_tree()?, + &[ChannelPrefix::SettledClosing.into()], + None, + ) + } + fn persist_chain_monitor(&self, monitor: &ChainMonitor) -> Result<(), Error> { self.open_tree(&[CHAIN_MONITOR_TREE])? .insert([CHAIN_MONITOR_KEY], monitor.serialize()?) @@ -729,6 +739,7 @@ fn serialize_channel(channel: &Channel) -> Result, ::std::io::Error> { Channel::FailedAccept(f) => f.serialize(), Channel::FailedSign(f) => f.serialize(), Channel::Closing(c) => c.serialize(), + Channel::SettledClosing(c) => c.serialize(), Channel::Closed(c) | Channel::CounterClosed(c) | Channel::CollaborativelyClosed(c) => { c.serialize() } @@ -771,6 +782,9 @@ fn deserialize_channel(buff: &sled::IVec) -> Result { ChannelPrefix::Closing => { Channel::Closing(ClosingChannel::deserialize(&mut cursor).map_err(to_storage_error)?) } + ChannelPrefix::SettledClosing => { + Channel::SettledClosing(SettledClosingChannel::deserialize(&mut cursor).map_err(to_storage_error)?) + } ChannelPrefix::Closed => { Channel::Closed(ClosedChannel::deserialize(&mut cursor).map_err(to_storage_error)?) } diff --git a/dlc/src/channel/mod.rs b/dlc/src/channel/mod.rs index 4fb728d1..386a5f8c 100644 --- a/dlc/src/channel/mod.rs +++ b/dlc/src/channel/mod.rs @@ -70,6 +70,8 @@ const SETTLE_OUTPUT_WEIGHT: usize = 172; */ const PUNISH_BUFFER_INPUT_WEIGHT: usize = 758; +// TODO: I think this weight applies to the settle transaction in general, not just when we spend +// it via punish. /** * In the worst case the witness input is (+1 is added to each witness for size * parameter): @@ -300,7 +302,12 @@ pub fn create_renewal_channel_transactions( }; if fund_output.value <= extra_fee + super::DUST_LIMIT { - return Err(Error::InvalidArgument(format!("Fund output: {} smaller or equal to extra fee: {} + dust limit: {}", fund_output.value, extra_fee, super::DUST_LIMIT))); + return Err(Error::InvalidArgument(format!( + "Fund output: {} smaller or equal to extra fee: {} + dust limit: {}", + fund_output.value, + extra_fee, + super::DUST_LIMIT + ))); } let outpoint = OutPoint { @@ -395,6 +402,81 @@ pub fn sign_cet( Ok(()) } +/// Create and sign a transaction claiming a settle transaction output. +pub fn create_and_sign_claim_settle_transaction( + secp: &Secp256k1, + own_params: &RevokeParams, + counter_params: &RevokeParams, + own_sk: &SecretKey, + settle_tx: &Transaction, + dest_address: &Address, + csv_timelock: u32, + lock_time: u32, + fee_rate_per_vb: u64, + is_offer: bool, +) -> Result { + let own_descriptor = settle_descriptor(own_params, &counter_params.own_pk, csv_timelock); + + let vout = if is_offer { + 0 + } else { + 1 + }; + + let tx_in = TxIn { + previous_output: OutPoint { + txid: settle_tx.txid(), + vout, + }, + sequence: Sequence::from_height(csv_timelock as u16), + script_sig: Script::default(), + witness: Witness::default(), + }; + + let input_value = settle_tx.output[vout as usize].value; + + let dest_script_pk_len = dest_address.script_pubkey().len(); + let var_int_prefix_len = crate::util::compute_var_int_prefix_size(dest_script_pk_len); + let output_weight = N_VALUE_WEIGHT + var_int_prefix_len + dest_script_pk_len * 4; + let tx_fee = + crate::util::tx_weight_to_fee(PUNISH_SETTLE_INPUT_WEIGHT + output_weight, fee_rate_per_vb)?; + + let mut tx = Transaction { + version: super::TX_VERSION, + lock_time: PackedLockTime(lock_time), + input: vec![tx_in], + output: vec![TxOut { + value: input_value - tx_fee, + script_pubkey: dest_address.script_pubkey(), + }], + }; + + let mut sigs = HashMap::new(); + + let own_pk = PublicKey { + inner: SecpPublicKey::from_secret_key(secp, own_sk), + compressed: true, + }; + sigs.insert( + own_pk, + EcdsaSig::sighash_all(super::util::get_raw_sig_for_tx_input( + secp, + &tx, + 0, + &own_descriptor.script_code()?, + input_value, + own_sk, + )?), + ); + + let satisfier = (sigs, Sequence::from_height(csv_timelock as u16)); + + own_descriptor + .satisfy(&mut tx.input[0], satisfier)?; + + Ok(tx) +} + /// Use the given parameters to build the descriptor of the given buffer transaction and inserts /// the signatures in the transaction witness. pub fn satisfy_buffer_descriptor( diff --git a/mocks/src/memory_storage_provider.rs b/mocks/src/memory_storage_provider.rs index b186c51d..2e55d9f9 100644 --- a/mocks/src/memory_storage_provider.rs +++ b/mocks/src/memory_storage_provider.rs @@ -4,6 +4,7 @@ use dlc_manager::channel::{ offered_channel::OfferedChannel, signed_channel::{SignedChannel, SignedChannelStateType}, Channel, + SettledClosingChannel, }; use dlc_manager::contract::{ offered_contract::OfferedContract, signed_contract::SignedContract, Contract, PreClosedContract, @@ -269,6 +270,20 @@ impl Storage for MemoryStorage { Ok(res) } + fn get_settled_closing_channels(&self) -> Result, DaemonError> { + let map = self.channels.read().expect("Could not get read lock"); + + let mut res: Vec = Vec::new(); + + for (_, val) in map.iter() { + if let Channel::SettledClosing(c) = val { + res.push(c.clone()) + } + } + + Ok(res) + } + fn persist_chain_monitor(&self, _: &ChainMonitor) -> Result<(), DaemonError> { // No need to persist for mocks Ok(())