Skip to main content

ark_client/
unilateral_exit.rs

1use crate::coin_select::coin_select_for_onchain;
2use crate::error::Error;
3use crate::error::ErrorContext;
4use crate::swap_storage::SwapStorage;
5use crate::utils::sleep;
6use crate::utils::timeout_op;
7use crate::wallet::OnchainWallet;
8use crate::Blockchain;
9use crate::Client;
10use ark_core::build_unilateral_exit_tree_txids;
11use ark_core::script::extract_checksig_pubkeys;
12use ark_core::unilateral_exit;
13use ark_core::unilateral_exit::create_unilateral_exit_transaction;
14use ark_core::unilateral_exit::finalize_unilateral_exit_tree;
15use ark_core::unilateral_exit::UnilateralExitTree;
16use backon::ExponentialBuilder;
17use backon::Retryable;
18use bitcoin::key::Secp256k1;
19use bitcoin::psbt;
20use bitcoin::Address;
21use bitcoin::Amount;
22use bitcoin::Transaction;
23use bitcoin::TxOut;
24use bitcoin::Txid;
25use std::collections::HashSet;
26
27// TODO: We should not _need_ to connect to the Ark server to perform unilateral exit. Currently we
28// do talk to the Ark server for simplicity.
29impl<B, W, S> Client<B, W, S>
30where
31    B: Blockchain,
32    W: OnchainWallet,
33    S: SwapStorage + 'static,
34{
35    /// Build the unilateral exit transaction tree for all spendable VTXOs.
36    ///
37    /// ### Returns
38    ///
39    /// The tree as a `Vec<Vec<Transaction>>`, where each branch represents a path from a
40    /// commitment transaction output to a spendable VTXO. Every transaction is finalized, but
41    /// requires fee bumping through a P2A output.
42    pub async fn build_unilateral_exit_trees(&self) -> Result<Vec<Vec<Transaction>>, Error> {
43        let vtxo_list = self
44            .list_vtxos()
45            .await
46            .context("failed to get spendable VTXOs")?;
47
48        let mut unilateral_exit_trees = Vec::new();
49
50        // For each spendable VTXO, generate its unilateral exit tree.
51        for contract_vtxo in vtxo_list.could_exit_unilaterally() {
52            let virtual_tx_outpoint = contract_vtxo.vtxo();
53            let vtxo_chain_response = timeout_op(
54                self.inner.timeout,
55                self.network_client()
56                    .get_vtxo_chain(Some(virtual_tx_outpoint.outpoint), None),
57            )
58            .await
59            .context(format!(
60                "failed to get VTXO chain for outpoint {}",
61                virtual_tx_outpoint.outpoint
62            ))??;
63
64            let paths = build_unilateral_exit_tree_txids(
65                &vtxo_chain_response.chains,
66                virtual_tx_outpoint.outpoint.txid,
67            )?;
68
69            // We don't want to fetch transactions more than once.
70            let txs = HashSet::<Txid>::from_iter(paths.concat());
71
72            let virtual_txs_response = timeout_op(
73                self.inner.timeout,
74                self.network_client()
75                    .get_virtual_txs(txs.iter().map(|tx| tx.to_string()).collect(), None),
76            )
77            .await
78            .context("failed to get virtual TXs")??;
79
80            let paths = paths
81                .into_iter()
82                .map(|path| {
83                    path.into_iter()
84                        .map(|txid| {
85                            virtual_txs_response
86                                .txs
87                                .iter()
88                                .find(|t| t.unsigned_tx.compute_txid() == txid)
89                                .cloned()
90                                .ok_or_else(|| {
91                                    Error::ad_hoc(format!("no PSBT found for virtual TX {txid}"))
92                                })
93                        })
94                        .collect::<Result<Vec<_>, _>>()
95                })
96                .collect::<Result<Vec<_>, _>>()?;
97
98            let unilateral_exit_tree =
99                UnilateralExitTree::new(virtual_tx_outpoint.commitment_txids.clone(), paths);
100
101            unilateral_exit_trees.push(unilateral_exit_tree);
102        }
103
104        let mut branches: Vec<Vec<Transaction>> = Vec::new();
105        for unilateral_exit_tree in unilateral_exit_trees {
106            let commitment_txids = unilateral_exit_tree.commitment_txids();
107
108            let mut commitment_txs = Vec::new();
109            for commitment_txid in commitment_txids.iter() {
110                let commitment_tx = timeout_op(
111                    self.inner.timeout,
112                    self.blockchain().find_tx(commitment_txid),
113                )
114                .await??
115                .ok_or_else(|| {
116                    Error::ad_hoc(format!("could not find commitment TX {commitment_txid}"))
117                })?;
118
119                commitment_txs.push(commitment_tx);
120            }
121
122            let finalized_unilateral_exit_tree =
123                finalize_unilateral_exit_tree(&unilateral_exit_tree, commitment_txs.as_slice())?;
124            branches.extend(finalized_unilateral_exit_tree);
125        }
126
127        Ok(branches)
128    }
129
130    /// Broadcast the next unconfirmed transaction in a branch, skipping transactions that are
131    /// already on the blockchain.
132    ///
133    /// ### Returns
134    ///
135    /// `Ok(Some(txid))` if a transaction was broadcast, `Ok(None)` if all are confirmed.
136    pub async fn broadcast_next_unilateral_exit_node(
137        &self,
138        branch: &[Transaction],
139    ) -> Result<Option<Txid>, Error> {
140        let blockchain = &self.blockchain();
141
142        for parent_tx in branch {
143            let parent_txid = parent_tx.compute_txid();
144
145            let broadcast = || async {
146                let is_not_published = blockchain.find_tx(&parent_txid).await?.is_none();
147
148                if is_not_published {
149                    let child_tx = self.bump_tx(parent_tx).await?;
150                    let bump_txid = child_tx.compute_txid();
151
152                    tracing::info!(
153                        txid = %parent_txid,
154                        %bump_txid,
155                        "Broadcasting unilateral exit TX"
156                    );
157
158                    blockchain
159                        .broadcast_package(&[parent_tx, &child_tx])
160                        .await?;
161
162                    Ok(Some(parent_txid))
163                } else {
164                    tracing::debug!(
165                        %parent_txid,
166                        "Unilateral exit TX already found on the blockchain"
167                    );
168
169                    Ok(None)
170                }
171            };
172
173            let res = broadcast
174                .retry(ExponentialBuilder::default().with_max_times(5))
175                .sleep(sleep)
176                .notify(|err: &Error, dur: std::time::Duration| {
177                    tracing::warn!(
178                        "Retrying broadcasting VTXO transaction {parent_txid} after {dur:?}. Error: {err}",
179                    );
180                })
181                .await
182                .with_context(|| format!("Failed to broadcast VTXO transaction {parent_txid}"))?;
183
184            if let Some(bump_txid) = res {
185                tracing::info!(
186                    txid = %parent_txid,
187                    %bump_txid,
188                    "Broadcast VTXO transaction"
189                );
190
191                return Ok(Some(parent_txid));
192            }
193        }
194
195        // All transactions in the branch are already on-chain
196        Ok(None)
197    }
198
199    /// Spend boarding outputs and VTXOs to an _on-chain_ address.
200    ///
201    /// All these outputs are spent unilaterally.
202    ///
203    /// To be able to spend a boarding output, we must wait for the exit delay to pass.
204    ///
205    /// To be able to spend a VTXO, the VTXO itself must be published on-chain (via something like
206    /// `unilateral_off_board`), and then we must wait for the exit delay to pass.
207    pub async fn send_on_chain(
208        &self,
209        to_address: Address,
210        to_amount: Amount,
211    ) -> Result<Txid, Error> {
212        let (tx, _) = self
213            .create_send_on_chain_transaction_inner(to_address, to_amount)
214            .await?;
215
216        let txid = tx.compute_txid();
217        tracing::info!(
218            %txid,
219            "Broadcasting transaction sending Ark outputs onchain"
220        );
221
222        timeout_op(self.inner.timeout, self.blockchain().broadcast(&tx))
223            .await
224            .with_context(|| format!("failed to broadcast transaction {txid}"))??;
225
226        Ok(txid)
227    }
228
229    /// Build the on-chain send transaction without broadcasting.
230    ///
231    /// Primarily useful for testing. Exposed publicly behind the `test-utils` feature.
232    #[cfg(feature = "test-utils")]
233    pub async fn create_send_on_chain_transaction(
234        &self,
235        to_address: Address,
236        to_amount: Amount,
237    ) -> Result<(Transaction, Vec<TxOut>), Error> {
238        self.create_send_on_chain_transaction_inner(to_address, to_amount)
239            .await
240    }
241
242    pub(crate) async fn create_send_on_chain_transaction_inner(
243        &self,
244        to_address: Address,
245        to_amount: Amount,
246    ) -> Result<(Transaction, Vec<TxOut>), Error> {
247        let dust = self.server_info().await?.dust;
248        if to_amount < dust {
249            return Err(Error::ad_hoc(format!(
250                "invalid amount {to_amount}, must be greater than dust: {}",
251                dust,
252            )));
253        }
254
255        // TODO: Do not use an arbitrary fee.
256        let fee = Amount::from_sat(1_000);
257
258        let (onchain_inputs, vtxo_inputs) = coin_select_for_onchain(self, to_amount + fee).await?;
259
260        let change_address = self.inner.wallet.get_onchain_address()?;
261
262        let sign = move |input: &mut psbt::Input, msg: bitcoin::secp256k1::Message| match &input
263            .witness_script
264        {
265            None => Err(ark_core::Error::ad_hoc(
266                "Missing witness script for psbt::Input when signing unilateral exit transaction",
267            )),
268            Some(script) => {
269                let mut res = vec![];
270                let pks = extract_checksig_pubkeys(script);
271
272                for pk in pks {
273                    if let Ok(keypair) = self.keypair_by_pk(&pk) {
274                        let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
275                        let pk = keypair.x_only_public_key().0;
276                        res.push((sig, pk))
277                    }
278                }
279
280                Ok(res)
281            }
282        };
283
284        let tx = create_unilateral_exit_transaction(
285            to_address,
286            to_amount,
287            change_address,
288            &onchain_inputs,
289            &vtxo_inputs,
290            sign,
291        )
292        .map_err(Error::from)?;
293
294        let prevouts = onchain_inputs
295            .iter()
296            .map(unilateral_exit::OnChainInput::previous_output)
297            .chain(
298                vtxo_inputs
299                    .iter()
300                    .map(unilateral_exit::VtxoInput::previous_output),
301            )
302            .collect();
303
304        Ok((tx, prevouts))
305    }
306}