vls_proxy/recovery/
mod.rs

1/// Direct access signer in the same process
2pub mod direct;
3
4use crate::tx_util::create_spending_transaction;
5use bitcoin::absolute::LockTime;
6use bitcoin::secp256k1::{PublicKey, SecretKey};
7use bitcoin::{Address, Network, ScriptBuf, Transaction, Witness};
8use bitcoind_client::esplora_client::EsploraClient;
9use bitcoind_client::{explorer_from_url, BlockExplorerType, Explorer};
10use lightning::chain::transaction::OutPoint;
11use lightning::sign::DelayedPaymentOutputDescriptor;
12use lightning_signer::bitcoin::address::{NetworkChecked, NetworkUnchecked};
13use lightning_signer::bitcoin::consensus::encode::serialize_hex;
14use lightning_signer::bitcoin::{Sequence, TxOut, Txid};
15use lightning_signer::lightning::ln::channel_keys::RevocationKey;
16use lightning_signer::node::{Allowable, ToStringForNetwork};
17use lightning_signer::util::status::Status;
18use lightning_signer::{bitcoin, lightning};
19use log::*;
20use std::collections::BTreeMap;
21use url::Url;
22
23/// Iterator
24pub struct Iter<T: RecoverySign> {
25    signers: Vec<T>,
26}
27
28impl<T: RecoverySign> Iterator for Iter<T> {
29    type Item = T;
30
31    fn next(&mut self) -> Option<Self::Item> {
32        self.signers.pop()
33    }
34}
35
36#[derive(serde::Deserialize, Debug, Clone)]
37struct UtxoResponse {
38    txid: Txid,
39    vout: u32,
40    value: u64,
41}
42
43/// Provide enough signer functionality to force-close all channels in a node
44pub trait RecoveryKeys {
45    type Signer: RecoverySign;
46    fn iter(&self) -> Iter<Self::Signer>;
47    fn sign_onchain_tx(
48        &self,
49        tx: &Transaction,
50        segwit_flags: &[bool],
51        ipaths: &Vec<Vec<u32>>,
52        prev_outs: &Vec<TxOut>,
53        uniclosekeys: Vec<Option<(SecretKey, Vec<Vec<u8>>)>>,
54        opaths: &Vec<Vec<u32>>,
55    ) -> Result<Vec<Vec<Vec<u8>>>, Status>;
56    fn wallet_address_native(&self, index: u32) -> Result<Address, Status>;
57    fn wallet_address_taproot(&self, index: u32) -> Result<Address, Status>;
58}
59
60/// Provide enough signer functionality to force-close a channel
61pub trait RecoverySign {
62    fn sign_holder_commitment_tx_for_recovery(
63        &self,
64    ) -> Result<
65        (Transaction, Vec<Transaction>, ScriptBuf, (SecretKey, Vec<Vec<u8>>), PublicKey),
66        Status,
67    >;
68    fn funding_outpoint(&self) -> OutPoint;
69    fn counterparty_selected_contest_delay(&self) -> u16;
70    fn get_per_commitment_point(&self) -> Result<PublicKey, Status>;
71}
72
73#[tokio::main(worker_threads = 2)]
74pub async fn recover_l1<R: RecoveryKeys>(
75    network: Network,
76    block_explorer_type: BlockExplorerType,
77    block_explorer_rpc: Option<Url>,
78    destination: &str,
79    keys: R,
80    max_index: u32,
81) {
82    match block_explorer_type {
83        BlockExplorerType::Esplora => {}
84        _ => {
85            panic!("only esplora supported for l1 recovery");
86        }
87    };
88
89    let url = block_explorer_rpc.expect("must have block explorer rpc");
90    let esplora = EsploraClient::new(url).await;
91
92    let mut utxos = Vec::new();
93    for index in 0..max_index {
94        let address = keys.wallet_address_native(index).expect("address");
95        let script_pubkey = address.payload.script_pubkey();
96        utxos.append(
97            &mut get_utxos(&esplora, address)
98                .await
99                .expect("get utxos")
100                .into_iter()
101                .map(|u| (index, u, script_pubkey.clone()))
102                .collect::<Vec<_>>(),
103        );
104
105        let taproot_address = keys.wallet_address_taproot(index).expect("address");
106        let taproot_script_pubkey = taproot_address.payload.script_pubkey();
107        utxos.append(
108            &mut get_utxos(&esplora, taproot_address)
109                .await
110                .expect("get utxos")
111                .into_iter()
112                .map(|u| (index, u, taproot_script_pubkey.clone()))
113                .collect::<Vec<_>>(),
114        );
115    }
116
117    if destination == "none" {
118        info!("no destination specified, only printing txs");
119    }
120
121    let destination_address: Address<NetworkUnchecked> =
122        destination.parse().expect("destination address must be valid");
123    assert!(
124        destination_address.is_valid_for_network(network),
125        "destination address must be valid for network"
126    );
127
128    let destination_address = destination_address.assume_checked();
129    let feerate_per_kw = get_feerate(&esplora).await.expect("get feerate");
130
131    for chunk in utxos.chunks(10) {
132        let tx = match make_l1_sweep(&keys, &destination_address, chunk, feerate_per_kw) {
133            Some(value) => value,
134            None => continue,
135        };
136
137        esplora.broadcast_transaction(&tx).await.expect("broadcast tx");
138    }
139}
140
141// chunk is a list of (derivation-index, utxo)
142fn make_l1_sweep<R: RecoveryKeys>(
143    keys: &R,
144    destination_address: &Address<NetworkChecked>,
145    chunk: &[(u32, UtxoResponse, ScriptBuf)],
146    feerate_per_kw: u64,
147) -> Option<Transaction> {
148    let value = chunk.iter().map(|(_, u, _)| u.value).sum::<u64>();
149
150    let mut tx = Transaction {
151        version: 2,
152        lock_time: LockTime::ZERO,
153        input: chunk
154            .iter()
155            .map(|(_, u, _)| bitcoin::TxIn {
156                previous_output: bitcoin::OutPoint { txid: u.txid, vout: u.vout },
157                sequence: Sequence::ZERO,
158                witness: Witness::default(),
159                script_sig: ScriptBuf::new(),
160            })
161            .collect(),
162        output: vec![TxOut { value, script_pubkey: destination_address.payload.script_pubkey() }],
163    };
164    let total_fee = feerate_per_kw * tx.weight().to_wu() / 1000;
165    if total_fee > value - 1000 {
166        warn!("not enough value to pay fee {:?}", tx);
167        return None;
168    }
169    tx.output[0].value -= total_fee;
170    info!("sending tx {} - {}", tx.txid().to_string(), serialize_hex(&tx));
171
172    let ipaths = chunk.iter().map(|(i, _, _)| vec![*i]).collect::<Vec<_>>();
173    let prev_outs = chunk
174        .iter()
175        .map(|(_, u, script_pubkey)| TxOut { value: u.value, script_pubkey: script_pubkey.clone() })
176        .collect::<Vec<_>>();
177    let unicosekeys = chunk.iter().map(|_| None).collect::<Vec<_>>();
178
179    // sign transaction
180    let witnesses = keys
181        .sign_onchain_tx(&tx, &vec![], &ipaths, &prev_outs, unicosekeys, &vec![vec![]])
182        .expect("sign tx");
183
184    for (i, witness) in witnesses.into_iter().enumerate() {
185        tx.input[i].witness = Witness::from_slice(&witness);
186    }
187    Some(tx)
188}
189
190// get the utxos for an address
191async fn get_utxos(esplora: &EsploraClient, address: Address) -> Result<Vec<UtxoResponse>, ()> {
192    let utxos: Vec<UtxoResponse> =
193        esplora.get(&format!("address/{}/utxo", address)).await.map_err(|e| {
194            error!("{}", e);
195        })?;
196    Ok(utxos)
197}
198
199// get the 24-block (4 hour) feerate
200async fn get_feerate(esplora: &EsploraClient) -> Result<u64, ()> {
201    let fees: BTreeMap<String, f64> = esplora.get("fee-estimates").await.map_err(|e| {
202        error!("{}", e);
203    })?;
204    let feerate = (fees.get("24").expect("feerate") * 1000f64).ceil() as u64;
205    Ok(feerate)
206}
207
208#[tokio::main(worker_threads = 2)]
209pub async fn recover_close<R: RecoveryKeys>(
210    network: Network,
211    block_explorer_type: BlockExplorerType,
212    block_explorer_rpc: Option<Url>,
213    destination: &str,
214    keys: R,
215) {
216    let explorer_client = match block_explorer_rpc {
217        Some(url) => Some(explorer_from_url(network, block_explorer_type, url).await),
218        None => None,
219    };
220
221    let mut sweeps = Vec::new();
222
223    for signer in keys.iter() {
224        info!("# funding {:?}", signer.funding_outpoint());
225
226        let (tx, htlc_txs, revocable_script, uck, revocation_pubkey) =
227            signer.sign_holder_commitment_tx_for_recovery().expect("sign");
228        debug!("closing tx {:?}", &tx);
229        info!("closing txid {}", tx.txid());
230        if let Some(bitcoind_client) = &explorer_client {
231            let funding_confirms = bitcoind_client
232                .get_utxo_confirmations(&signer.funding_outpoint().into_bitcoin_outpoint())
233                .await
234                .expect("get_txout for funding");
235            if funding_confirms.is_some() {
236                info!(
237                    "channel is open ({} confirms), broadcasting force-close {}",
238                    funding_confirms.unwrap(),
239                    tx.txid()
240                );
241                bitcoind_client.broadcast_transaction(&tx).await.expect("failed to broadcast");
242            } else {
243                let required_confirms = signer.counterparty_selected_contest_delay();
244                info!(
245                    "channel is already closed, check outputs, waiting until {} confirms",
246                    required_confirms
247                );
248                for (idx, out) in tx.output.iter().enumerate() {
249                    let script = out.script_pubkey.clone();
250                    if script == revocable_script {
251                        info!("our revocable output {} @ {}", out.value, idx);
252                        let out_point = OutPoint { txid: tx.txid(), index: idx as u16 };
253                        let confirms = bitcoind_client
254                            .get_utxo_confirmations(&out_point.into_bitcoin_outpoint())
255                            .await
256                            .expect("get_txout for our output");
257                        if let Some(confirms) = confirms {
258                            info!("revocable output is unspent ({} confirms)", confirms);
259                            if confirms >= required_confirms as u64 {
260                                info!("revocable output is mature, broadcasting sweep");
261                                let to_self_delay = signer.counterparty_selected_contest_delay();
262                                let descriptor = DelayedPaymentOutputDescriptor {
263                                    outpoint: out_point,
264                                    per_commitment_point: signer
265                                        .get_per_commitment_point()
266                                        .expect("commitment point"),
267                                    to_self_delay,
268                                    output: tx.output[idx].clone(),
269                                    revocation_pubkey: RevocationKey(revocation_pubkey),
270                                    channel_keys_id: [0; 32], // unused
271                                    channel_value_satoshis: 0,
272                                    channel_transaction_parameters: None,
273                                };
274                                sweeps.push((descriptor, uck.clone()));
275                            } else {
276                                warn!(
277                                    "revocable output is immature ({} < {})",
278                                    confirms, required_confirms
279                                );
280                            }
281                        } else {
282                            info!("revocable output is spent, skipping");
283                        }
284                    }
285                }
286            }
287        } else {
288            info!("tx: {}", serialize_hex(&tx));
289            for htlc_tx in htlc_txs {
290                info!("HTLC tx: {}", htlc_tx.txid());
291            }
292        }
293    }
294
295    if destination == "none" {
296        info!("no address specified, not sweeping");
297        return;
298    }
299
300    let wallet_path = vec![];
301    let destination_allowable = Allowable::from_str(destination, network).expect("address");
302    info!("sweeping to {}", destination_allowable.to_string(network));
303    let output_script = destination_allowable.to_script().expect("script");
304    for (descriptor, uck) in sweeps {
305        let feerate = 1000;
306        let sweep_tx = spend_delayed_outputs(
307            &keys,
308            &[descriptor],
309            uck,
310            output_script.clone(),
311            wallet_path.clone(),
312            feerate,
313        );
314        debug!("sweep tx {:?}", &sweep_tx);
315        info!("sweep txid {}", sweep_tx.txid());
316        if let Some(bitcoind_client) = &explorer_client {
317            bitcoind_client.broadcast_transaction(&sweep_tx).await.expect("failed to broadcast");
318        }
319    }
320}
321
322fn spend_delayed_outputs<R: RecoveryKeys>(
323    keys: &R,
324    descriptors: &[DelayedPaymentOutputDescriptor],
325    unilateral_close_key: (SecretKey, Vec<Vec<u8>>),
326    output_script: ScriptBuf,
327    opath: Vec<u32>,
328    feerate_sat_per_1000_weight: u32,
329) -> Transaction {
330    let mut tx =
331        create_spending_transaction(descriptors, output_script, feerate_sat_per_1000_weight)
332            .expect("create_spending_transaction");
333    let values_sat = descriptors.iter().map(|d| d.output.clone()).collect();
334    let ipaths = descriptors.iter().map(|_| vec![]).collect();
335    let uniclosekeys = descriptors.iter().map(|_| Some(unilateral_close_key.clone())).collect();
336    let input_txs = vec![]; // only need input txs for funding tx
337    let witnesses = keys
338        .sign_onchain_tx(&tx, &input_txs, &ipaths, &values_sat, uniclosekeys, &vec![opath])
339        .expect("sign");
340    assert_eq!(witnesses.len(), tx.input.len());
341    for (idx, w) in witnesses.into_iter().enumerate() {
342        tx.input[idx].witness = Witness::from_slice(&w);
343    }
344    tx
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350    use crate::recovery::direct::DirectRecoveryKeys;
351    use lightning_signer::node::SpendType;
352    use lightning_signer::util::test_utils::key::make_test_pubkey;
353    use lightning_signer::util::test_utils::{
354        init_node, make_test_previous_tx, TEST_NODE_CONFIG, TEST_SEED,
355    };
356    use std::collections::BTreeMap;
357
358    #[ignore]
359    #[tokio::test]
360    async fn esplora_utxo_test() {
361        fern::Dispatch::new().level(LevelFilter::Info).chain(std::io::stdout()).apply().unwrap();
362        let address: Address<NetworkUnchecked> =
363            "19XBuBAa78zccvfFrNWKB6PhnA1mMRASeT".parse().unwrap();
364        let address = address.assume_checked();
365        let esplora = EsploraClient::new("https://blockstream.info/api".parse().unwrap()).await;
366
367        let fees: BTreeMap<String, f64> =
368            esplora.get("fee-estimates").await.expect("fee_estimates");
369        info!("fees: {:?}", fees);
370
371        let utxos = get_utxos(&esplora, address.clone()).await.expect("get_utxos");
372        info!("address {} has {:?}", address, utxos);
373    }
374
375    #[test]
376    fn l1_sweep_test() {
377        let node = init_node(TEST_NODE_CONFIG, TEST_SEED[1]);
378        let pubkey = bitcoin::PublicKey::new(make_test_pubkey(2));
379        let address = Address::p2wpkh(&pubkey, Network::Testnet).unwrap();
380
381        node.add_allowlist(&[address.to_string()]).expect("add_allowlist");
382
383        let values = vec![(123, 12345u64, SpendType::P2wpkh)];
384        let (input_tx, input_txid) = make_test_previous_tx(&node, &values);
385        let utxo = UtxoResponse { txid: input_txid, vout: 0, value: 12345 };
386
387        let keys = DirectRecoveryKeys { node };
388        let tx = make_l1_sweep(
389            &keys,
390            &address,
391            &[(123, utxo, input_tx.output[0].script_pubkey.clone())],
392            1000,
393        )
394        .expect("make_l1_sweep");
395        tx.verify(|txo| {
396            if txo.txid == input_txid && txo.vout == 0 {
397                Some(input_tx.output[0].clone())
398            } else {
399                None
400            }
401        })
402        .expect("verify");
403
404        // won't verify if we change the input amount
405        let utxo = UtxoResponse { txid: input_txid, vout: 0, value: 12346 };
406        let tx = make_l1_sweep(
407            &keys,
408            &address,
409            &[(123, utxo, input_tx.output[0].script_pubkey.clone())],
410            1000,
411        )
412        .expect("make_l1_sweep");
413        tx.verify(|txo| {
414            if txo.txid == input_txid && txo.vout == 0 {
415                Some(input_tx.output[0].clone())
416            } else {
417                None
418            }
419        })
420        .expect_err("verify");
421    }
422}