1use crate::error::ErrorContext as _;
2use crate::swap_storage::SwapStorage;
3use crate::utils::sleep;
4use crate::utils::timeout_op;
5use crate::wallet::OnchainWallet;
6use crate::Blockchain;
7use crate::Client;
8use crate::Error;
9use ark_core::batch;
10use ark_core::batch::aggregate_nonces;
11use ark_core::batch::complete_delegate_forfeit_txs;
12use ark_core::batch::create_and_sign_forfeit_txs;
13use ark_core::batch::create_asset_preservation_packet;
14use ark_core::batch::generate_nonce_tree;
15use ark_core::batch::sign_batch_tree_tx;
16use ark_core::batch::sign_commitment_psbt;
17use ark_core::batch::Delegate;
18use ark_core::batch::NonceKps;
19use ark_core::contract::SpendPathKind;
20use ark_core::intent;
21use ark_core::script::extract_checksig_pubkeys;
22use ark_core::server;
23use ark_core::server::BatchTreeEventType;
24use ark_core::server::PartialSigTree;
25use ark_core::server::StreamEvent;
26use ark_core::ArkAddress;
27use ark_core::ArkNote;
28use ark_core::ExplorerUtxo;
29use ark_core::TxGraph;
30use backon::ExponentialBuilder;
31use backon::Retryable;
32use bitcoin::hashes::sha256;
33use bitcoin::hashes::Hash;
34use bitcoin::hex::DisplayHex;
35use bitcoin::key::Keypair;
36use bitcoin::key::Secp256k1;
37use bitcoin::psbt;
38use bitcoin::secp256k1;
39use bitcoin::secp256k1::schnorr;
40use bitcoin::secp256k1::PublicKey;
41use bitcoin::Address;
42use bitcoin::Amount;
43use bitcoin::OutPoint;
44use bitcoin::Psbt;
45use bitcoin::TxOut;
46use bitcoin::Txid;
47use bitcoin::XOnlyPublicKey;
48use futures::StreamExt;
49use rand::CryptoRng;
50use rand::Rng;
51use std::collections::HashMap;
52use std::collections::HashSet;
53
54impl<B, W, S> Client<B, W, S>
55where
56 B: Blockchain,
57 W: OnchainWallet,
58 S: SwapStorage + 'static,
59{
60 pub async fn settle_all<R>(&self, rng: &mut R) -> Result<Option<Txid>, Error>
66 where
67 R: Rng + CryptoRng + Clone,
68 {
69 self.settle_at(crate::utils::unix_now()?, rng).await
70 }
71
72 pub(crate) async fn settle_at<R>(&self, now: i64, rng: &mut R) -> Result<Option<Txid>, Error>
73 where
74 R: Rng + CryptoRng + Clone,
75 {
76 let server_info = self.server_info().await?;
77
78 let (to_address, _) = self.get_offchain_address_with_server_info(&server_info)?;
80
81 let (boarding_inputs, vtxo_inputs, total_amount) = self
82 .fetch_commitment_transaction_inputs(&server_info, now)
83 .await?;
84
85 tracing::debug!(
86 offchain_adress = %to_address.encode(),
87 ?boarding_inputs,
88 ?vtxo_inputs,
89 "Attempting to settle outputs"
90 );
91
92 if boarding_inputs.is_empty() && vtxo_inputs.is_empty() {
93 tracing::debug!("No inputs to board with");
94 return Ok(None);
95 }
96
97 let join_next_batch = || async {
98 self.join_next_batch(
99 &mut rng.clone(),
100 &server_info,
101 boarding_inputs.clone(),
102 vtxo_inputs.clone(),
103 BatchOutputType::Board {
104 to_address,
105 to_amount: total_amount,
106 },
107 )
108 .await
109 };
110
111 let commitment_txid = join_next_batch
113 .retry(ExponentialBuilder::default().with_max_times(0))
114 .sleep(sleep)
115 .when(|err| !err.is_server_info_changed())
116 .notify(|err: &Error, dur: std::time::Duration| {
117 tracing::warn!("Retrying joining next batch after {dur:?}. Error: {err}",);
118 })
119 .await
120 .context("Failed to join batch")?;
121
122 tracing::info!(%commitment_txid, "Settlement success");
123
124 Ok(Some(commitment_txid))
125 }
126
127 pub async fn settle<R>(&self, rng: &mut R) -> Result<Option<Txid>, Error>
141 where
142 R: Rng + CryptoRng + Clone,
143 {
144 let server_info = self.server_info().await?;
145
146 let vtxo_list = self.list_vtxos_with_server_info(&server_info).await?;
147 let vtxo_outpoints: Vec<OutPoint> = vtxo_list
148 .recoverable()
149 .map(|entry| entry.vtxo().outpoint)
150 .collect();
151
152 let (boarding_inputs, _, _) = self
153 .fetch_commitment_transaction_inputs(&server_info, crate::utils::unix_now()?)
154 .await?;
155 let boarding_outpoints: Vec<OutPoint> =
156 boarding_inputs.iter().map(|i| i.outpoint()).collect();
157
158 if vtxo_outpoints.is_empty() && boarding_outpoints.is_empty() {
159 tracing::debug!("No expired/recoverable VTXOs or boarding outputs to settle");
160 return Ok(None);
161 }
162
163 tracing::debug!(
164 num_vtxos = vtxo_outpoints.len(),
165 num_boarding = boarding_outpoints.len(),
166 "Attempting to settle expired/recoverable VTXOs and boarding outputs"
167 );
168
169 self.settle_vtxos_with_server_info(rng, &server_info, &vtxo_outpoints, &boarding_outpoints)
170 .await
171 }
172
173 pub async fn settle_with_notes<R>(
179 &self,
180 rng: &mut R,
181 notes: Vec<ArkNote>,
182 ) -> Result<Option<Txid>, Error>
183 where
184 R: Rng + CryptoRng + Clone,
185 {
186 let server_info = self.server_info().await?;
187
188 let (to_address, _) = self.get_offchain_address_with_server_info(&server_info)?;
189
190 let (boarding_inputs, vtxo_inputs, mut total_amount) = self
191 .fetch_commitment_transaction_inputs(&server_info, crate::utils::unix_now()?)
192 .await?;
193
194 let note_inputs: Vec<intent::Input> = notes
196 .iter()
197 .map(|note| {
198 total_amount += note.value();
199 note.to_intent_input()
200 })
201 .collect::<Result<Vec<_>, _>>()?;
202
203 let all_vtxo_inputs: Vec<intent::Input> =
205 vtxo_inputs.into_iter().chain(note_inputs).collect();
206
207 tracing::debug!(
208 offchain_address = %to_address.encode(),
209 ?boarding_inputs,
210 num_vtxo_inputs = all_vtxo_inputs.len(),
211 num_notes = notes.len(),
212 %total_amount,
213 "Attempting to settle outputs with notes"
214 );
215
216 if boarding_inputs.is_empty() && all_vtxo_inputs.is_empty() {
217 tracing::debug!("No inputs to settle");
218 return Ok(None);
219 }
220
221 let join_next_batch = || async {
222 self.join_next_batch(
223 &mut rng.clone(),
224 &server_info,
225 boarding_inputs.clone(),
226 all_vtxo_inputs.clone(),
227 BatchOutputType::Board {
228 to_address,
229 to_amount: total_amount,
230 },
231 )
232 .await
233 };
234
235 let commitment_txid = join_next_batch
236 .retry(ExponentialBuilder::default().with_max_times(0))
237 .sleep(sleep)
238 .when(|err| !err.is_server_info_changed())
239 .notify(|err: &Error, dur: std::time::Duration| {
240 tracing::warn!("Retrying joining next batch after {dur:?}. Error: {err}");
241 })
242 .await
243 .context("Failed to join batch")?;
244
245 tracing::info!(%commitment_txid, num_notes = notes.len(), "Settlement with notes success");
246
247 Ok(Some(commitment_txid))
248 }
249
250 pub async fn settle_vtxos<R>(
256 &self,
257 rng: &mut R,
258 vtxo_outpoints: &[OutPoint],
259 boarding_outpoints: &[OutPoint],
260 ) -> Result<Option<Txid>, Error>
261 where
262 R: Rng + CryptoRng + Clone,
263 {
264 let server_info = self.server_info().await?;
265 self.settle_vtxos_with_server_info(rng, &server_info, vtxo_outpoints, boarding_outpoints)
266 .await
267 }
268
269 pub(crate) async fn settle_vtxos_with_server_info<R>(
270 &self,
271 rng: &mut R,
272 server_info: &server::Info,
273 vtxo_outpoints: &[OutPoint],
274 boarding_outpoints: &[OutPoint],
275 ) -> Result<Option<Txid>, Error>
276 where
277 R: Rng + CryptoRng + Clone,
278 {
279 let (to_address, _) = self.get_offchain_address_with_server_info(server_info)?;
281
282 let (all_boarding_inputs, all_vtxo_inputs, _) = self
283 .fetch_commitment_transaction_inputs(server_info, crate::utils::unix_now()?)
284 .await?;
285
286 let boarding_inputs: Vec<_> = all_boarding_inputs
288 .into_iter()
289 .filter(|input| boarding_outpoints.contains(&input.outpoint()))
290 .collect();
291
292 let vtxo_inputs: Vec<_> = all_vtxo_inputs
294 .into_iter()
295 .filter(|input| vtxo_outpoints.contains(&input.outpoint()))
296 .collect();
297
298 let total_amount = boarding_inputs
300 .iter()
301 .map(|i| i.amount())
302 .chain(vtxo_inputs.iter().map(|i| i.amount()))
303 .fold(Amount::ZERO, |acc, a| acc + a);
304
305 tracing::debug!(
306 offchain_address = %to_address.encode(),
307 ?boarding_inputs,
308 ?vtxo_inputs,
309 %total_amount,
310 "Attempting to settle specific outputs"
311 );
312
313 if boarding_inputs.is_empty() && vtxo_inputs.is_empty() {
314 tracing::debug!("No matching inputs to settle");
315 return Ok(None);
316 }
317
318 let join_next_batch = || async {
319 self.join_next_batch(
320 &mut rng.clone(),
321 server_info,
322 boarding_inputs.clone(),
323 vtxo_inputs.clone(),
324 BatchOutputType::Board {
325 to_address,
326 to_amount: total_amount,
327 },
328 )
329 .await
330 };
331
332 let commitment_txid = join_next_batch
334 .retry(ExponentialBuilder::default().with_max_times(0))
335 .sleep(sleep)
336 .when(|err| !err.is_server_info_changed())
337 .notify(|err: &Error, dur: std::time::Duration| {
338 tracing::warn!("Retrying joining next batch after {dur:?}. Error: {err}",);
339 })
340 .await
341 .context("Failed to join batch")?;
342
343 tracing::info!(%commitment_txid, "Settlement of specific VTXOs success");
344
345 Ok(Some(commitment_txid))
346 }
347
348 pub async fn collaborative_redeem<R>(
351 &self,
352 rng: &mut R,
353 to_address: Address,
354 to_amount: Amount,
355 ) -> Result<Txid, Error>
356 where
357 R: Rng + CryptoRng + Clone,
358 {
359 let server_info = self.server_info().await?;
360
361 let (change_address, _) = self.get_offchain_address_with_server_info(&server_info)?;
362
363 let (boarding_inputs, vtxo_inputs, total_amount) = self
364 .fetch_commitment_transaction_inputs(&server_info, crate::utils::unix_now()?)
365 .await?;
366
367 let onchain_fee = self.eval_onchain_output_fee(ark_fees::Output {
368 amount: to_amount.to_sat(),
369 script: to_address.script_pubkey().to_string(),
370 })?;
371
372 let change_amount = total_amount
374 .checked_sub(to_amount)
375 .and_then(|a| a.checked_sub(onchain_fee))
376 .ok_or_else(|| {
377 Error::coin_select(format!(
378 "insufficient balance: {total_amount} < {to_amount} (send) + {onchain_fee} (fee)"
379 ))
380 })?;
381
382 tracing::info!(
383 %to_address,
384 send_amount = %to_amount,
385 fee = %onchain_fee,
386 change_address = %change_address.encode(),
387 %change_amount,
388 ?boarding_inputs,
389 "Attempting to collaboratively redeem outputs"
390 );
391
392 let join_next_batch = || async {
393 self.join_next_batch(
394 &mut rng.clone(),
395 &server_info,
396 boarding_inputs.clone(),
397 vtxo_inputs.clone(),
398 BatchOutputType::OffBoard {
399 to_address: to_address.clone(),
400 to_amount,
401 change_address,
402 change_amount,
403 },
404 )
405 .await
406 };
407
408 let commitment_txid = join_next_batch
410 .retry(ExponentialBuilder::default().with_max_times(3))
411 .sleep(sleep)
412 .when(|err| !err.is_server_info_changed())
413 .notify(|err: &Error, dur: std::time::Duration| {
414 tracing::warn!("Retrying joining next batch after {dur:?}. Error: {err}");
415 })
416 .await
417 .context("Failed to join batch")?;
418
419 tracing::info!(%commitment_txid, "Collaborative redeem success");
420
421 Ok(commitment_txid)
422 }
423
424 pub async fn collaborative_redeem_vtxo_selection<R>(
427 &self,
428 rng: &mut R,
429 input_vtxos: impl Iterator<Item = OutPoint> + Clone,
430 to_address: Address,
431 to_amount: Amount,
432 ) -> Result<Txid, Error>
433 where
434 R: Rng + CryptoRng + Clone,
435 {
436 let server_info = self.server_info().await?;
437
438 let (change_address, _) = self.get_offchain_address_with_server_info(&server_info)?;
439
440 let vtxo_inputs = self
441 .selected_batch_settleable_vtxo_inputs(&server_info, input_vtxos)
442 .await?;
443
444 if vtxo_inputs.is_empty() {
445 return Err(Error::ad_hoc("no matching VTXO outpoints found"));
446 }
447
448 let total_input_amount = vtxo_inputs
450 .iter()
451 .fold(Amount::ZERO, |acc, vtxo| acc + vtxo.amount());
452
453 let onchain_fee = self.eval_onchain_output_fee(ark_fees::Output {
454 amount: to_amount.to_sat(),
455 script: to_address.script_pubkey().to_string(),
456 })?;
457
458 let change_amount = total_input_amount
460 .checked_sub(to_amount)
461 .and_then(|a| a.checked_sub(onchain_fee))
462 .ok_or_else(|| {
463 Error::coin_select(format!(
464 "insufficient VTXO amount: {total_input_amount} < {to_amount} (send) + {onchain_fee} (fee)"
465 ))
466 })?;
467
468 tracing::info!(
469 %to_address,
470 send_amount = %to_amount,
471 fee = %onchain_fee,
472 change_address = %change_address.encode(),
473 %change_amount,
474 "Attempting to collaboratively redeem outputs"
475 );
476
477 let join_next_batch = || async {
478 self.join_next_batch(
479 &mut rng.clone(),
480 &server_info,
481 Vec::new(),
482 vtxo_inputs.clone(),
483 BatchOutputType::OffBoard {
484 to_address: to_address.clone(),
485 to_amount,
486 change_address,
487 change_amount,
488 },
489 )
490 .await
491 };
492
493 let commitment_txid = join_next_batch
495 .retry(ExponentialBuilder::default().with_max_times(3))
496 .sleep(sleep)
497 .when(|err| !err.is_server_info_changed())
498 .notify(|err: &Error, dur: std::time::Duration| {
499 tracing::warn!("Retrying joining next batch after {dur:?}. Error: {err}");
500 })
501 .await
502 .context("Failed to join batch")?;
503
504 tracing::info!(%commitment_txid, "Collaborative redeem success");
505
506 Ok(commitment_txid)
507 }
508
509 pub(crate) async fn selected_batch_settleable_vtxo_inputs(
510 &self,
511 server_info: &server::Info,
512 input_vtxos: impl IntoIterator<Item = OutPoint>,
513 ) -> Result<Vec<intent::Input>, Error> {
514 let requested: HashSet<OutPoint> = input_vtxos.into_iter().collect();
515
516 let vtxo_list = self
517 .list_vtxos_with_server_info(server_info)
518 .await
519 .context("failed to get VTXO list")?;
520 let now = crate::utils::unix_now()?;
521
522 let matching_unspent = vtxo_list
523 .all_unspent()
524 .filter(|entry| requested.contains(&entry.vtxo().outpoint))
525 .collect::<Vec<_>>();
526
527 let settleable = vtxo_list
528 .batch_settleable_at(server_info, now)
529 .filter(|entry| requested.contains(&entry.vtxo().outpoint))
530 .collect::<Vec<_>>();
531 let settleable_outpoints = settleable
532 .iter()
533 .map(|entry| entry.vtxo().outpoint)
534 .collect::<HashSet<_>>();
535
536 let blocked = matching_unspent
537 .iter()
538 .filter(|entry| !settleable_outpoints.contains(&entry.vtxo().outpoint))
539 .map(|entry| entry.vtxo().outpoint.to_string())
540 .collect::<Vec<_>>();
541 if !blocked.is_empty() {
542 return Err(Error::ad_hoc(format!(
543 "selected VTXO outpoints are not batch-settleable because their signer cutoff has passed: {}",
544 blocked.join(", ")
545 )));
546 }
547
548 settleable
549 .into_iter()
550 .map(|entry| {
551 let spend_selection = entry.spend_selection(SpendPathKind::Forfeit)?;
552
553 Ok(intent::Input::new_with_spend_selection(
554 entry.vtxo().outpoint,
555 entry.exit_delay()?,
556 TxOut {
557 value: entry.vtxo().amount,
558 script_pubkey: entry.script_pubkey(),
559 },
560 entry.tapscripts(),
561 spend_selection,
562 false,
563 entry.vtxo().is_swept,
564 entry.vtxo().assets.clone(),
565 ))
566 })
567 .collect::<Result<Vec<_>, Error>>()
568 }
569
570 pub async fn generate_delegate(
584 &self,
585 delegate_cosigner_pk: PublicKey,
586 ) -> Result<Delegate, Error> {
587 let server_info = self.server_info().await?;
588
589 let (to_address, _) = self.get_offchain_address_with_server_info(&server_info)?;
591
592 let (_, vtxo_inputs, _) = self
594 .fetch_commitment_transaction_inputs(&server_info, crate::utils::unix_now()?)
595 .await?;
596
597 let total_amount = vtxo_inputs
598 .iter()
599 .fold(Amount::ZERO, |acc, v| acc + v.amount());
600
601 if vtxo_inputs.is_empty() {
602 return Err(Error::ad_hoc("no inputs to settle via delegate"));
603 }
604
605 let mut outputs = vec![intent::Output::Offchain(TxOut {
606 value: total_amount,
607 script_pubkey: to_address.to_p2tr_script_pubkey(),
608 })];
609
610 if let Some(packet) = create_asset_preservation_packet(&vtxo_inputs, &outputs)? {
611 outputs.push(intent::Output::AssetPacket(packet.to_txout()));
612 }
613
614 let delegate = batch::prepare_delegate_psbts(
615 vtxo_inputs,
616 outputs,
617 delegate_cosigner_pk,
618 &server_info.forfeit_address,
619 server_info.dust,
620 )?;
621
622 Ok(delegate)
623 }
624
625 pub fn sign_delegate_psbts(
627 &self,
628 intent_psbt: &mut Psbt,
629 forfeit_psbts: &mut [Psbt],
630 ) -> Result<(), Error> {
631 let sign_fn =
632 |input: &mut psbt::Input,
633 msg: secp256k1::Message|
634 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
635 match &input.witness_script {
636 None => Err(ark_core::Error::ad_hoc(
637 "Missing witness script for psbt::Input",
638 )),
639 Some(script) => {
640 let mut res = vec![];
641 let pks = extract_checksig_pubkeys(script);
642 for pk in pks {
643 if let Ok(keypair) = self.keypair_by_pk(&pk) {
644 let sig = Secp256k1::new().sign_schnorr_no_aux_rand(&msg, &keypair);
645 let pk = keypair.x_only_public_key().0;
646 res.push((sig, pk));
647 }
648 }
649 Ok(res)
650 }
651 }
652 };
653
654 batch::sign_delegate_psbts(sign_fn, intent_psbt, forfeit_psbts)?;
655
656 Ok(())
657 }
658
659 pub async fn settle_delegate<R>(
674 &self,
675 rng: &mut R,
676 delegate: Delegate,
677 own_cosigner_kp: Keypair,
678 ) -> Result<Txid, Error>
679 where
680 R: Rng + CryptoRng,
681 {
682 if own_cosigner_kp.public_key() != delegate.delegate_cosigner_pk {
684 return Err(Error::ad_hoc(
685 "provided cosigner keypair does not match delegate_cosigner_pk",
686 ));
687 }
688
689 let server_info = self.server_info().await?;
690
691 let intent_id = timeout_op(
693 self.inner.timeout,
694 self.network_client()
695 .register_intent(delegate.intent.clone()),
696 )
697 .await
698 .context("failed to register delegated intent")??;
699
700 tracing::debug!(intent_id, "Registered delegated intent");
701
702 let network_client = self.network_client();
703
704 #[derive(Debug, PartialEq, Eq)]
705 enum Step {
706 Start,
707 BatchStarted,
708 BatchSigningStarted,
709 Finalized,
710 }
711
712 impl Step {
713 fn next(&self) -> Step {
714 match self {
715 Step::Start => Step::BatchStarted,
716 Step::BatchStarted => Step::BatchSigningStarted,
717 Step::BatchSigningStarted => Step::Finalized,
718 Step::Finalized => Step::Finalized,
719 }
720 }
721 }
722
723 let mut step = Step::Start;
724
725 let own_cosigner_kps = [own_cosigner_kp];
726 let own_cosigner_pks = own_cosigner_kps
727 .iter()
728 .map(|k| k.public_key())
729 .collect::<Vec<_>>();
730
731 let mut batch_id: Option<String> = None;
732
733 let vtxo_input_outpoints = delegate
734 .forfeit_psbts
735 .iter()
736 .map(|psbt| psbt.unsigned_tx.input[0].previous_output)
737 .collect::<Vec<_>>();
738
739 let topics = vtxo_input_outpoints
740 .iter()
741 .map(ToString::to_string)
742 .chain(
743 own_cosigner_pks
744 .iter()
745 .map(|pk| pk.serialize().to_lower_hex_string()),
746 )
747 .collect();
748
749 let mut stream = network_client.get_event_stream(topics).await?;
750
751 let (ark_forfeit_pk, _) = server_info.forfeit_pk.x_only_public_key();
752
753 let mut unsigned_commitment_tx = None;
754
755 let mut vtxo_batch_tree_graph_chunks = Some(Vec::new());
756 let mut vtxo_batch_tree_graph: Option<TxGraph> = None;
757
758 let mut connectors_graph_chunks = Some(Vec::new());
759 let mut batch_expiry = None;
760
761 let mut agg_nonce_pks = HashMap::new();
762
763 let mut our_nonce_trees: Option<HashMap<Keypair, NonceKps>> = None;
764
765 loop {
766 match timeout_op(self.inner.timeout, stream.next())
767 .await
768 .context("timed out waiting for batch event")?
769 {
770 Some(Ok(event)) => match event {
771 StreamEvent::BatchStarted(e) => {
772 if step != Step::Start {
773 continue;
774 }
775
776 let hash = sha256::Hash::hash(intent_id.as_bytes());
777 let hash = hash.as_byte_array().to_vec().to_lower_hex_string();
778
779 if e.intent_id_hashes.iter().any(|h| h == &hash) {
780 timeout_op(
781 self.inner.timeout,
782 self.network_client()
783 .confirm_registration(intent_id.clone()),
784 )
785 .await
786 .context("failed to confirm intent registration")??;
787
788 tracing::info!(batch_id = e.id, intent_id, "Intent ID found for batch");
789
790 batch_id = Some(e.id);
791
792 step = Step::BatchStarted;
793
794 batch_expiry = Some(e.batch_expiry);
795 } else {
796 tracing::debug!(
797 batch_id = e.id,
798 intent_id,
799 "Intent ID not found for batch"
800 );
801 }
802 }
803 StreamEvent::TreeTx(e) => {
804 if step != Step::BatchStarted && step != Step::BatchSigningStarted {
805 continue;
806 }
807
808 match e.batch_tree_event_type {
809 BatchTreeEventType::Vtxo => {
810 match &mut vtxo_batch_tree_graph_chunks {
811 Some(vtxo_batch_tree_graph_chunks) => {
812 tracing::debug!("Got new VTXO batch-tree graph chunk");
813
814 vtxo_batch_tree_graph_chunks.push(e.tx_graph_chunk)
815 }
816 None => {
817 return Err(Error::ark_server(
818 "received unexpected VTXO batch-tree graph chunk",
819 ));
820 }
821 };
822 }
823 BatchTreeEventType::Connector => {
824 match connectors_graph_chunks {
825 Some(ref mut connectors_graph_chunks) => {
826 tracing::debug!("Got new connectors graph chunk");
827
828 connectors_graph_chunks.push(e.tx_graph_chunk)
829 }
830 None => {
831 return Err(Error::ark_server(
832 "received unexpected connectors graph chunk",
833 ));
834 }
835 };
836 }
837 }
838 }
839 StreamEvent::TreeSignature(e) => {
840 if step != Step::BatchSigningStarted {
841 continue;
842 }
843
844 match e.batch_tree_event_type {
845 BatchTreeEventType::Vtxo => {
846 match vtxo_batch_tree_graph {
847 Some(ref mut vtxo_batch_tree_graph) => {
848 vtxo_batch_tree_graph.apply(|graph| {
849 if graph.root().unsigned_tx.compute_txid() != e.txid {
850 Ok(true)
851 } else {
852 graph.set_signature(e.signature);
853
854 Ok(false)
855 }
856 })?;
857 }
858 None => {
859 return Err(Error::ark_server(
860 "received batch-tree signature without transaction graph",
861 ));
862 }
863 };
864 }
865 BatchTreeEventType::Connector => {
866 return Err(Error::ark_server(
867 "received batch-tree signature for connector tree",
868 ));
869 }
870 }
871 }
872 StreamEvent::TreeSigningStarted(e) => {
873 if step != Step::BatchStarted {
874 continue;
875 }
876
877 let chunks = vtxo_batch_tree_graph_chunks.take().ok_or(Error::ark_server(
878 "received batch-tree signing started event without VTXO batch-tree graph chunks",
879 ))?;
880 vtxo_batch_tree_graph =
881 Some(TxGraph::new(chunks).map_err(Error::from).context(
882 "failed to build VTXO batch-tree graph before generating nonces",
883 )?);
884
885 tracing::info!(batch_id = e.id, "Batch signing started");
886
887 for own_cosigner_pk in own_cosigner_pks.iter() {
888 if !&e.cosigners_pubkeys.iter().any(|p| p == own_cosigner_pk) {
889 return Err(Error::ark_server(format!(
890 "own cosigner PK is not present in cosigner PKs: {own_cosigner_pk}"
891 )));
892 }
893 }
894
895 let mut our_nonce_tree_map = HashMap::new();
896 for own_cosigner_kp in own_cosigner_kps {
897 let own_cosigner_pk = own_cosigner_kp.public_key();
898 let nonce_tree = generate_nonce_tree(
899 rng,
900 vtxo_batch_tree_graph
901 .as_ref()
902 .expect("VTXO batch-tree graph"),
903 own_cosigner_pk,
904 &e.unsigned_commitment_tx,
905 )
906 .map_err(Error::from)
907 .context("failed to generate VTXO nonce tree")?;
908
909 tracing::info!(
910 cosigner_pk = %own_cosigner_pk,
911 "Submitting nonce tree for cosigner PK"
912 );
913
914 network_client
915 .submit_tree_nonces(
916 &e.id,
917 own_cosigner_pk,
918 nonce_tree.to_nonce_pks(),
919 )
920 .await
921 .map_err(Error::ark_server)
922 .context("failed to submit VTXO nonce tree")?;
923
924 our_nonce_tree_map.insert(own_cosigner_kp, nonce_tree);
925 }
926
927 unsigned_commitment_tx = Some(e.unsigned_commitment_tx);
928 our_nonce_trees = Some(our_nonce_tree_map);
929
930 step = step.next();
931 }
932 StreamEvent::TreeNonces(e) => {
933 if step != Step::BatchSigningStarted {
934 continue;
935 }
936
937 let tree_tx_nonce_pks = e.nonces;
938
939 let cosigner_pk = match tree_tx_nonce_pks.0.iter().find(|(pk, _)| {
940 own_cosigner_pks
941 .iter()
942 .any(|p| &&p.x_only_public_key().0 == pk)
943 }) {
944 Some((pk, _)) => *pk,
945 None => {
946 tracing::debug!(
947 batch_id = e.id,
948 txid = %e.txid,
949 "Received irrelevant TreeNonces event"
950 );
951
952 continue;
953 }
954 };
955
956 tracing::debug!(
957 batch_id = e.id,
958 txid = %e.txid,
959 %cosigner_pk,
960 "Received TreeNonces event"
961 );
962
963 let agg_nonce_pk = aggregate_nonces(tree_tx_nonce_pks);
964
965 agg_nonce_pks.insert(e.txid, agg_nonce_pk);
966
967 if vtxo_batch_tree_graph.is_none() {
968 let chunks = vtxo_batch_tree_graph_chunks.take().ok_or(Error::ark_server(
969 "received batch-tree nonces event without VTXO batch-tree graph chunks",
970 ))?;
971 vtxo_batch_tree_graph = Some(
972 TxGraph::new(chunks)
973 .map_err(Error::from)
974 .context("failed to build VTXO batch-tree graph before batch-tree signing")?,
975 );
976 }
977 let vtxo_batch_tree_graph_ref =
978 vtxo_batch_tree_graph.as_ref().expect("just populated");
979
980 if agg_nonce_pks.len() == vtxo_batch_tree_graph_ref.nb_of_nodes() {
981 let cosigner_kp = own_cosigner_kps
982 .iter()
983 .find(|kp| kp.public_key().x_only_public_key().0 == cosigner_pk)
984 .ok_or_else(|| {
985 Error::ad_hoc("no cosigner keypair to sign for own PK")
986 })?;
987
988 let our_nonce_trees = our_nonce_trees.as_mut().ok_or(
989 Error::ark_server("missing nonce trees during batch protocol"),
990 )?;
991
992 let our_nonce_tree =
993 our_nonce_trees
994 .get_mut(cosigner_kp)
995 .ok_or(Error::ark_server(
996 "missing nonce tree during batch protocol",
997 ))?;
998
999 let unsigned_commitment_tx = unsigned_commitment_tx
1000 .as_ref()
1001 .ok_or_else(|| Error::ad_hoc("missing commitment TX"))?;
1002
1003 let batch_expiry = batch_expiry
1004 .ok_or_else(|| Error::ad_hoc("missing batch expiry"))?;
1005
1006 let mut partial_sig_tree = PartialSigTree::default();
1007 for (txid, _) in vtxo_batch_tree_graph_ref.as_map() {
1008 let agg_nonce_pk = agg_nonce_pks.get(&txid).ok_or_else(|| {
1009 Error::ad_hoc(format!(
1010 "missing aggregated nonce PK for TX {txid}"
1011 ))
1012 })?;
1013
1014 let sigs = sign_batch_tree_tx(
1015 txid,
1016 batch_expiry,
1017 ark_forfeit_pk,
1018 cosigner_kp,
1019 *agg_nonce_pk,
1020 vtxo_batch_tree_graph_ref,
1021 unsigned_commitment_tx,
1022 our_nonce_tree,
1023 )
1024 .map_err(Error::from)
1025 .context("failed to sign VTXO batch-tree transactions")?;
1026
1027 partial_sig_tree.0.extend(sigs.0);
1028 }
1029
1030 network_client
1031 .submit_tree_signatures(
1032 &e.id,
1033 cosigner_kp.public_key(),
1034 partial_sig_tree,
1035 )
1036 .await
1037 .map_err(Error::ark_server)
1038 .context("failed to submit VTXO batch-tree signatures")?;
1039 }
1040 }
1041 StreamEvent::TreeNoncesAggregated(e) => {
1042 tracing::debug!(batch_id = e.id, "Batch combined nonces generated");
1043 }
1044 StreamEvent::BatchFinalization(e) => {
1045 if step != Step::BatchSigningStarted {
1046 continue;
1047 }
1048
1049 tracing::debug!(
1050 commitment_txid = %e.commitment_tx.unsigned_tx.compute_txid(),
1051 "Batch finalization started (delegate)"
1052 );
1053
1054 let chunks = connectors_graph_chunks.take().ok_or(Error::ark_server(
1055 "received batch finalization event without connectors",
1056 ))?;
1057
1058 if chunks.is_empty() {
1059 tracing::debug!(batch_id = e.id, "No delegated forfeit transactions");
1060 } else {
1061 let connectors_graph = TxGraph::new(chunks)
1062 .map_err(Error::from)
1063 .context(
1064 "failed to build connectors graph before completing forfeit TXs",
1065 )?;
1066
1067 tracing::debug!(
1068 batch_id = e.id,
1069 "Completing delegated forfeit transactions"
1070 );
1071
1072 let signed_forfeit_psbts = complete_delegate_forfeit_txs(
1073 &delegate.forfeit_psbts,
1074 &connectors_graph.leaves(),
1075 )?;
1076
1077 network_client
1078 .submit_signed_forfeit_txs(signed_forfeit_psbts, None)
1079 .await?;
1080 }
1081
1082 step = step.next();
1083 }
1084 StreamEvent::BatchFinalized(e) => {
1085 if step != Step::Finalized {
1086 continue;
1087 }
1088
1089 let commitment_txid = e.commitment_txid;
1090
1091 tracing::info!(batch_id = e.id, %commitment_txid, "Delegated batch finalized");
1092
1093 return Ok(commitment_txid);
1094 }
1095 StreamEvent::BatchFailed(ref e) => {
1096 if Some(&e.id) == batch_id.as_ref() {
1097 return Err(Error::ark_server(format!(
1098 "batch failed {}: {}",
1099 e.id, e.reason
1100 )));
1101 }
1102
1103 tracing::debug!("Unrelated batch failed: {e:?}");
1104 }
1105 StreamEvent::Heartbeat => {}
1106 StreamEvent::StreamStarted(_) => {}
1107 },
1108 Some(Err(e)) => {
1109 tracing::error!("Got error from event stream");
1110
1111 return Err(Error::ark_server(e));
1112 }
1113 None => {
1114 return Err(Error::ark_server("dropped batch event stream"));
1115 }
1116 }
1117 }
1118 }
1119
1120 pub(crate) async fn fetch_commitment_transaction_inputs(
1123 &self,
1124 server_info: &server::Info,
1125 now: i64,
1126 ) -> Result<(Vec<batch::OnChainInput>, Vec<intent::Input>, Amount), Error> {
1127 let now = u64::try_from(now).map_err(|_| Error::ad_hoc("negative timestamp"))?;
1128
1129 let boarding_outputs = self.boarding_outputs()?;
1131
1132 let mut boarding_inputs: Vec<batch::OnChainInput> = Vec::new();
1133 let mut total_amount = Amount::ZERO;
1134
1135 let mut seen_outpoints = HashSet::new();
1137
1138 for boarding_output in boarding_outputs {
1140 let outpoints = timeout_op(
1141 self.inner.timeout,
1142 self.blockchain().find_outpoints(boarding_output.address()),
1143 )
1144 .await
1145 .context("failed to find outpoints")??;
1146
1147 for o in outpoints.iter() {
1148 if let ExplorerUtxo {
1149 outpoint,
1150 amount,
1151 confirmation_blocktime: Some(confirmation_blocktime),
1152 confirmations,
1153 is_spent: false,
1154 } = o
1155 {
1156 if seen_outpoints.contains(outpoint) {
1158 continue;
1159 }
1160
1161 if server_info
1165 .signer_requires_recovery_at(boarding_output.server_pk(), now as i64)
1166 {
1167 continue;
1168 }
1169
1170 if !boarding_output.can_be_claimed_unilaterally_by_owner(
1172 std::time::Duration::from_secs(now),
1173 std::time::Duration::from_secs(*confirmation_blocktime),
1174 *confirmations,
1175 ) {
1176 seen_outpoints.insert(*outpoint);
1178
1179 let script_pubkey = boarding_output.script_pubkey();
1180 let tapscripts = boarding_output.tapscripts();
1181 let spend_selection =
1182 boarding_output.spend_selection(SpendPathKind::Forfeit)?;
1183
1184 boarding_inputs.push(batch::OnChainInput::new_with_spend_selection(
1185 boarding_output.exit_delay(),
1186 script_pubkey,
1187 tapscripts,
1188 spend_selection,
1189 boarding_output.owner_pk(),
1190 *amount,
1191 *outpoint,
1192 ));
1193 total_amount += *amount;
1194 }
1195 }
1196 }
1197 }
1198
1199 let vtxo_list = self.list_vtxos_with_server_info(server_info).await?;
1200 let settleable_vtxos: Vec<_> = vtxo_list
1204 .batch_settleable_at(server_info, now as i64)
1205 .collect();
1206
1207 total_amount += settleable_vtxos
1208 .iter()
1209 .fold(Amount::ZERO, |acc, entry| acc + entry.vtxo().amount);
1210
1211 let vtxo_inputs = settleable_vtxos
1212 .into_iter()
1213 .map(|entry| {
1214 let spend_selection = entry.spend_selection(SpendPathKind::Forfeit)?;
1215
1216 Ok(intent::Input::new_with_spend_selection(
1217 entry.vtxo().outpoint,
1218 entry.exit_delay()?,
1219 TxOut {
1220 value: entry.vtxo().amount,
1221 script_pubkey: entry.script_pubkey(),
1222 },
1223 entry.tapscripts(),
1224 spend_selection,
1225 false,
1226 entry.vtxo().is_swept,
1227 entry.vtxo().assets.clone(),
1228 ))
1229 })
1230 .collect::<Result<Vec<_>, Error>>()?;
1231
1232 Ok((boarding_inputs, vtxo_inputs, total_amount))
1233 }
1234
1235 pub(crate) fn prepare_intent<R>(
1240 &self,
1241 rng: &mut R,
1242 onchain_inputs: Vec<batch::OnChainInput>,
1243 vtxo_inputs: Vec<intent::Input>,
1244 output_type: BatchOutputType,
1245 intent_kind: PrepareIntentKind,
1246 dust: Amount,
1247 ) -> Result<PreparedIntent, Error>
1248 where
1249 R: Rng + CryptoRng,
1250 {
1251 if onchain_inputs.is_empty() && vtxo_inputs.is_empty() {
1252 return Err(Error::ad_hoc("cannot prepare intent without inputs"));
1253 }
1254
1255 let cosigner_keypair = Keypair::new(self.secp(), rng);
1257
1258 let vtxo_input_outpoints = vtxo_inputs.iter().map(|i| i.outpoint()).collect::<Vec<_>>();
1259
1260 let inputs = {
1261 let boarding_inputs = onchain_inputs.clone().into_iter().map(|o| {
1262 intent::Input::new(
1263 o.outpoint(),
1264 o.sequence(),
1265 None,
1266 TxOut {
1267 value: o.amount(),
1268 script_pubkey: o.script_pubkey().clone(),
1269 },
1270 o.tapscripts().to_vec(),
1271 o.spend_info().clone(),
1272 true,
1273 false,
1274 Vec::new(),
1275 )
1276 });
1277
1278 boarding_inputs
1279 .chain(vtxo_inputs.clone())
1280 .collect::<Vec<_>>()
1281 };
1282
1283 let mut outputs = vec![];
1284
1285 match output_type {
1286 BatchOutputType::Board {
1287 to_address,
1288 to_amount,
1289 } => {
1290 if to_amount < dust {
1291 return Err(Error::ad_hoc(format!(
1292 "cannot settle into sub-dust VTXO: {to_amount} < {dust}"
1293 )));
1294 }
1295
1296 outputs.push(intent::Output::Offchain(TxOut {
1297 value: to_amount,
1298 script_pubkey: to_address.to_p2tr_script_pubkey(),
1299 }));
1300 }
1301 BatchOutputType::OffBoard {
1302 to_address,
1303 to_amount,
1304 change_amount,
1305 ..
1306 } if change_amount == Amount::ZERO => {
1307 outputs.push(intent::Output::Onchain(TxOut {
1308 value: to_amount,
1309 script_pubkey: to_address.script_pubkey(),
1310 }));
1311 }
1312 BatchOutputType::OffBoard {
1313 to_address,
1314 to_amount,
1315 change_address,
1316 change_amount,
1317 } => {
1318 if change_amount < dust {
1319 return Err(Error::ad_hoc(format!(
1320 "cannot settle with sub-dust change VTXO: {change_amount} < {dust}"
1321 )));
1322 }
1323
1324 outputs.push(intent::Output::Onchain(TxOut {
1325 value: to_amount,
1326 script_pubkey: to_address.script_pubkey(),
1327 }));
1328
1329 outputs.push(intent::Output::Offchain(TxOut {
1330 value: change_amount,
1331 script_pubkey: change_address.to_p2tr_script_pubkey(),
1332 }));
1333 }
1334 }
1335
1336 let cosigner_pk = cosigner_keypair.public_key();
1337
1338 let secp = Secp256k1::new();
1339
1340 let sign_for_vtxo_fn =
1341 |input: &mut psbt::Input,
1342 msg: secp256k1::Message|
1343 -> Result<Vec<(schnorr::Signature, XOnlyPublicKey)>, ark_core::Error> {
1344 match &input.witness_script {
1345 None => Err(ark_core::Error::ad_hoc(
1346 "Missing witness script in psbt::Input when signing intent",
1347 )),
1348 Some(script) => {
1349 let pks = extract_checksig_pubkeys(script);
1350 let mut res = vec![];
1351 for pk in pks {
1352 if let Ok(keypair) = self.keypair_by_pk(&pk) {
1353 let sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
1354 res.push((sig, keypair.public_key().into()))
1355 }
1356 }
1357 Ok(res)
1358 }
1359 }
1360 };
1361
1362 let sign_for_onchain_fn =
1363 |input: &mut psbt::Input,
1364 msg: secp256k1::Message|
1365 -> Result<(schnorr::Signature, XOnlyPublicKey), ark_core::Error> {
1366 let onchain_input = onchain_inputs
1367 .iter()
1368 .find(|o| {
1369 Some(o.script_pubkey().clone())
1370 == input.witness_utxo.clone().map(|w| w.script_pubkey)
1371 })
1372 .ok_or_else(|| {
1373 ark_core::Error::ad_hoc(
1374 "could not find signing key for onchain input: {input:?}",
1375 )
1376 })?;
1377
1378 let owner_pk = onchain_input.owner_pk();
1379 let sig = self
1380 .sign_for_pk(&owner_pk, &msg)
1381 .map_err(|e| ark_core::Error::ad_hoc(e.to_string()))?;
1382
1383 Ok((sig, owner_pk))
1384 };
1385
1386 let now = std::time::SystemTime::now()
1387 .duration_since(std::time::UNIX_EPOCH)
1388 .map_err(|e| Error::ad_hoc(e.to_string()))
1389 .context("failed to compute now timestamp")?;
1390 let now = now.as_secs();
1391 let expire_at = now + (2 * 60);
1392
1393 if let Some(packet) = create_asset_preservation_packet(&inputs, &outputs)? {
1394 outputs.push(intent::Output::AssetPacket(packet.to_txout()));
1395 }
1396
1397 let mut onchain_output_indexes = Vec::new();
1398 for (i, output) in outputs.iter().enumerate() {
1399 if matches!(output, intent::Output::Onchain(_)) {
1400 onchain_output_indexes.push(i);
1401 }
1402 }
1403
1404 let message = match intent_kind {
1405 PrepareIntentKind::EstimateFee => intent::IntentMessage::EstimateIntentFee {
1406 onchain_output_indexes,
1407 valid_at: now,
1408 expire_at,
1409 own_cosigner_pks: vec![cosigner_pk],
1410 },
1411 PrepareIntentKind::Register => intent::IntentMessage::Register {
1412 onchain_output_indexes,
1413 valid_at: now,
1414 expire_at,
1415 own_cosigner_pks: vec![cosigner_pk],
1416 },
1417 };
1418
1419 let intent = intent::make_intent(
1420 sign_for_vtxo_fn,
1421 sign_for_onchain_fn,
1422 inputs,
1423 outputs.clone(),
1424 message,
1425 )?;
1426
1427 Ok(PreparedIntent {
1428 intent,
1429 cosigner_keypair,
1430 vtxo_input_outpoints,
1431 outputs,
1432 onchain_inputs,
1433 vtxo_inputs,
1434 })
1435 }
1436
1437 pub(crate) async fn join_next_batch<R>(
1438 &self,
1439 rng: &mut R,
1440 server_info: &server::Info,
1441 onchain_inputs: Vec<batch::OnChainInput>,
1442 vtxo_inputs: Vec<intent::Input>,
1443 output_type: BatchOutputType,
1444 ) -> Result<Txid, Error>
1445 where
1446 R: Rng + CryptoRng,
1447 {
1448 let prepared = self.prepare_intent(
1449 rng,
1450 onchain_inputs,
1451 vtxo_inputs,
1452 output_type,
1453 PrepareIntentKind::Register,
1454 server_info.dust,
1455 )?;
1456
1457 let PreparedIntent {
1458 intent,
1459 cosigner_keypair,
1460 vtxo_input_outpoints,
1461 outputs,
1462 onchain_inputs,
1463 vtxo_inputs,
1464 } = prepared;
1465
1466 let onchain_input_outpoints = onchain_inputs
1467 .iter()
1468 .map(|i| i.outpoint())
1469 .collect::<Vec<_>>();
1470
1471 let own_cosigner_kps = [cosigner_keypair];
1472 let own_cosigner_pks = own_cosigner_kps
1473 .iter()
1474 .map(|k| k.public_key())
1475 .collect::<Vec<_>>();
1476
1477 let secp = Secp256k1::new();
1478
1479 let mut step = Step::Start;
1480
1481 let intent_id = timeout_op(
1482 self.inner.timeout,
1483 self.network_client().register_intent(intent),
1484 )
1485 .await
1486 .context("failed to register intent")??;
1487
1488 tracing::debug!(
1489 intent_id,
1490 ?onchain_input_outpoints,
1491 ?vtxo_input_outpoints,
1492 ?outputs,
1493 "Registered intent for batch"
1494 );
1495
1496 let network_client = self.network_client();
1497
1498 let mut batch_id: Option<String> = None;
1499
1500 let topics = vtxo_input_outpoints
1501 .iter()
1502 .map(ToString::to_string)
1503 .chain(
1504 own_cosigner_pks
1505 .iter()
1506 .map(|pk| pk.serialize().to_lower_hex_string()),
1507 )
1508 .collect();
1509
1510 let mut stream = network_client.get_event_stream(topics).await?;
1511
1512 let (ark_forfeit_pk, _) = server_info.forfeit_pk.x_only_public_key();
1513
1514 let mut unsigned_commitment_tx = None;
1515
1516 let mut vtxo_batch_tree_graph_chunks = Some(Vec::new());
1517 let mut vtxo_batch_tree_graph: Option<TxGraph> = None;
1518
1519 let mut connectors_graph_chunks = Some(Vec::new());
1520 let mut batch_expiry = None;
1521
1522 let mut agg_nonce_pks = HashMap::new();
1523
1524 let mut our_nonce_trees: Option<HashMap<Keypair, NonceKps>> = None;
1525 loop {
1526 match timeout_op(self.inner.timeout, stream.next())
1527 .await
1528 .context("timed out waiting for batch event")?
1529 {
1530 Some(Ok(event)) => match event {
1531 StreamEvent::BatchStarted(e) => {
1532 if step != Step::Start {
1533 continue;
1534 }
1535
1536 let hash = sha256::Hash::hash(intent_id.as_bytes());
1537 let hash = hash.as_byte_array().to_vec().to_lower_hex_string();
1538
1539 if e.intent_id_hashes.iter().any(|h| h == &hash) {
1540 timeout_op(
1541 self.inner.timeout,
1542 self.network_client()
1543 .confirm_registration(intent_id.clone()),
1544 )
1545 .await
1546 .context("failed to confirm intent registration")??;
1547
1548 tracing::info!(batch_id = e.id, intent_id, "Intent ID found for batch");
1549
1550 batch_id = Some(e.id);
1551
1552 step = match outputs
1555 .iter()
1556 .any(|o| matches!(o, intent::Output::Offchain(_)))
1557 {
1558 true => Step::BatchStarted,
1559 false => Step::BatchSigningStarted,
1560 };
1561
1562 batch_expiry = Some(e.batch_expiry);
1563 } else {
1564 tracing::debug!(
1565 batch_id = e.id,
1566 intent_id,
1567 "Intent ID not found for batch"
1568 );
1569 }
1570 }
1571 StreamEvent::TreeTx(e) => {
1572 if step != Step::BatchStarted && step != Step::BatchSigningStarted {
1573 continue;
1574 }
1575
1576 match e.batch_tree_event_type {
1577 BatchTreeEventType::Vtxo => {
1578 match &mut vtxo_batch_tree_graph_chunks {
1579 Some(vtxo_batch_tree_graph_chunks) => {
1580 tracing::debug!("Got new VTXO batch-tree graph chunk");
1581
1582 vtxo_batch_tree_graph_chunks.push(e.tx_graph_chunk)
1583 }
1584 None => {
1585 return Err(Error::ark_server(
1586 "received unexpected VTXO batch-tree graph chunk",
1587 ));
1588 }
1589 };
1590 }
1591 BatchTreeEventType::Connector => {
1592 match connectors_graph_chunks {
1593 Some(ref mut connectors_graph_chunks) => {
1594 tracing::debug!("Got new connectors graph chunk");
1595
1596 connectors_graph_chunks.push(e.tx_graph_chunk)
1597 }
1598 None => {
1599 return Err(Error::ark_server(
1600 "received unexpected connectors graph chunk",
1601 ));
1602 }
1603 };
1604 }
1605 }
1606 }
1607 StreamEvent::TreeSignature(e) => {
1608 if step != Step::BatchSigningStarted {
1609 continue;
1610 }
1611
1612 match e.batch_tree_event_type {
1613 BatchTreeEventType::Vtxo => {
1614 match vtxo_batch_tree_graph {
1615 Some(ref mut vtxo_batch_tree_graph) => {
1616 vtxo_batch_tree_graph.apply(|graph| {
1617 if graph.root().unsigned_tx.compute_txid() != e.txid {
1618 Ok(true)
1619 } else {
1620 graph.set_signature(e.signature);
1621
1622 Ok(false)
1623 }
1624 })?;
1625 }
1626 None => {
1627 return Err(Error::ark_server(
1628 "received batch-tree signature without transaction graph",
1629 ));
1630 }
1631 };
1632 }
1633 BatchTreeEventType::Connector => {
1634 return Err(Error::ark_server(
1635 "received batch-tree signature for connector tree",
1636 ));
1637 }
1638 }
1639 }
1640 StreamEvent::TreeSigningStarted(e) => {
1641 if step != Step::BatchStarted {
1642 continue;
1643 }
1644
1645 let chunks = vtxo_batch_tree_graph_chunks.take().ok_or(Error::ark_server(
1646 "received batch-tree signing started event without VTXO batch-tree graph chunks",
1647 ))?;
1648 vtxo_batch_tree_graph =
1649 Some(TxGraph::new(chunks).map_err(Error::from).context(
1650 "failed to build VTXO batch-tree graph before generating nonces",
1651 )?);
1652
1653 tracing::info!(batch_id = e.id, "Batch signing started");
1654
1655 for own_cosigner_pk in own_cosigner_pks.iter() {
1656 if !&e.cosigners_pubkeys.iter().any(|p| p == own_cosigner_pk) {
1657 return Err(Error::ark_server(format!(
1658 "own cosigner PK is not present in cosigner PKs: {own_cosigner_pk}"
1659 )));
1660 }
1661 }
1662
1663 let mut our_nonce_tree_map = HashMap::new();
1665 for own_cosigner_kp in own_cosigner_kps {
1666 let own_cosigner_pk = own_cosigner_kp.public_key();
1667 let nonce_tree = generate_nonce_tree(
1668 rng,
1669 vtxo_batch_tree_graph
1670 .as_ref()
1671 .expect("VTXO batch-tree graph"),
1672 own_cosigner_pk,
1673 &e.unsigned_commitment_tx,
1674 )
1675 .map_err(Error::from)
1676 .context("failed to generate VTXO nonce tree")?;
1677
1678 tracing::info!(
1679 cosigner_pk = %own_cosigner_pk,
1680 "Submitting nonce tree for cosigner PK"
1681 );
1682
1683 network_client
1684 .submit_tree_nonces(
1685 &e.id,
1686 own_cosigner_pk,
1687 nonce_tree.to_nonce_pks(),
1688 )
1689 .await
1690 .map_err(Error::ark_server)
1691 .context("failed to submit VTXO nonce tree")?;
1692
1693 our_nonce_tree_map.insert(own_cosigner_kp, nonce_tree);
1694 }
1695
1696 unsigned_commitment_tx = Some(e.unsigned_commitment_tx);
1697 our_nonce_trees = Some(our_nonce_tree_map);
1698
1699 step = step.next();
1700 }
1701 StreamEvent::TreeNonces(e) => {
1702 if step != Step::BatchSigningStarted {
1703 continue;
1704 }
1705
1706 let tree_tx_nonce_pks = e.nonces;
1707
1708 let cosigner_pk = match tree_tx_nonce_pks.0.iter().find(|(pk, _)| {
1709 own_cosigner_pks
1710 .iter()
1711 .any(|p| &&p.x_only_public_key().0 == pk)
1712 }) {
1713 Some((pk, _)) => *pk,
1714 None => {
1715 tracing::debug!(
1716 batch_id = e.id,
1717 txid = %e.txid,
1718 "Received irrelevant TreeNonces event"
1719 );
1720
1721 continue;
1722 }
1723 };
1724
1725 tracing::debug!(
1726 batch_id = e.id,
1727 txid = %e.txid,
1728 %cosigner_pk,
1729 "Received TreeNonces event"
1730 );
1731
1732 let agg_nonce_pk = aggregate_nonces(tree_tx_nonce_pks);
1733
1734 agg_nonce_pks.insert(e.txid, agg_nonce_pk);
1735
1736 if vtxo_batch_tree_graph.is_none() {
1737 let chunks = vtxo_batch_tree_graph_chunks.take().ok_or(Error::ark_server(
1738 "received batch-tree nonces event without VTXO batch-tree graph chunks",
1739 ))?;
1740 vtxo_batch_tree_graph = Some(
1741 TxGraph::new(chunks)
1742 .map_err(Error::from)
1743 .context("failed to build VTXO batch-tree graph before batch-tree signing")?,
1744 );
1745 }
1746 let vtxo_batch_tree_graph_ref =
1747 vtxo_batch_tree_graph.as_ref().expect("just populated");
1748
1749 if agg_nonce_pks.len() == vtxo_batch_tree_graph_ref.nb_of_nodes() {
1752 let cosigner_kp = own_cosigner_kps
1753 .iter()
1754 .find(|kp| kp.public_key().x_only_public_key().0 == cosigner_pk)
1755 .ok_or_else(|| {
1756 Error::ad_hoc("no cosigner keypair to sign for own PK")
1757 })?;
1758
1759 let our_nonce_trees = our_nonce_trees.as_mut().ok_or(
1760 Error::ark_server("missing nonce trees during batch protocol"),
1761 )?;
1762
1763 let our_nonce_tree =
1764 our_nonce_trees
1765 .get_mut(cosigner_kp)
1766 .ok_or(Error::ark_server(
1767 "missing nonce tree during batch protocol",
1768 ))?;
1769
1770 let unsigned_commitment_tx = unsigned_commitment_tx
1771 .as_ref()
1772 .ok_or_else(|| Error::ad_hoc("missing commitment TX"))?;
1773
1774 let batch_expiry = batch_expiry
1775 .ok_or_else(|| Error::ad_hoc("missing batch expiry"))?;
1776
1777 let mut partial_sig_tree = PartialSigTree::default();
1778 for (txid, _) in vtxo_batch_tree_graph_ref.as_map() {
1779 let agg_nonce_pk = agg_nonce_pks.get(&txid).ok_or_else(|| {
1780 Error::ad_hoc(format!(
1781 "missing aggregated nonce PK for TX {txid}"
1782 ))
1783 })?;
1784
1785 let sigs = sign_batch_tree_tx(
1786 txid,
1787 batch_expiry,
1788 ark_forfeit_pk,
1789 cosigner_kp,
1790 *agg_nonce_pk,
1791 vtxo_batch_tree_graph_ref,
1792 unsigned_commitment_tx,
1793 our_nonce_tree,
1794 )
1795 .map_err(Error::from)
1796 .context("failed to sign VTXO batch-tree transactions")?;
1797
1798 partial_sig_tree.0.extend(sigs.0);
1799 }
1800
1801 network_client
1802 .submit_tree_signatures(
1803 &e.id,
1804 cosigner_kp.public_key(),
1805 partial_sig_tree,
1806 )
1807 .await
1808 .map_err(Error::ark_server)
1809 .context("failed to submit VTXO batch-tree signatures")?;
1810 }
1811 }
1812 StreamEvent::TreeNoncesAggregated(e) => {
1813 tracing::debug!(batch_id = e.id, "Batch combined nonces generated");
1814 }
1815 StreamEvent::BatchFinalization(e) => {
1816 if step != Step::BatchSigningStarted {
1817 continue;
1818 }
1819
1820 tracing::debug!(
1821 commitment_txid = %e.commitment_tx.unsigned_tx.compute_txid(),
1822 "Batch finalization started"
1823 );
1824
1825 let signed_forfeit_psbts = if !vtxo_inputs.is_empty() {
1826 let chunks =
1827 connectors_graph_chunks.take().ok_or(Error::ark_server(
1828 "received batch finalization event without connectors",
1829 ))?;
1830
1831 if chunks.is_empty() {
1832 tracing::debug!(batch_id = e.id, "No forfeit transactions");
1833
1834 Vec::new()
1835 } else {
1836 let connectors_graph = TxGraph::new(chunks)
1837 .map_err(Error::from)
1838 .context(
1839 "failed to build connectors graph before signing forfeit TXs",
1840 )?;
1841
1842 tracing::debug!(batch_id = e.id, "Batch finalization started");
1843
1844 create_and_sign_forfeit_txs(
1845 |input: &mut psbt::Input, msg: secp256k1::Message| match &input
1846 .witness_script
1847 {
1848 None => Err(ark_core::Error::ad_hoc(
1849 "Missing witness script in psbt::Input when signing forfeit",
1850 )),
1851 Some(script) => {
1852 let pks = extract_checksig_pubkeys(script);
1853 let mut res = vec![];
1854 for pk in pks {
1855 if let Ok(keypair) =
1856 self.keypair_by_pk(&pk) {
1857 let sig =
1858 secp.sign_schnorr_no_aux_rand(&msg, &keypair);
1859 res.push((sig, keypair.public_key().into()))
1860 }
1861 }
1862 Ok(res)
1863 }
1864 },
1865 vtxo_inputs.as_slice(),
1866 &connectors_graph.leaves(),
1867 &server_info.forfeit_address,
1868 server_info.dust,
1869 )
1870 .map_err(Error::from)?
1871 }
1872 } else {
1873 Vec::new()
1874 };
1875
1876 let commitment_psbt = if onchain_inputs.is_empty() {
1877 None
1878 } else {
1879 let mut commitment_psbt = e.commitment_tx;
1880
1881 let sign_for_pk_fn = |pk: &XOnlyPublicKey,
1882 msg: &secp256k1::Message|
1883 -> Result<
1884 schnorr::Signature,
1885 ark_core::Error,
1886 > {
1887 self.sign_for_pk(pk, msg)
1888 .map_err(|e| ark_core::Error::ad_hoc(e.to_string()))
1889 };
1890
1891 sign_commitment_psbt(
1892 sign_for_pk_fn,
1893 &mut commitment_psbt,
1894 &onchain_inputs,
1895 )
1896 .map_err(Error::from)?;
1897
1898 Some(commitment_psbt)
1899 };
1900
1901 if !signed_forfeit_psbts.is_empty() || commitment_psbt.is_some() {
1902 network_client
1903 .submit_signed_forfeit_txs(signed_forfeit_psbts, commitment_psbt)
1904 .await?;
1905 }
1906
1907 step = step.next();
1908 }
1909 StreamEvent::BatchFinalized(e) => {
1910 if step != Step::Finalized {
1911 continue;
1912 }
1913
1914 let commitment_txid = e.commitment_txid;
1915
1916 tracing::info!(batch_id = e.id, %commitment_txid, "Batch finalized");
1917
1918 return Ok(commitment_txid);
1919 }
1920 StreamEvent::BatchFailed(ref e) => {
1921 if Some(&e.id) == batch_id.as_ref() {
1922 return Err(Error::ark_server(format!(
1923 "batch failed {}: {}",
1924 e.id, e.reason
1925 )));
1926 }
1927
1928 tracing::debug!("Unrelated batch failed: {e:?}");
1929 }
1930 StreamEvent::Heartbeat => {}
1931 StreamEvent::StreamStarted(_) => {}
1932 },
1933 Some(Err(e)) => {
1934 tracing::error!("Got error from event stream");
1935
1936 return Err(Error::ark_server(e));
1937 }
1938 None => {
1939 return Err(Error::ark_server("dropped batch event stream"));
1940 }
1941 }
1942 }
1943
1944 #[derive(Debug, PartialEq, Eq)]
1945 enum Step {
1946 Start,
1947 BatchStarted,
1948 BatchSigningStarted,
1949 Finalized,
1950 }
1951
1952 impl Step {
1953 fn next(&self) -> Step {
1954 match self {
1955 Step::Start => Step::BatchStarted,
1956 Step::BatchStarted => Step::BatchSigningStarted,
1957 Step::BatchSigningStarted => Step::Finalized,
1958 Step::Finalized => Step::Finalized, }
1960 }
1961 }
1962 }
1963}
1964
1965#[derive(Debug, Clone)]
1966pub(crate) enum PrepareIntentKind {
1967 Register,
1968 EstimateFee,
1969}
1970
1971#[derive(Debug, Clone)]
1972pub(crate) enum BatchOutputType {
1973 Board {
1974 to_address: ArkAddress,
1975 to_amount: Amount,
1976 },
1977 OffBoard {
1978 to_address: Address,
1979 to_amount: Amount,
1980 change_address: ArkAddress,
1981 change_amount: Amount,
1982 },
1983}
1984
1985pub(crate) struct PreparedIntent {
1987 pub intent: intent::Intent,
1989 pub cosigner_keypair: Keypair,
1991 pub vtxo_input_outpoints: Vec<OutPoint>,
1993 pub outputs: Vec<intent::Output>,
1995 pub onchain_inputs: Vec<batch::OnChainInput>,
1997 pub vtxo_inputs: Vec<intent::Input>,
1999}