Skip to main content

ark/arkoor/
package.rs

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