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/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/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 1755b8e3..941b209c 100644 --- a/dlc-manager/src/manager.rs +++ b/dlc-manager/src/manager.rs @@ -19,7 +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::{OutPoint, Transaction}; +use bitcoin::hashes::hex::{ToHex}; use bitcoin::{locktime, Address, LockTime}; use dlc_messages::channel::{ AcceptChannel, CollaborativeCloseOffer, OfferChannel, Reject, RenewAccept, RenewConfirm, @@ -39,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; @@ -639,7 +641,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; @@ -924,6 +926,26 @@ 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)?; + + 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; + self.store.upsert_channel(Channel::Cancelled(offered_channel), Some(Contract::Rejected(offered_contract)))?; + + 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 +2008,60 @@ 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) => { + 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)))?; + }, + Channel::Signed(mut signed_channel) => { - crate::channel_updater::on_reject(&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), contract)?; + }, + 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) } 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,