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