diff --git a/dlc-manager/src/channel/ser.rs b/dlc-manager/src/channel/ser.rs index 2144c0bf..3745241d 100644 --- a/dlc-manager/src/channel/ser.rs +++ b/dlc-manager/src/channel/ser.rs @@ -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), (contract_id, writeable), (is_initiator, writeable)}) ;; ); diff --git a/dlc-manager/src/channel/signed_channel.rs b/dlc-manager/src/channel/signed_channel.rs index e2637dff..e8b03c49 100644 --- a/dlc-manager/src/channel/signed_channel.rs +++ b/dlc-manager/src/channel/signed_channel.rs @@ -297,6 +297,18 @@ typed_enum!( /// Whether the local party initiated the closing of the channel. is_initiator: bool, }, + /// A [`SignedChannel`] is in `SettledClosing` state when the local party + /// has broadcast a settle transaction and is waiting to finalize the + /// closing of a the channel by claiming one of its outputs. + SettledClosing { + /// The settle transaction that was broadcast. + settle_transaction: Transaction, + /// The [`crate::ContractId`] of the contract that was used to close + /// the channel. + contract_id: ContractId, + /// 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..b492d24f 100644 --- a/dlc-manager/src/channel_updater.rs +++ b/dlc-manager/src/channel_updater.rs @@ -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 }; @@ -2712,26 +2712,14 @@ 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_initiator: bool, -) -> Result<(Transaction, Channel), Error> + reference_id: Option +) -> Result<(), Error> where S::Target: Signer, { @@ -2832,24 +2820,118 @@ where )?; } + let contract_id = signed_channel.get_contract_id().ok_or_else(|| { + Error::InvalidState( + "Expected to be in a state with an associated contract id but was not.".to_string(), + ) + })?; + + signed_channel.state = SignedChannelState::SettledClosing { + settle_transaction: settle_tx, + contract_id, + is_initiator, + }; + + 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, + confirmed_contract: &SignedContract, + destination_address: &Address, + fee_rate_per_vb: u64, + signer: &S, + is_initiator: bool, +) -> Result<(Transaction, Channel), Error> +where + S::Target: Signer, +{ + let settle_transaction = + get_signed_channel_state!(signed_channel, SettledClosing, settle_transaction)?; + + let is_offer = confirmed_contract + .accepted_contract + .offered_contract + .is_offer_party; + + + + let (offer_points, accept_points, offer_per_update_point, accept_per_update_point) = if is_offer + { + ( + &signed_channel.own_points, + &signed_channel.counter_points, + &signed_channel.own_per_update_point, + &signed_channel.counter_per_update_point, + ) + } else { + ( + &signed_channel.counter_points, + &signed_channel.own_points, + &signed_channel.counter_per_update_point, + &signed_channel.own_per_update_point, + ) + }; + + let offer_revoke_params = offer_points.get_revokable_params( + secp, + &accept_points.revocation_basepoint, + offer_per_update_point, + ); + + let accept_revoke_params = accept_points.get_revokable_params( + secp, + &offer_points.revocation_basepoint, + accept_per_update_point, + ); + + let (own_per_update_point, own_basepoint) = if is_offer { + ( + &offer_per_update_point, + &offer_points.own_basepoint, + ) + } else { + ( + &accept_per_update_point, + &accept_points.own_basepoint, + ) + }; + + 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, + &offer_revoke_params, + &accept_revoke_params, + &own_sk, + settle_transaction, + destination_address, + CET_NSEQUENCE, + 0, + fee_rate_per_vb, + is_offer, + )?; + + let closed_channel = 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: claim_tx.txid() + }; + 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() - }) + Channel::Closed(closed_channel) } 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() - }) + Channel::CounterClosed(closed_channel) }; - Ok((settle_tx, channel)) + + Ok((claim_tx, channel)) } /// Returns the current time as unix time (in seconds) diff --git a/dlc-manager/src/manager.rs b/dlc-manager/src/manager.rs index d679b61c..cf6d414b 100644 --- a/dlc-manager/src/manager.rs +++ b/dlc-manager/src/manager.rs @@ -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,72 @@ where Ok(()) } + fn try_finalize_settled_closing_channel( + &self, + signed_channel: SignedChannel, + ) -> Result<(), Error> { + let (settle_tx, contract_id, &is_initiator) = get_signed_channel_state!( + signed_channel, + SettledClosing, + settle_transaction, + contract_id, + is_initiator + )?; + + if self + .blockchain + .get_transaction_confirmations(&settle_tx.txid())? + >= CET_NSEQUENCE + { + log::info!( + "Settle transaction for contract {} has enough confirmations to spend from it", + serialize_hex(&contract_id) + ); + + let confirmed_contract = + get_contract_in_state!(self, contract_id, Confirmed, None as Option)?; + + 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 (signed_claim_tx, closed_channel) = + channel_updater::finalize_unilateral_close_settled_channel( + &self.secp, + &signed_channel, + &confirmed_contract, + &self.wallet.get_new_address()?, + fee_rate_per_vb, + &self.wallet, + is_initiator, + )?; + + let closed_contract = self.get_unilaterally_settled_contract( + &confirmed_contract.accepted_contract.get_contract_id(), + signed_claim_tx.output[0].value, + true, + )?; + + self.chain_monitor + .lock() + .unwrap() + .cleanup_channel(signed_channel.channel_id); + + self.store + .upsert_channel(closed_channel, Some(Contract::Closed(closed_contract)))?; + } + + Ok(()) + } + fn on_offer_channel( &self, offer_channel: &OfferChannel, @@ -2057,6 +2123,16 @@ where } } + let settled_closing_channels = self + .store + .get_signed_channels(Some(SignedChannelStateType::SettledClosing))?; + + for channel in settled_closing_channels { + if let Err(e) = self.try_finalize_settled_closing_channel(channel) { + error!("Error trying to close established channel: {}", e); + } + } + if let Err(e) = self.check_for_timed_out_channels() { error!("Error checking timed out channels {}", e); } @@ -2312,19 +2388,29 @@ 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 contract_id = signed_channel + .get_contract_id() + .expect("to have a contract id"); + + let mut state = SignedChannelState::SettledClosing { + settle_transaction: tx.clone(), + is_initiator: false, + contract_id, + }; + 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 +2556,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 +2572,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 +2626,32 @@ 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( + crate::channel_updater::initiate_unilateral_close_settled_channel( &self.secp, - &signed_channel, + &mut signed_channel, &self.wallet, sub_channel, 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 +2659,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(()) } @@ -2635,6 +2726,38 @@ where pnl, }) } + + fn get_unilaterally_settled_contract( + &self, + contract_id: &ContractId, + payout: u64, + is_own_payout: bool, + ) -> Result { + let contract = get_contract_in_state!(self, contract_id, Confirmed, None::)?; + let own_collateral = if contract.accepted_contract.offered_contract.is_offer_party { + contract + .accepted_contract + .offered_contract + .offer_params + .collateral + } else { + contract.accepted_contract.accept_params.collateral + }; + let own_payout = if is_own_payout { + payout + } else { + contract.accepted_contract.offered_contract.total_collateral - payout + }; + let pnl = own_payout as i64 - own_collateral as i64; + Ok(ClosedContract { + attestations: None, + signed_cet: None, + contract_id: *contract_id, + temporary_contract_id: contract.accepted_contract.offered_contract.id, + counter_party_id: contract.accepted_contract.offered_contract.counter_party, + pnl, + }) + } } #[cfg(test)] diff --git a/dlc-sled-storage-provider/src/lib.rs b/dlc-sled-storage-provider/src/lib.rs index dc519652..f0eda35e 100644 --- a/dlc-sled-storage-provider/src/lib.rs +++ b/dlc-sled-storage-provider/src/lib.rs @@ -151,6 +151,7 @@ convertible_enum!( RenewOffered, RenewConfirmed, RenewFinalized, + SettledClosing, }, SignedChannelStateType ); diff --git a/dlc/src/channel/mod.rs b/dlc/src/channel/mod.rs index 4fb728d1..5fb0230e 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,83 @@ 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, + offer_params: &RevokeParams, + accept_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_params, counter_params) = if is_offer { + (offer_params, accept_params) + } else { + (accept_params, offer_params) + }; + + let descriptor = settle_descriptor(counter_params, &own_params.own_pk, csv_timelock); + + // TODO: Do we need to flip this? + let vout = u32::from(is_offer); + + let tx_in = TxIn { + previous_output: OutPoint { + txid: settle_tx.txid(), + vout, + }, + sequence: Sequence::ZERO, + 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, + &descriptor.script_code()?, + input_value, + own_sk, + )?), + ); + + descriptor + .satisfy(&mut tx.input[0], sigs) + .map_err(|e| Error::InvalidArgument(format!("{e:#}")))?; + + 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(