1
2use std::convert::Infallible;
3
4use bitcoin::{Transaction, Txid};
5use bitcoin::amount::CheckedSum;
6use bitcoin::secp256k1::Keypair;
7
8use crate::{Vtxo, VtxoId, VtxoPolicy, ServerVtxo, Amount};
9use crate::arkoor::ArkoorDestination;
10use crate::arkoor::{
11 ArkoorBuilder, ArkoorConstructionError, state, ArkoorCosignResponse,
12 ArkoorSigningError, ArkoorCosignRequest,
13};
14use crate::vtxo::Full;
15
16
17pub struct ArkoorPackageBuilder<S: state::BuilderState> {
25 pub builders: Vec<ArkoorBuilder<S>>,
26}
27
28#[derive(Debug, Clone)]
29pub struct ArkoorPackageCosignRequest<V> {
30 pub requests: Vec<ArkoorCosignRequest<V>>
31}
32
33impl<V> ArkoorPackageCosignRequest<V> {
34 pub fn convert_vtxo<F, O>(self, mut f: F) -> ArkoorPackageCosignRequest<O>
35 where F: FnMut(V) -> O
36 {
37 ArkoorPackageCosignRequest {
38 requests: self.requests.into_iter().map(|r| {
39 ArkoorCosignRequest {
40 user_pub_nonces: r.user_pub_nonces,
41 input: f(r.input),
42 outputs: r.outputs,
43 isolated_outputs: r.isolated_outputs,
44 use_checkpoint: r.use_checkpoint,
45 attestation: r.attestation,
46 }
47 }).collect::<Vec<_>>(),
48 }
49 }
50
51 pub fn inputs(&self) -> impl Iterator<Item=&V> {
52 self.requests.iter()
53 .map(|r| Some(&r.input))
54 .flatten()
55 }
56
57 pub fn all_outputs(
58 &self,
59 ) -> impl Iterator<Item = &ArkoorDestination> + Clone {
60 self.requests.iter()
61 .map(|r| r.all_outputs())
62 .flatten()
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
67#[error("VTXO id mismatch. Expected {expected}, got {got}")]
68pub struct InputMismatchError {
69 expected: VtxoId,
70 got: VtxoId,
71}
72
73impl ArkoorPackageCosignRequest<VtxoId> {
74 pub fn set_vtxos(
75 self,
76 vtxos: impl IntoIterator<Item = Vtxo<Full>>,
77 ) -> Result<ArkoorPackageCosignRequest<Vtxo<Full>>, InputMismatchError> {
78 let package = ArkoorPackageCosignRequest {
79 requests: self.requests.into_iter().zip(vtxos).map(|(r, vtxo)| {
80 if r.input != vtxo.id() {
81 return Err(InputMismatchError {
82 expected: r.input,
83 got: vtxo.id(),
84 })
85 }
86
87 Ok(ArkoorCosignRequest {
88 input: vtxo,
89 user_pub_nonces: r.user_pub_nonces,
90 outputs: r.outputs,
91 isolated_outputs: r.isolated_outputs,
92 use_checkpoint: r.use_checkpoint,
93 attestation: r.attestation,
94 })
95 }).collect::<Result<Vec<_>, _>>()?,
96 };
97
98 Ok(package)
99 }
100}
101
102#[derive(Debug, Clone)]
103pub struct ArkoorPackageCosignResponse {
104 pub responses: Vec<ArkoorCosignResponse>
105}
106
107impl ArkoorPackageBuilder<state::Initial> {
108 fn allocate_outputs_to_inputs(
113 inputs: impl IntoIterator<Item = Vtxo<Full>>,
114 outputs: Vec<ArkoorDestination>,
115 ) -> Result<Vec<(Vtxo<Full>, Vec<ArkoorDestination>)>, ArkoorConstructionError> {
116 let total_output = outputs.iter().map(|r| r.total_amount)
118 .checked_sum()
119 .ok_or(ArkoorConstructionError::Overflow)?;
120 if outputs.is_empty() || total_output == Amount::ZERO {
121 return Err(ArkoorConstructionError::NoOutputs);
122 }
123
124 let mut allocations: Vec<(Vtxo<Full>, Vec<ArkoorDestination>)> = Vec::new();
125
126 let mut output_iter = outputs.into_iter();
127 let mut current_output = output_iter.next();
128 let mut current_output_remaining = current_output.as_ref()
129 .map(|o| o.total_amount).unwrap_or_default();
130
131 let mut total_input = Amount::ZERO;
132 'inputs:
133 for input in inputs {
134 total_input = total_input.checked_add(input.amount())
135 .ok_or(ArkoorConstructionError::Overflow)?;
136
137 let mut input_remaining = input.amount();
138 let mut input_allocation: Vec<ArkoorDestination> = Vec::new();
139
140 'outputs:
141 while let Some(ref output) = current_output {
142 let _: Infallible = if input_remaining == current_output_remaining {
143 input_allocation.push(ArkoorDestination {
145 total_amount: current_output_remaining,
146 policy: output.policy.clone(),
147 });
148
149 current_output = output_iter.next();
150 current_output_remaining = current_output.as_ref()
151 .map(|o| o.total_amount).unwrap_or_default();
152 allocations.push((input, input_allocation));
153 continue 'inputs;
154 } else if input_remaining > current_output_remaining {
155 input_allocation.push(ArkoorDestination {
157 total_amount: current_output_remaining,
158 policy: output.policy.clone(),
159 });
160
161 input_remaining -= current_output_remaining;
162
163 current_output = output_iter.next();
164 current_output_remaining = current_output.as_ref()
165 .map(|o| o.total_amount).unwrap_or_default();
166 continue 'outputs;
167 } else {
168 input_allocation.push(ArkoorDestination {
170 total_amount: input_remaining,
171 policy: output.policy.clone(),
172 });
173
174 current_output_remaining -= input_remaining;
175
176 allocations.push((input, input_allocation));
177 continue 'inputs;
178 };
179 }
180 }
181
182 if total_input != total_output {
183 return Err(ArkoorConstructionError::Unbalanced {
184 input: total_input,
185 output: total_output,
186 });
187 }
188
189 Ok(allocations)
190 }
191
192 pub fn new_with_checkpoints(
194 inputs: impl IntoIterator<Item = Vtxo<Full>>,
195 outputs: Vec<ArkoorDestination>,
196 ) -> Result<Self, ArkoorConstructionError> {
197 Self::new(inputs, outputs, true)
198 }
199
200 pub fn new_without_checkpoints(
202 inputs: impl IntoIterator<Item = Vtxo<Full>>,
203 outputs: Vec<ArkoorDestination>,
204 ) -> Result<Self, ArkoorConstructionError> {
205 Self::new(inputs, outputs, false)
206 }
207
208 pub fn new_single_output_with_checkpoints(
213 inputs: impl IntoIterator<Item = Vtxo<Full>>,
214 output: ArkoorDestination,
215 change_policy: VtxoPolicy,
216 ) -> Result<Self, ArkoorConstructionError> {
217 let inputs = inputs.into_iter().collect::<Vec<_>>();
219 let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
220
221 let change_amount = total_input.checked_sub(output.total_amount)
222 .ok_or(ArkoorConstructionError::Unbalanced {
223 input: total_input,
224 output: output.total_amount,
225 })?;
226
227 let outputs = if change_amount == Amount::ZERO {
228 vec![output]
229 } else {
230 vec![
231 output,
232 ArkoorDestination {
233 total_amount: change_amount,
234 policy: change_policy,
235 },
236 ]
237 };
238
239 Self::new_with_checkpoints(inputs, outputs)
240 }
241
242 pub fn new_claim_all_with_checkpoints(
244 inputs: impl IntoIterator<Item = Vtxo<Full>>,
245 output_policy: VtxoPolicy,
246 ) -> Result<Self, ArkoorConstructionError> {
247 let inputs = inputs.into_iter().collect::<Vec<_>>();
249 let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
250
251 let output = ArkoorDestination {
252 total_amount: total_input,
253 policy: output_policy,
254 };
255
256 Self::new_with_checkpoints(inputs, vec![output])
257 }
258
259 pub fn new_claim_all_without_checkpoints(
261 inputs: impl IntoIterator<Item = Vtxo<Full>>,
262 output_policy: VtxoPolicy,
263 ) -> Result<Self, ArkoorConstructionError> {
264 let inputs = inputs.into_iter().collect::<Vec<_>>();
266 let total_input = inputs.iter().map(|v| v.amount()).sum::<Amount>();
267
268 let output = ArkoorDestination {
269 total_amount: total_input,
270 policy: output_policy,
271 };
272
273 Self::new_without_checkpoints(inputs, vec![output])
274 }
275
276 fn new(
277 inputs: impl IntoIterator<Item = Vtxo<Full>>,
278 outputs: Vec<ArkoorDestination>,
279 use_checkpoint: bool,
280 ) -> Result<Self, ArkoorConstructionError> {
281 let allocations = Self::allocate_outputs_to_inputs(inputs, outputs)?;
283
284 let mut builders = Vec::with_capacity(allocations.len());
286 for (input, allocated_outputs) in allocations {
287 let builder = ArkoorBuilder::new_isolate_dust(
288 input,
289 allocated_outputs,
290 use_checkpoint,
291 )?;
292 builders.push(builder);
293 }
294
295 Ok(Self { builders })
296 }
297
298 pub fn generate_user_nonces(
299 self,
300 user_keypairs: &[Keypair],
301 ) -> Result<ArkoorPackageBuilder<state::UserGeneratedNonces>, ArkoorSigningError> {
302 if user_keypairs.len() != self.builders.len() {
303 return Err(ArkoorSigningError::InvalidNbKeypairs {
304 expected: self.builders.len(),
305 got: user_keypairs.len(),
306 })
307 }
308
309 let mut builder = Vec::with_capacity(self.builders.len());
310 for (idx, package) in self.builders.into_iter().enumerate() {
311 builder.push(package.generate_user_nonces(user_keypairs[idx]));
312 }
313 Ok(ArkoorPackageBuilder { builders: builder })
314 }
315
316 pub fn cosign_both(
320 self,
321 user_keypairs: &[Keypair],
322 server_keypair: &Keypair,
323 ) -> Result<ArkoorPackageBuilder<state::UserSigned>, ArkoorSigningError> {
324 if user_keypairs.len() != self.builders.len() {
325 return Err(ArkoorSigningError::InvalidNbKeypairs {
326 expected: self.builders.len(),
327 got: user_keypairs.len(),
328 })
329 }
330
331 let mut packages = Vec::with_capacity(self.builders.len());
332 for (idx, pkg) in self.builders.into_iter().enumerate() {
333 packages.push(pkg.cosign_both(&user_keypairs[idx], server_keypair)?);
334 }
335 Ok(ArkoorPackageBuilder { builders: packages })
336 }
337}
338
339impl ArkoorPackageBuilder<state::UserGeneratedNonces> {
340 pub fn user_cosign(
341 self,
342 user_keypairs: &[Keypair],
343 server_cosign_response: ArkoorPackageCosignResponse,
344 ) -> Result<ArkoorPackageBuilder<state::UserSigned>, ArkoorSigningError> {
345 if server_cosign_response.responses.len() != self.builders.len() {
346 return Err(ArkoorSigningError::InvalidNbPackages {
347 expected: self.builders.len(),
348 got: server_cosign_response.responses.len()
349 })
350 }
351
352 if user_keypairs.len() != self.builders.len() {
353 return Err(ArkoorSigningError::InvalidNbKeypairs {
354 expected: self.builders.len(),
355 got: user_keypairs.len(),
356 })
357 }
358
359 let mut packages = Vec::with_capacity(self.builders.len());
360
361 for (idx, pkg) in self.builders.into_iter().enumerate() {
362 packages.push(pkg.user_cosign(
363 &user_keypairs[idx],
364 &server_cosign_response.responses[idx],
365 )?,);
366 }
367 Ok(ArkoorPackageBuilder { builders: packages })
368 }
369
370 pub fn cosign_request(&self) -> ArkoorPackageCosignRequest<Vtxo<Full>> {
371 let requests = self.builders.iter()
372 .map(|package| package.cosign_request())
373 .collect::<Vec<_>>();
374
375 ArkoorPackageCosignRequest { requests }
376 }
377
378}
379
380impl ArkoorPackageBuilder<state::UserSigned> {
381 pub fn build_signed_vtxos(self) -> Vec<Vtxo<Full>> {
382 self.builders.into_iter()
383 .map(|b| b.build_signed_vtxos())
384 .flatten()
385 .collect::<Vec<_>>()
386 }
387
388 pub fn build_signed_internal_vtxos(&self) -> Vec<(ServerVtxo<Full>, Txid)> {
391 self.builders.iter()
392 .map(|b| b.build_signed_internal_vtxos())
393 .flatten()
394 .collect()
395 }
396
397 pub fn signed_virtual_transactions(&self) -> Vec<Transaction> {
398 self.builders.iter()
399 .flat_map(|b| b.signed_virtual_transactions())
400 .collect()
401 }
402}
403
404impl ArkoorPackageBuilder<state::ServerCanCosign> {
405 pub fn from_cosign_request(
406 cosign_request: ArkoorPackageCosignRequest<Vtxo<Full>>,
407 ) -> Result<Self, (usize, ArkoorSigningError)> {
408 let request_iter = cosign_request.requests.into_iter();
409 let mut packages = Vec::with_capacity(request_iter.size_hint().0);
410 for (idx, request) in request_iter.enumerate() {
411 packages.push(ArkoorBuilder::from_cosign_request(request)
412 .map_err(|e| (idx, e))?);
413 }
414
415 Ok(Self { builders: packages })
416 }
417
418 pub fn server_cosign(
419 self,
420 server_keypair: &Keypair,
421 ) -> Result<ArkoorPackageBuilder<state::ServerSigned>, ArkoorSigningError> {
422 let mut packages = Vec::with_capacity(self.builders.len());
423 for package in self.builders.into_iter() {
424 packages.push(package.server_cosign(&server_keypair)?);
425 }
426 Ok(ArkoorPackageBuilder { builders: packages })
427 }
428}
429
430impl ArkoorPackageBuilder<state::ServerSigned> {
431 pub fn cosign_response(&self) -> ArkoorPackageCosignResponse {
432 let responses = self.builders.iter()
433 .map(|package| package.cosign_response())
434 .collect::<Vec<_>>();
435
436 ArkoorPackageCosignResponse { responses }
437 }
438}
439
440impl<S: state::BuilderState> ArkoorPackageBuilder<S> {
441 pub fn input_ids<'a>(&'a self) -> impl Iterator<Item = VtxoId> + Clone + 'a {
443 self.builders.iter().map(|b| b.input().id())
444 }
445
446 pub fn build_unsigned_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo<Full>> + 'a {
447 self.builders.iter()
448 .map(|b| b.build_unsigned_vtxos())
449 .flatten()
450 }
451
452 pub fn build_unsigned_internal_vtxos(&self) -> Vec<(ServerVtxo<Full>, Txid)> {
455 self.builders.iter()
456 .map(|b| b.build_unsigned_internal_vtxos())
457 .flatten()
458 .collect()
459 }
460
461 pub fn input_spend_info<'a>(&'a self) -> impl Iterator<Item = (VtxoId, Txid)> + 'a {
463 self.builders.iter().map(|b| b.input_spend_info())
464 }
465
466 pub fn spend_info<'a>(&'a self) -> impl Iterator<Item = (VtxoId, Txid)> + 'a {
469 self.builders.iter()
470 .map(|b| b.spend_info())
471 .flatten()
472 }
473
474 pub fn virtual_transactions<'a>(&'a self) -> impl Iterator<Item = Txid> + 'a {
475 self.builders.iter()
476 .flat_map(|b| b.virtual_transactions())
477 }
478}
479
480#[cfg(test)]
481mod test {
482 use std::collections::{HashMap, HashSet};
483 use std::str::FromStr;
484
485 use bitcoin::{Transaction, Txid};
486 use bitcoin::secp256k1::Keypair;
487
488 use bitcoin_ext::P2TR_DUST;
489
490 use super::*;
491 use crate::test_util::dummy::DummyTestVtxoSpec;
492 use crate::PublicKey;
493
494 fn server_keypair() -> Keypair {
495 Keypair::from_str("f7a2a5d150afb575e98fff9caeebf6fbebbaeacfdfa7433307b208b39f1155f2").expect("Invalid key")
496 }
497
498 fn alice_keypair() -> Keypair {
499 Keypair::from_str("9b4382c8985f12e4bd8d1b51e63615bf0187843630829f4c5e9c45ef2cf994a4").expect("Invalid key")
500 }
501
502 fn bob_keypair() -> Keypair {
503 Keypair::from_str("c86435ba7e30d7afd7c5df9f3263ce2eb86b3ff9866a16ccd22a0260496ddf0f").expect("Invalid key")
504 }
505
506
507 fn alice_public_key() -> PublicKey {
508 alice_keypair().public_key()
509 }
510
511 fn bob_public_key() -> PublicKey {
512 bob_keypair().public_key()
513 }
514
515 fn dummy_vtxo_for_amount(amt: Amount) -> (Transaction, Vtxo<Full>) {
516 DummyTestVtxoSpec {
517 amount: amt + P2TR_DUST,
518 fee: P2TR_DUST,
519 expiry_height: 1000,
520 exit_delta: 128,
521 user_keypair: alice_keypair(),
522 server_keypair: server_keypair()
523 }.build()
524 }
525
526 fn verify_package_builder(
527 builder: ArkoorPackageBuilder<state::Initial>,
528 keypairs: &[Keypair],
529 funding_tx_map: HashMap<Txid, Transaction>,
530 ) {
531 let vtxs: Vec<Txid> = builder.virtual_transactions().collect();
533 let vtx_set: HashSet<Txid> = vtxs.iter().copied().collect();
534 let spend_txids: HashSet<Txid> = builder.spend_info().map(|(_, txid)| txid).collect();
535
536 assert_eq!(vtxs.len(), vtx_set.len(), "virtual_transactions() contains duplicates");
538
539 for txid in &vtx_set {
541 assert!(spend_txids.contains(txid), "virtual_transaction {} not in spend_info", txid);
542 }
543
544 for txid in &spend_txids {
546 assert!(vtx_set.contains(txid), "spend_info txid {} not in virtual_transactions", txid);
547 }
548
549 let user_builder = builder.generate_user_nonces(keypairs).expect("Valid nb of keypairs");
550 let cosign_requests = user_builder.cosign_request();
551
552 let cosign_responses = ArkoorPackageBuilder::from_cosign_request(cosign_requests)
553 .expect("Invalid cosign requests")
554 .server_cosign(&server_keypair())
555 .expect("Wrong server key")
556 .cosign_response();
557
558
559 let vtxos = user_builder.user_cosign(keypairs, cosign_responses)
560 .expect("Invalid cosign responses")
561 .build_signed_vtxos();
562
563 for vtxo in vtxos {
564 let funding_txid = vtxo.chain_anchor().txid;
565 let funding_tx = funding_tx_map.get(&funding_txid).expect("Funding tx not found");
566 vtxo.validate(&funding_tx).expect("Invalid vtxo");
567
568 let mut prev_tx = funding_tx.clone();
569 for tx in vtxo.transactions().map(|item| item.tx) {
570 crate::test_util::verify_tx(
571 &[prev_tx.output[vtxo.chain_anchor().vout as usize].clone()],
572 0,
573 &tx).expect("Invalid transaction");
574 prev_tx = tx;
575 }
576 }
577 }
578
579 #[test]
580 fn send_full_vtxo() {
581 let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(100_000));
584
585 let package_builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
586 [alice_vtxo],
587 ArkoorDestination {
588 total_amount: Amount::from_sat(100_000),
589 policy: VtxoPolicy::new_pubkey(bob_public_key()),
590 },
591 VtxoPolicy::new_pubkey(alice_public_key())
592 ).expect("Valid package");
593
594 let funding_map = HashMap::from([(funding_tx.compute_txid(), funding_tx)]);
595 verify_package_builder(package_builder, &[alice_keypair()], funding_map);
596 }
597
598 #[test]
599 fn arkoor_dust_change() {
600 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
604 let package_builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
605 [alice_vtxo],
606 ArkoorDestination {
607 total_amount: Amount::from_sat(900),
608 policy: VtxoPolicy::new_pubkey(bob_public_key()),
609 },
610 VtxoPolicy::new_pubkey(alice_public_key())
611 ).expect("Valid package");
612
613 let vtxos: Vec<Vtxo<Full>> = package_builder.build_unsigned_vtxos().collect();
615 assert_eq!(vtxos.len(), 3);
616 assert_eq!(vtxos[0].amount(), Amount::from_sat(670));
617 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
618 assert_eq!(vtxos[1].amount(), Amount::from_sat(230));
619 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
620 assert_eq!(vtxos[2].amount(), Amount::from_sat(100));
621 assert_eq!(vtxos[2].policy().user_pubkey(), alice_public_key());
622 }
623
624 #[test]
625 fn can_send_multiple_inputs() {
626 let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
629 let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
630 let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
631
632 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
633 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
634 ArkoorDestination {
635 total_amount: Amount::from_sat(17_000),
636 policy: VtxoPolicy::new_pubkey(bob_public_key()),
637 },
638 VtxoPolicy::new_pubkey(alice_public_key())
639 ).expect("Valid package");
640
641 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
642 assert_eq!(vtxos.len(), 3);
643 assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
644 assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
645 assert_eq!(vtxos[2].amount(), Amount::from_sat(2_000));
646 assert_eq!(
647 vtxos.iter().map(|v| v.policy().user_pubkey()).collect::<Vec<_>>(),
648 vec![bob_public_key(); 3],
649 );
650
651 let funding_map = HashMap::from([
652 (funding_tx_1.compute_txid(), funding_tx_1),
653 (funding_tx_2.compute_txid(), funding_tx_2),
654 (funding_tx_3.compute_txid(), funding_tx_3),
655 ]);
656 verify_package_builder(
657 package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map,
658 );
659 }
660
661 #[test]
662 fn can_send_multiple_inputs_with_change() {
663 let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
667 let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
668 let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
669
670 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
671 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
672 ArkoorDestination {
673 total_amount: Amount::from_sat(16_000),
674 policy: VtxoPolicy::new_pubkey(bob_public_key()),
675 },
676 VtxoPolicy::new_pubkey(alice_public_key())
677 ).expect("Valid package");
678
679 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
680 assert_eq!(vtxos.len(), 4);
681 assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
682 assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
683 assert_eq!(vtxos[2].amount(), Amount::from_sat(1_000));
684 assert_eq!(vtxos[3].amount(), Amount::from_sat(1_000),
685 "Alice should receive a 1000 sats as change",
686 );
687
688 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
689 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
690 assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
691 assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
692
693 let funding_map = HashMap::from([
694 (funding_tx_1.compute_txid(), funding_tx_1),
695 (funding_tx_2.compute_txid(), funding_tx_2),
696 (funding_tx_3.compute_txid(), funding_tx_3),
697 ]);
698 verify_package_builder(
699 package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map,
700 );
701 }
702
703 #[test]
704 fn can_send_multiple_vtxos_with_dust_change() {
705 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
709 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1_000));
710
711 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
712 [alice_vtxo_1, alice_vtxo_2],
713 ArkoorDestination {
714 total_amount: Amount::from_sat(5_700),
715 policy: VtxoPolicy::new_pubkey(bob_public_key()),
716 },
717 VtxoPolicy::new_pubkey(alice_public_key())
718 ).expect("Valid package");
719
720 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
721 assert_eq!(vtxos.len(), 4);
722 assert_eq!(vtxos[0].amount(), Amount::from_sat(5_000));
723 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
724 assert_eq!(vtxos[1].amount(), Amount::from_sat(670));
725 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
726 assert_eq!(vtxos[2].amount(), Amount::from_sat(30));
727 assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
728 assert_eq!(vtxos[3].amount(), Amount::from_sat(300));
729 assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
730 }
731
732 #[test]
733 fn not_enough_money() {
734 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(900));
738 let result = ArkoorPackageBuilder::new_single_output_with_checkpoints(
739 [alice_vtxo],
740 ArkoorDestination {
741 total_amount: Amount::from_sat(1000),
742 policy: VtxoPolicy::new_pubkey(bob_public_key()),
743 },
744 VtxoPolicy::new_pubkey(alice_public_key())
745 );
746
747 match result {
748 Ok(_) => panic!("Package should be invalid"),
749 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
750 assert_eq!(input, Amount::from_sat(900));
751 assert_eq!(output, Amount::from_sat(1000));
752 }
753 Err(e) => panic!("Unexpected error: {:?}", e),
754 }
755 }
756
757 #[test]
758 fn not_enough_money_with_multiple_inputs() {
759 let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
763 let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
764 let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
765
766 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
767 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
768 ArkoorDestination {
769 total_amount: Amount::from_sat(20_000),
770 policy: VtxoPolicy::new_pubkey(bob_public_key()),
771 },
772 VtxoPolicy::new_pubkey(alice_public_key())
773 );
774
775 match package {
776 Ok(_) => panic!("Package should be invalid"),
777 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
778 assert_eq!(input, Amount::from_sat(17_000));
779 assert_eq!(output, Amount::from_sat(20_000));
780 }
781 Err(e) => panic!("Unexpected error: {:?}", e)
782 }
783 }
784
785 #[test]
786 fn can_use_all_provided_inputs_with_change() {
787 let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
792 let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1000));
793 let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1000));
794 let (_funding_tx, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(1000));
795
796 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
797 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3, alice_vtxo_4],
798 ArkoorDestination {
799 total_amount: Amount::from_sat(2000),
800 policy: VtxoPolicy::new_pubkey(bob_public_key()),
801 },
802 VtxoPolicy::new_pubkey(alice_public_key())
803 ).expect("Package should be valid");
804
805 let vtxos = package.build_unsigned_vtxos().collect::<Vec<_>>();
807 let total_output = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
808 assert_eq!(total_output, Amount::from_sat(4000));
809 }
810
811 #[test]
812 fn single_input_multiple_outputs() {
813 let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
815
816 let outputs = vec![
817 ArkoorDestination {
818 total_amount: Amount::from_sat(4_000),
819 policy: VtxoPolicy::new_pubkey(bob_public_key())
820 },
821 ArkoorDestination {
822 total_amount: Amount::from_sat(3_000),
823 policy: VtxoPolicy::new_pubkey(bob_public_key())
824 },
825 ArkoorDestination {
826 total_amount: Amount::from_sat(3_000),
827 policy: VtxoPolicy::new_pubkey(bob_public_key())
828 },
829 ];
830
831 let package = ArkoorPackageBuilder::new_with_checkpoints(
832 [alice_vtxo.clone()],
833 outputs,
834 ).expect("Valid package");
835
836 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
837 assert_eq!(vtxos.len(), 3);
838 assert_eq!(vtxos[0].amount(), Amount::from_sat(4_000));
839 assert_eq!(vtxos[1].amount(), Amount::from_sat(3_000));
840 assert_eq!(vtxos[2].amount(), Amount::from_sat(3_000));
841
842 let user_keypair = alice_keypair();
844 let user_builder = package.generate_user_nonces(&[user_keypair])
845 .expect("Valid nb of keypairs");
846 let cosign_requests = user_builder.cosign_request();
847
848 let cosign_responses = ArkoorPackageBuilder::from_cosign_request(cosign_requests)
849 .expect("Invalid cosign requests")
850 .server_cosign(&server_keypair())
851 .expect("Wrong server key")
852 .cosign_response();
853
854 let signed_vtxos = user_builder.user_cosign(&[user_keypair], cosign_responses)
855 .expect("Invalid cosign responses")
856 .build_signed_vtxos();
857
858 assert_eq!(signed_vtxos.len(), 3, "Should create 3 signed vtxos");
859
860 signed_vtxos[0].validate(&funding_tx).expect("First vtxo should be valid");
862 }
863
864 #[test]
865 fn output_split_across_inputs() {
866 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(600));
869 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
870
871 let outputs = vec![
872 ArkoorDestination {
873 total_amount: Amount::from_sat(800),
874 policy: VtxoPolicy::new_pubkey(bob_public_key())
875 },
876 ArkoorDestination {
877 total_amount: Amount::from_sat(300),
878 policy: VtxoPolicy::new_pubkey(bob_public_key())
879 },
880 ];
881
882 let package = ArkoorPackageBuilder::new_with_checkpoints(
883 [alice_vtxo_1, alice_vtxo_2],
884 outputs,
885 ).expect("Valid package");
886
887 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
888 assert_eq!(vtxos.len(), 3);
889 assert_eq!(vtxos[0].amount(), Amount::from_sat(600));
890 assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
891 assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
892 assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
893 assert_eq!(vtxos[2].amount(), Amount::from_sat(300));
894 assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
895 }
896
897 #[test]
898 fn dust_splits_allowed() {
899 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(500));
902 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
903
904 let outputs = vec![
905 ArkoorDestination {
906 total_amount: Amount::from_sat(750),
907 policy: VtxoPolicy::new_pubkey(bob_public_key())
908 },
909 ArkoorDestination {
910 total_amount: Amount::from_sat(250),
911 policy: VtxoPolicy::new_pubkey(bob_public_key())
912 },
913 ];
914
915 let package = ArkoorPackageBuilder::new_with_checkpoints(
916 [alice_vtxo_1, alice_vtxo_2],
917 outputs,
918 ).expect("Valid package");
919
920 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
921 assert_eq!(vtxos.len(), 3);
922 assert_eq!(vtxos[0].amount(), Amount::from_sat(500));
923 assert_eq!(vtxos[1].amount(), Amount::from_sat(250)); assert_eq!(vtxos[2].amount(), Amount::from_sat(250));
925 }
926
927 #[test]
928 fn unbalanced_amounts_rejected() {
929 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
931
932 let outputs = vec![
933 ArkoorDestination {
934 total_amount: Amount::from_sat(600),
935 policy: VtxoPolicy::new_pubkey(bob_public_key())
936 },
937 ArkoorDestination {
938 total_amount: Amount::from_sat(600),
939 policy: VtxoPolicy::new_pubkey(bob_public_key())
940 },
941 ];
942
943 let result = ArkoorPackageBuilder::new_with_checkpoints(
944 [alice_vtxo],
945 outputs,
946 );
947
948 match result {
949 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
950 assert_eq!(input, Amount::from_sat(1000));
951 assert_eq!(output, Amount::from_sat(1200));
952 }
953 _ => panic!("Expected Unbalanced error"),
954 }
955 }
956
957 #[test]
958 fn empty_outputs_rejected() {
959 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
960
961 let result = ArkoorPackageBuilder::new_with_checkpoints(
962 [alice_vtxo],
963 vec![],
964 );
965
966 match result {
967 Err(ArkoorConstructionError::NoOutputs) => {}
968 Err(e) => panic!("Expected NoOutputs error, got: {:?}", e),
969 Ok(_) => panic!("Expected NoOutputs error, got Ok"),
970 }
971 }
972
973 #[test]
974 fn multiple_inputs_multiple_outputs_exact_balance() {
975 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
977 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(2000));
978 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1500));
979
980 let outputs = vec![
981 ArkoorDestination {
982 total_amount: Amount::from_sat(2500),
983 policy: VtxoPolicy::new_pubkey(bob_public_key())
984 },
985 ArkoorDestination {
986 total_amount: Amount::from_sat(2000),
987 policy: VtxoPolicy::new_pubkey(bob_public_key())
988 },
989 ];
990
991 let package = ArkoorPackageBuilder::new_with_checkpoints(
992 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
993 outputs,
994 ).expect("Valid package");
995
996 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
997 assert_eq!(vtxos.len(), 4);
998 assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
1002 assert_eq!(vtxos[1].amount(), Amount::from_sat(1500));
1003 assert_eq!(vtxos[2].amount(), Amount::from_sat(500));
1004 assert_eq!(vtxos[3].amount(), Amount::from_sat(1500));
1005 }
1006
1007 #[test]
1008 fn single_output_across_many_inputs() {
1009 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(100));
1012 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(100));
1013 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(100));
1014 let (_funding_tx_4, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(100));
1015
1016 let outputs = vec![
1017 ArkoorDestination {
1018 total_amount: Amount::from_sat(400),
1019 policy: VtxoPolicy::new_pubkey(bob_public_key())
1020 },
1021 ];
1022
1023 let package = ArkoorPackageBuilder::new_with_checkpoints(
1024 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3, alice_vtxo_4],
1025 outputs,
1026 ).expect("Valid package");
1027
1028 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1029 assert_eq!(vtxos.len(), 4);
1030 assert_eq!(vtxos[0].amount(), Amount::from_sat(100));
1031 assert_eq!(vtxos[1].amount(), Amount::from_sat(100));
1032 assert_eq!(vtxos[2].amount(), Amount::from_sat(100));
1033 assert_eq!(vtxos[3].amount(), Amount::from_sat(100));
1034 let total: Amount = vtxos.iter().map(|v| v.amount()).sum();
1035 assert_eq!(total, Amount::from_sat(400));
1036 }
1037
1038 #[test]
1039 fn many_outputs_from_single_input() {
1040 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
1042
1043 let outputs = vec![
1044 ArkoorDestination {
1045 total_amount: Amount::from_sat(100),
1046 policy: VtxoPolicy::new_pubkey(bob_public_key())
1047 },
1048 ArkoorDestination {
1049 total_amount: Amount::from_sat(200),
1050 policy: VtxoPolicy::new_pubkey(bob_public_key())
1051 },
1052 ArkoorDestination {
1053 total_amount: Amount::from_sat(150),
1054 policy: VtxoPolicy::new_pubkey(bob_public_key())
1055 },
1056 ArkoorDestination {
1057 total_amount: Amount::from_sat(250),
1058 policy: VtxoPolicy::new_pubkey(bob_public_key())
1059 },
1060 ArkoorDestination {
1061 total_amount: Amount::from_sat(300),
1062 policy: VtxoPolicy::new_pubkey(bob_public_key())
1063 },
1064 ];
1065
1066 let package = ArkoorPackageBuilder::new_with_checkpoints(
1067 [alice_vtxo],
1068 outputs,
1069 ).expect("Valid package");
1070
1071 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1072 assert_eq!(vtxos.len(), 5);
1073 assert_eq!(vtxos[0].amount(), Amount::from_sat(100));
1074 assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
1075 assert_eq!(vtxos[2].amount(), Amount::from_sat(150));
1076 assert_eq!(vtxos[3].amount(), Amount::from_sat(250));
1077 assert_eq!(vtxos[4].amount(), Amount::from_sat(300));
1078 }
1079
1080 #[test]
1081 fn first_input_exactly_matches_first_output() {
1082 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
1085 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
1086
1087 let outputs = vec![
1088 ArkoorDestination {
1089 total_amount: Amount::from_sat(1000),
1090 policy: VtxoPolicy::new_pubkey(bob_public_key())
1091 },
1092 ArkoorDestination {
1093 total_amount: Amount::from_sat(500),
1094 policy: VtxoPolicy::new_pubkey(bob_public_key())
1095 },
1096 ];
1097
1098 let package = ArkoorPackageBuilder::new_with_checkpoints(
1099 [alice_vtxo_1, alice_vtxo_2],
1100 outputs,
1101 ).expect("Valid package");
1102
1103 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1104 assert_eq!(vtxos.len(), 2);
1105 assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
1106 assert_eq!(vtxos[1].amount(), Amount::from_sat(500));
1107 }
1108
1109 #[test]
1110 fn empty_inputs_rejected() {
1111 let outputs = vec![
1113 ArkoorDestination {
1114 total_amount: Amount::from_sat(1000),
1115 policy: VtxoPolicy::new_pubkey(bob_public_key())
1116 },
1117 ];
1118
1119 let result = ArkoorPackageBuilder::new_with_checkpoints(
1120 Vec::<Vtxo<Full>>::new(),
1121 outputs,
1122 );
1123
1124 match result {
1125 Ok(_) => panic!("Should reject empty inputs"),
1126 Err(ArkoorConstructionError::Unbalanced { input, output }) => {
1127 assert_eq!(input, Amount::ZERO);
1128 assert_eq!(output, Amount::from_sat(1000));
1129 }
1130 Err(e) => panic!("Unexpected error: {:?}", e),
1131 }
1132 }
1133
1134 #[test]
1135 fn alternating_split_pattern() {
1136 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(300));
1141 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(700));
1142 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(500));
1143
1144 let outputs = vec![
1145 ArkoorDestination {
1146 total_amount: Amount::from_sat(500),
1147 policy: VtxoPolicy::new_pubkey(bob_public_key())
1148 },
1149 ArkoorDestination {
1150 total_amount: Amount::from_sat(400),
1151 policy: VtxoPolicy::new_pubkey(bob_public_key())
1152 },
1153 ArkoorDestination {
1154 total_amount: Amount::from_sat(600),
1155 policy: VtxoPolicy::new_pubkey(bob_public_key())
1156 },
1157 ];
1158
1159 let package = ArkoorPackageBuilder::new_with_checkpoints(
1160 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
1161 outputs,
1162 ).expect("Valid package");
1163
1164 let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1165 assert_eq!(vtxos.len(), 5);
1166 assert_eq!(vtxos[0].amount(), Amount::from_sat(300));
1168 assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
1170 assert_eq!(vtxos[2].amount(), Amount::from_sat(400));
1171 assert_eq!(vtxos[3].amount(), Amount::from_sat(100));
1172 assert_eq!(vtxos[4].amount(), Amount::from_sat(500));
1174 let total: Amount = vtxos.iter().map(|v| v.amount()).sum();
1175 assert_eq!(total, Amount::from_sat(1500));
1176 }
1177
1178 #[test]
1179 fn spend_info_correctness_simple_checkpoint() {
1180 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1182 Amount::from_sat(100_000)
1183 );
1184 let input_id = alice_vtxo.id();
1185
1186 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1187 [alice_vtxo],
1188 ArkoorDestination {
1189 total_amount: Amount::from_sat(100_000),
1190 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1191 },
1192 VtxoPolicy::new_pubkey(alice_public_key())
1193 ).expect("Valid package");
1194
1195 let internal_vtxos: Vec<VtxoId> = package
1197 .build_unsigned_internal_vtxos()
1198 .iter().map(|(v, _)| v.id())
1199 .collect();
1200
1201 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1203
1204 let mut expected_vtxo_ids = vec![input_id];
1206 expected_vtxo_ids.extend(internal_vtxos.iter());
1207
1208 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1209 .iter()
1210 .map(|(id, _)| *id)
1211 .collect();
1212
1213 for id in &expected_vtxo_ids {
1215 assert!(
1216 actual_vtxo_ids.contains(id),
1217 "Expected VTXO ID {} not found in spend_info",
1218 id
1219 );
1220 }
1221
1222 assert_eq!(
1224 actual_vtxo_ids.len(),
1225 expected_vtxo_ids.len(),
1226 "spend_info contains unexpected entries"
1227 );
1228 }
1229
1230 #[test]
1231 fn spend_info_correctness_with_dust_isolation() {
1232 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1234 Amount::from_sat(1000)
1235 );
1236 let input_id = alice_vtxo.id();
1237
1238 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1239 [alice_vtxo],
1240 ArkoorDestination {
1241 total_amount: Amount::from_sat(900),
1242 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1243 },
1244 VtxoPolicy::new_pubkey(alice_public_key())
1245 ).expect("Valid package");
1246
1247 let internal_vtxos: Vec<VtxoId> = package
1249 .build_unsigned_internal_vtxos()
1250 .iter().map(|(v, _)| v.id())
1251 .collect();
1252
1253 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1255
1256 let mut expected_vtxo_ids = vec![input_id];
1258 expected_vtxo_ids.extend(internal_vtxos.iter());
1259
1260 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1261 .iter()
1262 .map(|(id, _)| *id)
1263 .collect();
1264
1265 for id in &expected_vtxo_ids {
1267 assert!(
1268 actual_vtxo_ids.contains(id),
1269 "Expected VTXO ID {} not found in spend_info",
1270 id
1271 );
1272 }
1273
1274 assert_eq!(
1276 actual_vtxo_ids.len(),
1277 expected_vtxo_ids.len(),
1278 "spend_info contains unexpected entries"
1279 );
1280 }
1281
1282 #[test]
1283 fn spend_info_correctness_without_checkpoints() {
1284 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1286 Amount::from_sat(100_000)
1287 );
1288 let input_id = alice_vtxo.id();
1289
1290 let package = ArkoorPackageBuilder::new_without_checkpoints(
1291 [alice_vtxo],
1292 vec![
1293 ArkoorDestination {
1294 total_amount: Amount::from_sat(100_000),
1295 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1296 }
1297 ]
1298 ).expect("Valid package");
1299
1300 let internal_vtxos: Vec<VtxoId> = package
1302 .build_unsigned_internal_vtxos()
1303 .iter().map(|(v, _)| v.id())
1304 .collect();
1305
1306 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1308
1309 let mut expected_vtxo_ids = vec![input_id];
1311 expected_vtxo_ids.extend(internal_vtxos.iter());
1312
1313 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1314 .iter()
1315 .map(|(id, _)| *id)
1316 .collect();
1317
1318 for id in &expected_vtxo_ids {
1320 assert!(
1321 actual_vtxo_ids.contains(id),
1322 "Expected VTXO ID {} not found in spend_info",
1323 id
1324 );
1325 }
1326
1327 assert_eq!(
1329 actual_vtxo_ids.len(),
1330 expected_vtxo_ids.len(),
1331 "spend_info contains unexpected entries"
1332 );
1333 }
1334
1335 #[test]
1336 fn spend_info_correctness_multiple_inputs() {
1337 let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(
1339 Amount::from_sat(10_000)
1340 );
1341 let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(
1342 Amount::from_sat(5_000)
1343 );
1344 let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(
1345 Amount::from_sat(2_000)
1346 );
1347
1348 let input_ids = vec![
1349 alice_vtxo_1.id(),
1350 alice_vtxo_2.id(),
1351 alice_vtxo_3.id(),
1352 ];
1353
1354 let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1355 [alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
1356 ArkoorDestination {
1357 total_amount: Amount::from_sat(16_000),
1358 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1359 },
1360 VtxoPolicy::new_pubkey(alice_public_key())
1361 ).expect("Valid package");
1362
1363 let internal_vtxos: Vec<VtxoId> = package
1365 .build_unsigned_internal_vtxos()
1366 .iter().map(|(v, _)| v.id())
1367 .collect();
1368
1369 let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1371
1372 let mut expected_vtxo_ids = input_ids.clone();
1374 expected_vtxo_ids.extend(internal_vtxos.iter());
1375
1376 let actual_vtxo_ids: Vec<VtxoId> = spend_info
1377 .iter()
1378 .map(|(id, _)| *id)
1379 .collect();
1380
1381 for id in &expected_vtxo_ids {
1383 assert!(
1384 actual_vtxo_ids.contains(id),
1385 "Expected VTXO ID {} not found in spend_info",
1386 id
1387 );
1388 }
1389
1390 assert_eq!(
1392 actual_vtxo_ids.len(),
1393 expected_vtxo_ids.len(),
1394 "spend_info contains unexpected entries"
1395 );
1396 }
1397
1398 #[test]
1399 fn output_sum_overflow_rejected() {
1400 let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
1403 let outputs = vec![
1404 ArkoorDestination {
1405 total_amount: Amount::from_sat(u64::MAX),
1406 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1407 },
1408 ArkoorDestination {
1409 total_amount: Amount::from_sat(u64::MAX),
1410 policy: VtxoPolicy::new_pubkey(bob_public_key()),
1411 },
1412 ];
1413
1414 let result = ArkoorPackageBuilder::new_with_checkpoints([alice_vtxo], outputs);
1415 assert_eq!(result.err(), Some(ArkoorConstructionError::Overflow));
1416 }
1417}