From 645b9bfb95b722de06251d3077a1c1b997eaa7f6 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Tue, 30 Jan 2024 15:02:01 +0100 Subject: [PATCH 1/7] feat: Add reject dlc channel offer handling This change allows the counterparty to reject a dlc channel offer and adds a handling to gracefully transition the dlc channel to a `Cancelled` state. --- dlc-manager/src/channel/mod.rs | 6 +++ dlc-manager/src/manager.rs | 49 +++++++++++++++++--- dlc-manager/tests/channel_execution_tests.rs | 19 ++++++++ dlc-manager/tests/test_utils.rs | 1 + dlc-sled-storage-provider/src/lib.rs | 7 ++- 5 files changed, 75 insertions(+), 7 deletions(-) diff --git a/dlc-manager/src/channel/mod.rs b/dlc-manager/src/channel/mod.rs index 20abe0de..7113f1ef 100644 --- a/dlc-manager/src/channel/mod.rs +++ b/dlc-manager/src/channel/mod.rs @@ -32,6 +32,8 @@ pub enum Channel { /// A channel that failed when validating an /// [`dlc_messages::channel::SignChannel`] message. FailedSign(FailedSign), + /// A [`OfferedChannel`] that got rejected by the counterparty. + Cancelled(OfferedChannel), } impl std::fmt::Debug for Channel { @@ -42,6 +44,7 @@ impl std::fmt::Debug for Channel { Channel::Signed(_) => "signed", Channel::FailedAccept(_) => "failed accept", Channel::FailedSign(_) => "failed sign", + Channel::Cancelled(_) => "cancelled" }; f.debug_struct("Contract").field("state", &state).finish() } @@ -56,6 +59,7 @@ impl Channel { Channel::Signed(s) => s.counter_party, Channel::FailedAccept(f) => f.counter_party, Channel::FailedSign(f) => f.counter_party, + Channel::Cancelled(o) => o.counter_party, } } } @@ -98,6 +102,7 @@ impl Channel { Channel::Accepted(a) => a.temporary_channel_id, Channel::Signed(s) => s.temporary_channel_id, Channel::FailedAccept(f) => f.temporary_channel_id, + Channel::Cancelled(o) => o.temporary_channel_id, _ => unimplemented!(), } } @@ -110,6 +115,7 @@ impl Channel { Channel::Signed(s) => s.channel_id, Channel::FailedAccept(f) => f.temporary_channel_id, Channel::FailedSign(f) => f.channel_id, + Channel::Cancelled(o) => o.temporary_channel_id, } } } diff --git a/dlc-manager/src/manager.rs b/dlc-manager/src/manager.rs index 1755b8e3..848efaa1 100644 --- a/dlc-manager/src/manager.rs +++ b/dlc-manager/src/manager.rs @@ -20,6 +20,7 @@ use crate::error::Error; use crate::{ChannelId, ContractId, ContractSignerProvider}; use bitcoin::locktime::Height; use bitcoin::Transaction; +use bitcoin::hashes::hex::ToHex; use bitcoin::{locktime, Address, LockTime}; use dlc_messages::channel::{ AcceptChannel, CollaborativeCloseOffer, OfferChannel, Reject, RenewAccept, RenewConfirm, @@ -924,6 +925,17 @@ where Ok(msg) } + /// Reject a channel that was offered. Returns the [`dlc_messages::channel::Reject`] + /// message to be sent as well as the public key of the offering node. + pub fn reject_channel(&self, channel_id: &ChannelId) -> Result<(Reject, PublicKey), Error> { + let offered_channel = get_channel_in_state!(self, channel_id, Offered, None as Option)?; + let counterparty = offered_channel.counter_party; + self.store.upsert_channel(Channel::Cancelled(offered_channel), None)?; + + let msg = Reject{ channel_id: *channel_id }; + Ok((msg, counterparty)) + } + /// Accept a channel that was offered. Returns the [`dlc_messages::channel::AcceptChannel`] /// message to be sent, the updated [`crate::ChannelId`] and [`crate::ContractId`], /// as well as the public key of the offering node. @@ -1986,14 +1998,39 @@ where Ok(()) } - fn on_reject(&mut self, reject: &Reject, counter_party: &PublicKey) -> Result<(), Error> { - let mut signed_channel = - get_channel_in_state!(self, &reject.channel_id, Signed, Some(*counter_party))?; + fn on_reject(&self, reject: &Reject, counter_party: &PublicKey) -> Result<(), Error> { + let channel = self.store.get_channel(&reject.channel_id)?; + + if let Some(channel) = channel { + if channel.get_counter_party_id() != *counter_party { + return Err(Error::InvalidParameters(format!( + "Peer {:02x?} is not involved with {} {:02x?}.", + counter_party, + stringify!(Channel), + channel.get_id() + ))); + } + match channel { + Channel::Offered(offered_channel) => { + // remove rejected channel, since nothing has been confirmed on chain yet. + self.store.upsert_channel(Channel::Cancelled(offered_channel), None)?; + }, + Channel::Signed(mut signed_channel) => { + crate::channel_updater::on_reject(&mut signed_channel)?; - crate::channel_updater::on_reject(&mut signed_channel)?; + self.store + .upsert_channel(Channel::Signed(signed_channel), None)?; + }, + channel => { + return Err(Error::InvalidState( + format!("Not in a state adequate to receive a reject message. {:?}", channel), + )) + } + } + } else { + warn!("Couldn't find rejected dlc channel with id: {}", reject.channel_id.to_hex()); + } - self.store - .upsert_channel(Channel::Signed(signed_channel), None)?; Ok(()) } diff --git a/dlc-manager/tests/channel_execution_tests.rs b/dlc-manager/tests/channel_execution_tests.rs index f20a5efd..2426549b 100644 --- a/dlc-manager/tests/channel_execution_tests.rs +++ b/dlc-manager/tests/channel_execution_tests.rs @@ -101,6 +101,7 @@ enum TestPath { RenewReject, RenewRace, RenewEstablishedClose, + CancelOffer, } #[test] @@ -256,6 +257,12 @@ fn channel_renew_race_test() { channel_execution_test(get_enum_test_params(1, 1, None), TestPath::RenewRace); } +#[test] +#[ignore] +fn channel_offer_reject_test() { + channel_execution_test(get_enum_test_params(1, 1, None), TestPath::CancelOffer); +} + fn channel_execution_test(test_params: TestParams, path: TestPath) { env_logger::init(); let (alice_send, bob_receive) = channel::>(); @@ -466,6 +473,18 @@ fn channel_execution_test(test_params: TestParams, path: TestPath) { assert_channel_state!(alice_manager_send, temporary_channel_id, Offered); + if let TestPath::CancelOffer = path { + let (reject_msg, _) = alice_manager_send.lock().unwrap().reject_channel(&temporary_channel_id).expect("Error rejecting contract offer"); + assert_channel_state!(alice_manager_send, temporary_channel_id, Cancelled); + alice_send + .send(Some(Message::Reject(reject_msg))) + .unwrap(); + + sync_receive.recv().expect("Error synchronizing"); + assert_channel_state!(bob_manager_send, temporary_channel_id, Cancelled); + return; + } + let (mut accept_msg, channel_id, contract_id, _) = alice_manager_send .lock() .unwrap() diff --git a/dlc-manager/tests/test_utils.rs b/dlc-manager/tests/test_utils.rs index 652195b1..bf844731 100644 --- a/dlc-manager/tests/test_utils.rs +++ b/dlc-manager/tests/test_utils.rs @@ -168,6 +168,7 @@ macro_rules! assert_channel_state { Some(Channel::Signed(_)) => "signed", Some(Channel::FailedAccept(_)) => "failed accept", Some(Channel::FailedSign(_)) => "failed sign", + Some(Channel::Cancelled(_)) => "cancelled", None => "none", }; panic!("Unexpected channel state {}", state); diff --git a/dlc-sled-storage-provider/src/lib.rs b/dlc-sled-storage-provider/src/lib.rs index c43a5aa8..c3a2d27e 100644 --- a/dlc-sled-storage-provider/src/lib.rs +++ b/dlc-sled-storage-provider/src/lib.rs @@ -121,7 +121,8 @@ convertible_enum!( Accepted, Signed, FailedAccept, - FailedSign,; + FailedSign, + Cancelled,; }, Channel ); @@ -604,6 +605,7 @@ fn serialize_channel(channel: &Channel) -> Result, ::std::io::Error> { Channel::Signed(s) => s.serialize(), Channel::FailedAccept(f) => f.serialize(), Channel::FailedSign(f) => f.serialize(), + Channel::Cancelled(o) => o.serialize(), }; let mut serialized = serialized?; let mut res = Vec::with_capacity(serialized.len() + 1); @@ -638,6 +640,9 @@ fn deserialize_channel(buff: &sled::IVec) -> Result { ChannelPrefix::FailedSign => { Channel::FailedSign(FailedSign::deserialize(&mut cursor).map_err(to_storage_error)?) } + ChannelPrefix::Cancelled => { + Channel::Cancelled(OfferedChannel::deserialize(&mut cursor).map_err(to_storage_error)?) + } }; Ok(channel) } From ec83983e1fd5fd8698b211321a254ab4d0461efc Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Thu, 15 Feb 2024 14:49:46 +0100 Subject: [PATCH 2/7] fixup! feat: Add reject dlc channel offer handling Set offered contract state to rejected --- dlc-manager/src/manager.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dlc-manager/src/manager.rs b/dlc-manager/src/manager.rs index 848efaa1..4c97cd95 100644 --- a/dlc-manager/src/manager.rs +++ b/dlc-manager/src/manager.rs @@ -929,8 +929,9 @@ where /// message to be sent as well as the public key of the offering node. pub fn reject_channel(&self, channel_id: &ChannelId) -> Result<(Reject, PublicKey), Error> { let offered_channel = get_channel_in_state!(self, channel_id, Offered, None as Option)?; + let offered_contract = get_contract_in_state!(self, &offered_channel.offered_contract_id, Offered, None as Option)?; let counterparty = offered_channel.counter_party; - self.store.upsert_channel(Channel::Cancelled(offered_channel), None)?; + self.store.upsert_channel(Channel::Cancelled(offered_channel), Some(Contract::Rejected(offered_contract)))?; let msg = Reject{ channel_id: *channel_id }; Ok((msg, counterparty)) From dac9f3e3f2f406ce4595544dde37463610776f0a Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Thu, 15 Feb 2024 14:52:03 +0100 Subject: [PATCH 3/7] chore: Fix clippy warning --- dlc-manager/src/manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlc-manager/src/manager.rs b/dlc-manager/src/manager.rs index 4c97cd95..a9f434c3 100644 --- a/dlc-manager/src/manager.rs +++ b/dlc-manager/src/manager.rs @@ -640,7 +640,7 @@ where contract_id: &ContractId, attestations: Vec<(usize, OracleAttestation)>, ) -> Result { - let contract = get_contract_in_state!(self, &contract_id, Confirmed, None::)?; + let contract = get_contract_in_state!(self, contract_id, Confirmed, None::)?; let contract_infos = &contract.accepted_contract.offered_contract.contract_info; let adaptor_infos = &contract.accepted_contract.adaptor_infos; From 19b0e87cc39386f2e0fd041002d904e4cdd64ea0 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Thu, 15 Feb 2024 14:53:07 +0100 Subject: [PATCH 4/7] feat: Unreserve utxos after offered channel gets rejected --- bitcoin-rpc-provider/src/lib.rs | 8 ++++++++ dlc-manager/src/lib.rs | 2 ++ dlc-manager/src/manager.rs | 18 ++++++++++++++++-- mocks/src/mock_wallet.rs | 6 +++++- simple-wallet/src/lib.rs | 13 +++++++++---- 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/bitcoin-rpc-provider/src/lib.rs b/bitcoin-rpc-provider/src/lib.rs index 7a1b681d..72a9d402 100644 --- a/bitcoin-rpc-provider/src/lib.rs +++ b/bitcoin-rpc-provider/src/lib.rs @@ -382,6 +382,14 @@ impl Wallet for BitcoinCoreProvider { Ok(()) } + + fn unreserve_utxos(&self, outpoints: &[OutPoint]) -> Result<(), ManagerError> { + match self.client.lock().unwrap().unlock_unspent(outpoints).map_err(rpc_err_to_manager_err)? { + true => Ok(()), + false => Err(ManagerError::StorageError(format!("Failed to unlock utxos: {outpoints:?}"))) + } + + } } impl Blockchain for BitcoinCoreProvider { diff --git a/dlc-manager/src/lib.rs b/dlc-manager/src/lib.rs index ac2a007d..f82d7a6c 100644 --- a/dlc-manager/src/lib.rs +++ b/dlc-manager/src/lib.rs @@ -167,6 +167,8 @@ pub trait Wallet { psbt: &mut PartiallySignedTransaction, input_index: usize, ) -> Result<(), Error>; + /// Unlock reserved utxo + fn unreserve_utxos(&self, outpoints: &[OutPoint]) -> Result<(), Error>; } /// Blockchain trait provides access to the bitcoin blockchain. diff --git a/dlc-manager/src/manager.rs b/dlc-manager/src/manager.rs index a9f434c3..adf2b241 100644 --- a/dlc-manager/src/manager.rs +++ b/dlc-manager/src/manager.rs @@ -19,8 +19,8 @@ use crate::contract_updater::{accept_contract, verify_accepted_and_sign_contract use crate::error::Error; use crate::{ChannelId, ContractId, ContractSignerProvider}; use bitcoin::locktime::Height; -use bitcoin::Transaction; -use bitcoin::hashes::hex::ToHex; +use bitcoin::{OutPoint, Transaction}; +use bitcoin::hashes::hex::{ToHex}; use bitcoin::{locktime, Address, LockTime}; use dlc_messages::channel::{ AcceptChannel, CollaborativeCloseOffer, OfferChannel, Reject, RenewAccept, RenewConfirm, @@ -40,6 +40,7 @@ use std::collections::HashMap; use std::ops::Deref; use std::string::ToString; use std::sync::Arc; +use bitcoin::consensus::Decodable; /// The number of confirmations required before moving the the confirmed state. pub const NB_CONFIRMATIONS: u32 = 6; @@ -930,6 +931,19 @@ where pub fn reject_channel(&self, channel_id: &ChannelId) -> Result<(Reject, PublicKey), Error> { let offered_channel = get_channel_in_state!(self, channel_id, Offered, None as Option)?; let offered_contract = get_contract_in_state!(self, &offered_channel.offered_contract_id, Offered, None as Option)?; + + if offered_channel.is_offer_party { + let utxos = offered_contract.funding_inputs_info.iter().map(|funding_input_info| { + let txid = Transaction::consensus_decode(&mut funding_input_info.funding_input.prev_tx.as_slice()) + .expect("Transaction Decode Error") + .txid(); + let vout = funding_input_info.funding_input.prev_tx_vout; + OutPoint{txid, vout} + }).collect::>(); + + self.wallet.unreserve_utxos(&utxos)?; + } + let counterparty = offered_channel.counter_party; self.store.upsert_channel(Channel::Cancelled(offered_channel), Some(Contract::Rejected(offered_contract)))?; diff --git a/mocks/src/mock_wallet.rs b/mocks/src/mock_wallet.rs index de547c4b..b481bb40 100644 --- a/mocks/src/mock_wallet.rs +++ b/mocks/src/mock_wallet.rs @@ -2,7 +2,7 @@ use std::rc::Rc; use bitcoin::psbt::PartiallySignedTransaction; use bitcoin::secp256k1::PublicKey; -use bitcoin::{Address, PackedLockTime, Script, Transaction, TxOut}; +use bitcoin::{Address, OutPoint, PackedLockTime, Script, Transaction, TxOut}; use dlc_manager::{error::Error, Blockchain, ContractSignerProvider, SimpleSigner, Utxo, Wallet}; use secp256k1_zkp::{rand::seq::SliceRandom, SecretKey}; @@ -115,6 +115,10 @@ impl Wallet for MockWallet { fn sign_psbt_input(&self, _: &mut PartiallySignedTransaction, _: usize) -> Result<(), Error> { Ok(()) } + + fn unreserve_utxos(&self, _outpoints: &[OutPoint]) -> Result<(), Error> { + Ok(()) + } } fn get_address() -> Address { diff --git a/simple-wallet/src/lib.rs b/simple-wallet/src/lib.rs index 0b8b3c2a..b6562408 100644 --- a/simple-wallet/src/lib.rs +++ b/simple-wallet/src/lib.rs @@ -6,10 +6,7 @@ use bdk::{ FeeRate, KeychainKind, LocalUtxo, Utxo as BdkUtxo, WeightedUtxo, }; use bitcoin::psbt::PartiallySignedTransaction; -use bitcoin::{ - hashes::Hash, Address, Network, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, - Txid, Witness, -}; +use bitcoin::{hashes::Hash, Address, Network, PackedLockTime, Script, Sequence, Transaction, TxIn, TxOut, Txid, Witness, OutPoint}; use dlc_manager::{ error::Error, Blockchain, ContractSignerProvider, KeysId, SimpleSigner, Utxo, Wallet, }; @@ -297,6 +294,14 @@ where Ok(()) } + fn unreserve_utxos(&self, outputs: &[OutPoint]) -> std::result::Result<(), Error> { + for outpoint in outputs { + self.storage.unreserve_utxo(&outpoint.txid, outpoint.vout)?; + } + + Ok(()) + } + fn sign_psbt_input( &self, psbt: &mut PartiallySignedTransaction, From fd34a33a57a829a5143a2bb2ca84a3e90dca23f0 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Fri, 16 Feb 2024 14:20:37 +0100 Subject: [PATCH 5/7] fixup! feat: Add reject dlc channel offer handling Set offered contract to rejected on offering side --- dlc-manager/src/manager.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dlc-manager/src/manager.rs b/dlc-manager/src/manager.rs index adf2b241..de22e03c 100644 --- a/dlc-manager/src/manager.rs +++ b/dlc-manager/src/manager.rs @@ -2027,8 +2027,9 @@ where } match channel { Channel::Offered(offered_channel) => { + let offered_contract = get_contract_in_state!(self, &offered_channel.offered_contract_id, Offered, None as Option)?; // remove rejected channel, since nothing has been confirmed on chain yet. - self.store.upsert_channel(Channel::Cancelled(offered_channel), None)?; + self.store.upsert_channel(Channel::Cancelled(offered_channel), Some(Contract::Rejected(offered_contract)))?; }, Channel::Signed(mut signed_channel) => { crate::channel_updater::on_reject(&mut signed_channel)?; From 300cbf1dd884f0fa8dcbfb6356604699880f06e0 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Fri, 16 Feb 2024 14:25:44 +0100 Subject: [PATCH 6/7] fixup! feat: Unreserve utxos after offered channel gets rejected Only unreserve utxos on the offering side. --- dlc-manager/src/manager.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/dlc-manager/src/manager.rs b/dlc-manager/src/manager.rs index de22e03c..ab1869e2 100644 --- a/dlc-manager/src/manager.rs +++ b/dlc-manager/src/manager.rs @@ -932,18 +932,6 @@ where let offered_channel = get_channel_in_state!(self, channel_id, Offered, None as Option)?; let offered_contract = get_contract_in_state!(self, &offered_channel.offered_contract_id, Offered, None as Option)?; - if offered_channel.is_offer_party { - let utxos = offered_contract.funding_inputs_info.iter().map(|funding_input_info| { - let txid = Transaction::consensus_decode(&mut funding_input_info.funding_input.prev_tx.as_slice()) - .expect("Transaction Decode Error") - .txid(); - let vout = funding_input_info.funding_input.prev_tx_vout; - OutPoint{txid, vout} - }).collect::>(); - - self.wallet.unreserve_utxos(&utxos)?; - } - let counterparty = offered_channel.counter_party; self.store.upsert_channel(Channel::Cancelled(offered_channel), Some(Contract::Rejected(offered_contract)))?; @@ -2028,6 +2016,16 @@ where match channel { Channel::Offered(offered_channel) => { let offered_contract = get_contract_in_state!(self, &offered_channel.offered_contract_id, Offered, None as Option)?; + let utxos = offered_contract.funding_inputs_info.iter().map(|funding_input_info| { + let txid = Transaction::consensus_decode(&mut funding_input_info.funding_input.prev_tx.as_slice()) + .expect("Transaction Decode Error") + .txid(); + let vout = funding_input_info.funding_input.prev_tx_vout; + OutPoint{txid, vout} + }).collect::>(); + + self.wallet.unreserve_utxos(&utxos)?; + // remove rejected channel, since nothing has been confirmed on chain yet. self.store.upsert_channel(Channel::Cancelled(offered_channel), Some(Contract::Rejected(offered_contract)))?; }, From 6c00d11bfdebf33c6cf8ecb76c7141ffec8f9dbe Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Wed, 21 Feb 2024 11:14:59 +0100 Subject: [PATCH 7/7] fixup! feat: Add reject dlc channel offer handling --- dlc-manager/src/manager.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/dlc-manager/src/manager.rs b/dlc-manager/src/manager.rs index ab1869e2..941b209c 100644 --- a/dlc-manager/src/manager.rs +++ b/dlc-manager/src/manager.rs @@ -930,6 +930,13 @@ where /// message to be sent as well as the public key of the offering node. pub fn reject_channel(&self, channel_id: &ChannelId) -> Result<(Reject, PublicKey), Error> { let offered_channel = get_channel_in_state!(self, channel_id, Offered, None as Option)?; + + if offered_channel.is_offer_party { + return Err(Error::InvalidState( + "Cannot reject channel initiated by us.".to_string(), + )); + } + let offered_contract = get_contract_in_state!(self, &offered_channel.offered_contract_id, Offered, None as Option)?; let counterparty = offered_channel.counter_party; @@ -2030,10 +2037,20 @@ where self.store.upsert_channel(Channel::Cancelled(offered_channel), Some(Contract::Rejected(offered_contract)))?; }, Channel::Signed(mut signed_channel) => { + + let contract = match signed_channel.state { + SignedChannelState::RenewOffered { offered_contract_id, .. } => { + let offered_contract = get_contract_in_state!(self, &offered_contract_id, Offered, None::)?; + Some(Contract::Rejected(offered_contract)) + + } + _ => None + }; + crate::channel_updater::on_reject(&mut signed_channel)?; self.store - .upsert_channel(Channel::Signed(signed_channel), None)?; + .upsert_channel(Channel::Signed(signed_channel), contract)?; }, channel => { return Err(Error::InvalidState(