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				}
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				})
93			}).collect::<Result<Vec<_>, _>>()?,
94		};
95
96		Ok(package)
97	}
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, ArkoorSigningError> {
365		let request_iter = cosign_request.requests.into_iter();
366		let mut packages = Vec::with_capacity(request_iter.size_hint().0);
367		for request in request_iter {
368			packages.push(ArkoorBuilder::from_cosign_request(request)?);
369		}
370
371		Ok(Self { builders: packages })
372	}
373
374	pub fn server_cosign(
375		self,
376		server_keypair: &Keypair,
377	) -> Result<ArkoorPackageBuilder<state::ServerSigned>, ArkoorSigningError> {
378		let mut packages = Vec::with_capacity(self.builders.len());
379		for package in self.builders.into_iter() {
380			packages.push(package.server_cosign(&server_keypair)?);
381		}
382		Ok(ArkoorPackageBuilder { builders: packages })
383	}
384}
385
386impl ArkoorPackageBuilder<state::ServerSigned> {
387	pub fn cosign_response(&self) -> ArkoorPackageCosignResponse {
388		let responses = self.builders.iter()
389			.map(|package| package.cosign_response())
390			.collect::<Vec<_>>();
391
392		ArkoorPackageCosignResponse { responses }
393	}
394}
395
396impl<S: state::BuilderState> ArkoorPackageBuilder<S> {
397	/// Access the input VTXO IDs
398	pub fn input_ids<'a>(&'a self) -> impl Iterator<Item = VtxoId> + Clone + 'a {
399		self.builders.iter().map(|b| b.input().id())
400	}
401
402	pub fn build_unsigned_vtxos<'a>(&'a self) -> impl Iterator<Item = Vtxo<Full>> + 'a {
403		self.builders.iter()
404			.map(|b| b.build_unsigned_vtxos())
405			.flatten()
406	}
407
408	/// Builds the unsigned internal VTXOs
409	///
410	/// Returns the checkpoint outputs (if checkpoints are used) and the
411	/// dust isolation output (if dust isolation is used).
412	pub fn build_unsigned_internal_vtxos<'a>(&'a self) -> impl Iterator<Item = ServerVtxo<Full>> + 'a {
413		self.builders.iter()
414			.map(|b| b.build_unsigned_internal_vtxos())
415			.flatten()
416	}
417
418	/// Each [VtxoId] in the list is spent by [Txid]
419	/// in an out-of-round transaction
420	pub fn spend_info<'a>(&'a self) -> impl Iterator<Item = (VtxoId, Txid)> + 'a {
421		self.builders.iter()
422			.map(|b| b.spend_info())
423			.flatten()
424	}
425
426	pub fn virtual_transactions<'a>(&'a self) -> impl Iterator<Item = Txid> + 'a {
427		self.builders.iter()
428			.flat_map(|b| b.virtual_transactions())
429	}
430}
431
432#[cfg(test)]
433mod test {
434	use std::collections::{HashMap, HashSet};
435	use std::str::FromStr;
436
437	use bitcoin::{Transaction, Txid};
438	use bitcoin::secp256k1::Keypair;
439
440	use bitcoin_ext::P2TR_DUST;
441
442	use super::*;
443	use crate::test_util::dummy::DummyTestVtxoSpec;
444	use crate::PublicKey;
445
446	fn server_keypair() -> Keypair {
447		Keypair::from_str("f7a2a5d150afb575e98fff9caeebf6fbebbaeacfdfa7433307b208b39f1155f2").expect("Invalid key")
448	}
449
450	fn alice_keypair() -> Keypair {
451		Keypair::from_str("9b4382c8985f12e4bd8d1b51e63615bf0187843630829f4c5e9c45ef2cf994a4").expect("Invalid key")
452	}
453
454	fn bob_keypair() -> Keypair {
455		Keypair::from_str("c86435ba7e30d7afd7c5df9f3263ce2eb86b3ff9866a16ccd22a0260496ddf0f").expect("Invalid key")
456	}
457
458
459	fn alice_public_key() -> PublicKey {
460		alice_keypair().public_key()
461	}
462
463	fn bob_public_key() -> PublicKey {
464		bob_keypair().public_key()
465	}
466
467	fn dummy_vtxo_for_amount(amt: Amount) -> (Transaction, Vtxo<Full>) {
468		DummyTestVtxoSpec {
469			amount: amt + P2TR_DUST,
470			fee: P2TR_DUST,
471			expiry_height: 1000,
472			exit_delta: 128,
473			user_keypair: alice_keypair(),
474			server_keypair: server_keypair()
475		}.build()
476	}
477
478	fn verify_package_builder(
479		builder: ArkoorPackageBuilder<state::Initial>,
480		keypairs: &[Keypair],
481		funding_tx_map: HashMap<Txid, Transaction>,
482	) {
483		// Verify virtual_transactions and spend_info consistency
484		let vtxs: Vec<Txid> = builder.virtual_transactions().collect();
485		let vtx_set: HashSet<Txid> = vtxs.iter().copied().collect();
486		let spend_txids: HashSet<Txid> = builder.spend_info().map(|(_, txid)| txid).collect();
487
488		// No duplicates in virtual_transactions
489		assert_eq!(vtxs.len(), vtx_set.len(), "virtual_transactions() contains duplicates");
490
491		// Every virtual_transaction is in spend_info
492		for txid in &vtx_set {
493			assert!(spend_txids.contains(txid), "virtual_transaction {} not in spend_info", txid);
494		}
495
496		// Every spend_info txid is in virtual_transactions
497		for txid in &spend_txids {
498			assert!(vtx_set.contains(txid), "spend_info txid {} not in virtual_transactions", txid);
499		}
500
501		let user_builder = builder.generate_user_nonces(keypairs).expect("Valid nb of keypairs");
502		let cosign_requests = user_builder.cosign_request();
503
504		let cosign_responses = ArkoorPackageBuilder::from_cosign_request(cosign_requests)
505			.expect("Invalid cosign requests")
506			.server_cosign(&server_keypair())
507			.expect("Wrong server key")
508			.cosign_response();
509
510
511		let vtxos = user_builder.user_cosign(keypairs, cosign_responses)
512			.expect("Invalid cosign responses")
513			.build_signed_vtxos();
514
515		for vtxo in vtxos {
516			let funding_txid = vtxo.chain_anchor().txid;
517			let funding_tx = funding_tx_map.get(&funding_txid).expect("Funding tx not found");
518			vtxo.validate(&funding_tx).expect("Invalid vtxo");
519
520			let mut prev_tx = funding_tx.clone();
521			for tx in vtxo.transactions().map(|item| item.tx) {
522				crate::test_util::verify_tx(
523					&[prev_tx.output[vtxo.chain_anchor().vout as usize].clone()],
524					0,
525					&tx).expect("Invalid transaction");
526				prev_tx = tx;
527			}
528		}
529	}
530
531	#[test]
532	fn send_full_vtxo() {
533		// Alice sends 100_000 sat to Bob
534		// She owns a single vtxo and fully spends it
535		let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(100_000));
536
537		let package_builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
538			[alice_vtxo],
539			ArkoorDestination {
540				total_amount: Amount::from_sat(100_000),
541				policy: VtxoPolicy::new_pubkey(bob_public_key()),
542			},
543			VtxoPolicy::new_pubkey(alice_public_key())
544		).expect("Valid package");
545
546		let funding_map = HashMap::from([(funding_tx.compute_txid(), funding_tx)]);
547		verify_package_builder(package_builder, &[alice_keypair()], funding_map);
548	}
549
550	#[test]
551	fn arkoor_dust_change() {
552		// Alice tries to send 900 sats to Bob
553		// She only has a vtxo worth a 1000 sats
554		// She will create two outputs: 900 for Bob, 100 subdust change for Alice
555		let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
556		let package_builder = ArkoorPackageBuilder::new_single_output_with_checkpoints(
557			[alice_vtxo],
558			ArkoorDestination {
559				total_amount: Amount::from_sat(900),
560				policy: VtxoPolicy::new_pubkey(bob_public_key()),
561			},
562			VtxoPolicy::new_pubkey(alice_public_key())
563		).expect("Valid package");
564
565		// We should generate 3 vtxos: 670 and 230 for Bob, 100 dust change for Alice
566		let vtxos: Vec<Vtxo<Full>> = package_builder.build_unsigned_vtxos().collect();
567		assert_eq!(vtxos.len(), 3);
568		assert_eq!(vtxos[0].amount(), Amount::from_sat(670));
569		assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
570		assert_eq!(vtxos[1].amount(), Amount::from_sat(230));
571		assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
572		assert_eq!(vtxos[2].amount(), Amount::from_sat(100));
573		assert_eq!(vtxos[2].policy().user_pubkey(), alice_public_key());
574	}
575
576	#[test]
577	fn can_send_multiple_inputs() {
578		// Alice has a vtxo of 10_000, 5_000 and 2_000 sats
579		// Seh can make a payment of 17_000 sats to Bob and spend all her money
580		let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
581		let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
582		let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
583
584		let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
585			[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
586			ArkoorDestination {
587				total_amount: Amount::from_sat(17_000),
588				policy: VtxoPolicy::new_pubkey(bob_public_key()),
589			},
590			VtxoPolicy::new_pubkey(alice_public_key())
591		).expect("Valid package");
592
593		let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
594		assert_eq!(vtxos.len(), 3);
595		assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
596		assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
597		assert_eq!(vtxos[2].amount(), Amount::from_sat(2_000));
598		assert_eq!(
599			vtxos.iter().map(|v| v.policy().user_pubkey()).collect::<Vec<_>>(),
600			vec![bob_public_key(); 3],
601		);
602
603		let funding_map = HashMap::from([
604			(funding_tx_1.compute_txid(), funding_tx_1),
605			(funding_tx_2.compute_txid(), funding_tx_2),
606			(funding_tx_3.compute_txid(), funding_tx_3),
607		]);
608		verify_package_builder(
609			package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map,
610		);
611	}
612
613	#[test]
614	fn can_send_multiple_inputs_with_change() {
615		// Alice has a vtxo of 10_000, 5_000 and 2_000 sats
616		// She can make a payment of 16_000 sats to Bob
617		// She will also get a vtxo with 1_000 sats as change
618		let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
619		let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
620		let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
621
622		let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
623			[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
624			ArkoorDestination {
625				total_amount: Amount::from_sat(16_000),
626				policy: VtxoPolicy::new_pubkey(bob_public_key()),
627			},
628			VtxoPolicy::new_pubkey(alice_public_key())
629		).expect("Valid package");
630
631		let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
632		assert_eq!(vtxos.len(), 4);
633		assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
634		assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
635		assert_eq!(vtxos[2].amount(), Amount::from_sat(1_000));
636		assert_eq!(vtxos[3].amount(), Amount::from_sat(1_000),
637			"Alice should receive a 1000 sats as change",
638		);
639
640		assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
641		assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
642		assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
643		assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
644
645		let funding_map = HashMap::from([
646			(funding_tx_1.compute_txid(), funding_tx_1),
647			(funding_tx_2.compute_txid(), funding_tx_2),
648			(funding_tx_3.compute_txid(), funding_tx_3),
649		]);
650		verify_package_builder(
651			package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map,
652		);
653	}
654
655	#[test]
656	fn can_send_multiple_vtxos_with_dust_change() {
657		// Alice has a vtxo of 5_000 sat and one of 1_000 sat
658		// Alice will send 5_700 sats to Bob
659		// The 300 sat change is subdust but will be created as separate output
660		let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
661		let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1_000));
662
663		let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
664			[alice_vtxo_1, alice_vtxo_2],
665			ArkoorDestination {
666				total_amount: Amount::from_sat(5_700),
667				policy: VtxoPolicy::new_pubkey(bob_public_key()),
668			},
669			VtxoPolicy::new_pubkey(alice_public_key())
670		).expect("Valid package");
671
672		let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
673		assert_eq!(vtxos.len(), 4);
674		assert_eq!(vtxos[0].amount(), Amount::from_sat(5_000));
675		assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
676		assert_eq!(vtxos[1].amount(), Amount::from_sat(670));
677		assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
678		assert_eq!(vtxos[2].amount(), Amount::from_sat(30));
679		assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
680		assert_eq!(vtxos[3].amount(), Amount::from_sat(300));
681		assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
682	}
683
684	#[test]
685	fn not_enough_money() {
686		// Alice tries to send 1000 sats to Bob
687		// She only has a vtxo worth a 900 sats
688		// She will not be able to send the payment
689		let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(900));
690		let result = ArkoorPackageBuilder::new_single_output_with_checkpoints(
691			[alice_vtxo],
692			ArkoorDestination {
693				total_amount: Amount::from_sat(1000),
694				policy: VtxoPolicy::new_pubkey(bob_public_key()),
695			},
696			VtxoPolicy::new_pubkey(alice_public_key())
697		);
698
699		match result {
700			Ok(_) => panic!("Package should be invalid"),
701			Err(ArkoorConstructionError::Unbalanced { input, output }) => {
702				assert_eq!(input, Amount::from_sat(900));
703				assert_eq!(output, Amount::from_sat(1000));
704			}
705			Err(e) => panic!("Unexpected error: {:?}", e),
706		}
707	}
708
709	#[test]
710	fn not_enough_money_with_multiple_inputs() {
711		// Alice has a vtxo of 10_000, 5_000 and 2_000 sats
712		// She tries to send 20_000 sats to Bob
713		// She will not be able to send the payment
714		let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
715		let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
716		let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
717
718		let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
719			[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
720			ArkoorDestination {
721				total_amount: Amount::from_sat(20_000),
722				policy: VtxoPolicy::new_pubkey(bob_public_key()),
723			},
724			VtxoPolicy::new_pubkey(alice_public_key())
725		);
726
727		match package {
728			Ok(_) => panic!("Package should be invalid"),
729			Err(ArkoorConstructionError::Unbalanced { input, output }) => {
730				assert_eq!(input, Amount::from_sat(17_000));
731				assert_eq!(output, Amount::from_sat(20_000));
732			}
733			Err(e) => panic!("Unexpected error: {:?}", e)
734		}
735	}
736
737	#[test]
738	fn can_use_all_provided_inputs_with_change() {
739		// Alice has 4 vtxos of a thousand sats each
740		// She will make a payment of 2000 sats to Bob
741		// She includes all vtxos as input to the arkoor builder
742		// The builder will use all inputs and create 2000 sats of change
743		let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
744		let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1000));
745		let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1000));
746		let (_funding_tx, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(1000));
747
748		let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
749			[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3, alice_vtxo_4],
750			ArkoorDestination {
751				total_amount: Amount::from_sat(2000),
752				policy: VtxoPolicy::new_pubkey(bob_public_key()),
753			},
754			VtxoPolicy::new_pubkey(alice_public_key())
755		).expect("Package should be valid");
756
757		// Verify outputs: should have 2000 for Bob and 2000 change for Alice
758		let vtxos = package.build_unsigned_vtxos().collect::<Vec<_>>();
759		let total_output = vtxos.iter().map(|v| v.amount()).sum::<Amount>();
760		assert_eq!(total_output, Amount::from_sat(4000));
761	}
762
763	#[test]
764	fn single_input_multiple_outputs() {
765		// [10_000] -> [4_000, 3_000, 3_000]
766		let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
767
768		let outputs = vec![
769			ArkoorDestination {
770				total_amount: Amount::from_sat(4_000),
771				policy: VtxoPolicy::new_pubkey(bob_public_key())
772			},
773			ArkoorDestination {
774				total_amount: Amount::from_sat(3_000),
775				policy: VtxoPolicy::new_pubkey(bob_public_key())
776			},
777			ArkoorDestination {
778				total_amount: Amount::from_sat(3_000),
779				policy: VtxoPolicy::new_pubkey(bob_public_key())
780			},
781		];
782
783		let package = ArkoorPackageBuilder::new_with_checkpoints(
784			[alice_vtxo.clone()],
785			outputs,
786		).expect("Valid package");
787
788		let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
789		assert_eq!(vtxos.len(), 3);
790		assert_eq!(vtxos[0].amount(), Amount::from_sat(4_000));
791		assert_eq!(vtxos[1].amount(), Amount::from_sat(3_000));
792		assert_eq!(vtxos[2].amount(), Amount::from_sat(3_000));
793
794		// Manually test one vtxo to verify the approach
795		let user_keypair = alice_keypair();
796		let user_builder = package.generate_user_nonces(&[user_keypair])
797			.expect("Valid nb of keypairs");
798		let cosign_requests = user_builder.cosign_request();
799
800		let cosign_responses = ArkoorPackageBuilder::from_cosign_request(cosign_requests)
801			.expect("Invalid cosign requests")
802			.server_cosign(&server_keypair())
803			.expect("Wrong server key")
804			.cosign_response();
805
806		let signed_vtxos = user_builder.user_cosign(&[user_keypair], cosign_responses)
807			.expect("Invalid cosign responses")
808			.build_signed_vtxos();
809
810		assert_eq!(signed_vtxos.len(), 3, "Should create 3 signed vtxos");
811
812		// Just validate the first vtxo against funding tx
813		signed_vtxos[0].validate(&funding_tx).expect("First vtxo should be valid");
814	}
815
816	#[test]
817	fn output_split_across_inputs() {
818		// [600, 500] -> [800, 300]
819		// Expect: input[0]->600, input[1]->[200, 300]
820		let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(600));
821		let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
822
823		let outputs = vec![
824			ArkoorDestination {
825				total_amount: Amount::from_sat(800),
826				policy: VtxoPolicy::new_pubkey(bob_public_key())
827			},
828			ArkoorDestination {
829				total_amount: Amount::from_sat(300),
830				policy: VtxoPolicy::new_pubkey(bob_public_key())
831			},
832		];
833
834		let package = ArkoorPackageBuilder::new_with_checkpoints(
835			[alice_vtxo_1, alice_vtxo_2],
836			outputs,
837		).expect("Valid package");
838
839		let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
840		assert_eq!(vtxos.len(), 3);
841		assert_eq!(vtxos[0].amount(), Amount::from_sat(600));
842		assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
843		assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
844		assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
845		assert_eq!(vtxos[2].amount(), Amount::from_sat(300));
846		assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
847	}
848
849	#[test]
850	fn dust_splits_allowed() {
851		// [500, 500] -> [750, 250]
852		// Results in 250 sat fragments (< 330)
853		let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(500));
854		let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
855
856		let outputs = vec![
857			ArkoorDestination {
858				total_amount: Amount::from_sat(750),
859				policy: VtxoPolicy::new_pubkey(bob_public_key())
860			},
861			ArkoorDestination {
862				total_amount: Amount::from_sat(250),
863				policy: VtxoPolicy::new_pubkey(bob_public_key())
864			},
865		];
866
867		let package = ArkoorPackageBuilder::new_with_checkpoints(
868			[alice_vtxo_1, alice_vtxo_2],
869			outputs,
870		).expect("Valid package");
871
872		let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
873		assert_eq!(vtxos.len(), 3);
874		assert_eq!(vtxos[0].amount(), Amount::from_sat(500));
875		assert_eq!(vtxos[1].amount(), Amount::from_sat(250)); // sub-dust!
876		assert_eq!(vtxos[2].amount(), Amount::from_sat(250));
877	}
878
879	#[test]
880	fn unbalanced_amounts_rejected() {
881		// [1000] -> [600, 600] = 1200 > 1000
882		let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
883
884		let outputs = vec![
885			ArkoorDestination {
886				total_amount: Amount::from_sat(600),
887				policy: VtxoPolicy::new_pubkey(bob_public_key())
888			},
889			ArkoorDestination {
890				total_amount: Amount::from_sat(600),
891				policy: VtxoPolicy::new_pubkey(bob_public_key())
892			},
893		];
894
895		let result = ArkoorPackageBuilder::new_with_checkpoints(
896			[alice_vtxo],
897			outputs,
898		);
899
900		match result {
901			Err(ArkoorConstructionError::Unbalanced { input, output }) => {
902				assert_eq!(input, Amount::from_sat(1000));
903				assert_eq!(output, Amount::from_sat(1200));
904			}
905			_ => panic!("Expected Unbalanced error"),
906		}
907	}
908
909	#[test]
910	fn empty_outputs_rejected() {
911		let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
912
913		let result = ArkoorPackageBuilder::new_with_checkpoints(
914			[alice_vtxo],
915			vec![],
916		);
917
918		match result {
919			Err(ArkoorConstructionError::NoOutputs) => {}
920			Err(e) => panic!("Expected NoOutputs error, got: {:?}", e),
921			Ok(_) => panic!("Expected NoOutputs error, got Ok"),
922		}
923	}
924
925	#[test]
926	fn multiple_inputs_multiple_outputs_exact_balance() {
927		// [1000, 2000, 1500] -> [2500, 2000]
928		let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
929		let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(2000));
930		let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1500));
931
932		let outputs = vec![
933			ArkoorDestination {
934				total_amount: Amount::from_sat(2500),
935				policy: VtxoPolicy::new_pubkey(bob_public_key())
936			},
937			ArkoorDestination {
938				total_amount: Amount::from_sat(2000),
939				policy: VtxoPolicy::new_pubkey(bob_public_key())
940			},
941		];
942
943		let package = ArkoorPackageBuilder::new_with_checkpoints(
944			[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
945			outputs,
946		).expect("Valid package");
947
948		let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
949		assert_eq!(vtxos.len(), 4);
950		// input[0] 1000 -> output[0]
951		// input[1] 2000 -> output[0] 1500, output[1] 500
952		// input[2] 1500 -> output[1] 1500
953		assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
954		assert_eq!(vtxos[1].amount(), Amount::from_sat(1500));
955		assert_eq!(vtxos[2].amount(), Amount::from_sat(500));
956		assert_eq!(vtxos[3].amount(), Amount::from_sat(1500));
957	}
958
959	#[test]
960	fn single_output_across_many_inputs() {
961		// [100, 100, 100, 100] -> [400]
962		// All inputs consumed fully to create single output
963		let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(100));
964		let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(100));
965		let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(100));
966		let (_funding_tx_4, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(100));
967
968		let outputs = vec![
969			ArkoorDestination {
970				total_amount: Amount::from_sat(400),
971				policy: VtxoPolicy::new_pubkey(bob_public_key())
972			},
973		];
974
975		let package = ArkoorPackageBuilder::new_with_checkpoints(
976			[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3, alice_vtxo_4],
977			outputs,
978		).expect("Valid package");
979
980		let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
981		assert_eq!(vtxos.len(), 4);
982		assert_eq!(vtxos[0].amount(), Amount::from_sat(100));
983		assert_eq!(vtxos[1].amount(), Amount::from_sat(100));
984		assert_eq!(vtxos[2].amount(), Amount::from_sat(100));
985		assert_eq!(vtxos[3].amount(), Amount::from_sat(100));
986		let total: Amount = vtxos.iter().map(|v| v.amount()).sum();
987		assert_eq!(total, Amount::from_sat(400));
988	}
989
990	#[test]
991	fn many_outputs_from_single_input() {
992		// [1000] -> [100, 200, 150, 250, 300]
993		let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
994
995		let outputs = vec![
996			ArkoorDestination {
997				total_amount: Amount::from_sat(100),
998				policy: VtxoPolicy::new_pubkey(bob_public_key())
999			},
1000			ArkoorDestination {
1001				total_amount: Amount::from_sat(200),
1002				policy: VtxoPolicy::new_pubkey(bob_public_key())
1003			},
1004			ArkoorDestination {
1005				total_amount: Amount::from_sat(150),
1006				policy: VtxoPolicy::new_pubkey(bob_public_key())
1007			},
1008			ArkoorDestination {
1009				total_amount: Amount::from_sat(250),
1010				policy: VtxoPolicy::new_pubkey(bob_public_key())
1011			},
1012			ArkoorDestination {
1013				total_amount: Amount::from_sat(300),
1014				policy: VtxoPolicy::new_pubkey(bob_public_key())
1015			},
1016		];
1017
1018		let package = ArkoorPackageBuilder::new_with_checkpoints(
1019			[alice_vtxo],
1020			outputs,
1021		).expect("Valid package");
1022
1023		let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1024		assert_eq!(vtxos.len(), 5);
1025		assert_eq!(vtxos[0].amount(), Amount::from_sat(100));
1026		assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
1027		assert_eq!(vtxos[2].amount(), Amount::from_sat(150));
1028		assert_eq!(vtxos[3].amount(), Amount::from_sat(250));
1029		assert_eq!(vtxos[4].amount(), Amount::from_sat(300));
1030	}
1031
1032	#[test]
1033	fn first_input_exactly_matches_first_output() {
1034		// [1000, 500] -> [1000, 500]
1035		// Perfect alignment - each input goes to one output
1036		let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
1037		let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(500));
1038
1039		let outputs = vec![
1040			ArkoorDestination {
1041				total_amount: Amount::from_sat(1000),
1042				policy: VtxoPolicy::new_pubkey(bob_public_key())
1043			},
1044			ArkoorDestination {
1045				total_amount: Amount::from_sat(500),
1046				policy: VtxoPolicy::new_pubkey(bob_public_key())
1047			},
1048		];
1049
1050		let package = ArkoorPackageBuilder::new_with_checkpoints(
1051			[alice_vtxo_1, alice_vtxo_2],
1052			outputs,
1053		).expect("Valid package");
1054
1055		let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1056		assert_eq!(vtxos.len(), 2);
1057		assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
1058		assert_eq!(vtxos[1].amount(), Amount::from_sat(500));
1059	}
1060
1061	#[test]
1062	fn empty_inputs_rejected() {
1063		// [] -> [1000] should fail
1064		let outputs = vec![
1065			ArkoorDestination {
1066				total_amount: Amount::from_sat(1000),
1067				policy: VtxoPolicy::new_pubkey(bob_public_key())
1068			},
1069		];
1070
1071		let result = ArkoorPackageBuilder::new_with_checkpoints(
1072			Vec::<Vtxo<Full>>::new(),
1073			outputs,
1074		);
1075
1076		match result {
1077			Ok(_) => panic!("Should reject empty inputs"),
1078			Err(ArkoorConstructionError::Unbalanced { input, output }) => {
1079				assert_eq!(input, Amount::ZERO);
1080				assert_eq!(output, Amount::from_sat(1000));
1081			}
1082			Err(e) => panic!("Unexpected error: {:?}", e),
1083		}
1084	}
1085
1086	#[test]
1087	fn alternating_split_pattern() {
1088		// [300, 700, 500] -> [500, 400, 600]
1089		// Complex pattern: input[0] split across output[0-1],
1090		// input[1] covers rest of output[1] and part of output[2],
1091		// input[2] covers rest of output[2]
1092		let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(300));
1093		let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(700));
1094		let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(500));
1095
1096		let outputs = vec![
1097			ArkoorDestination {
1098				total_amount: Amount::from_sat(500),
1099				policy: VtxoPolicy::new_pubkey(bob_public_key())
1100			},
1101			ArkoorDestination {
1102				total_amount: Amount::from_sat(400),
1103				policy: VtxoPolicy::new_pubkey(bob_public_key())
1104			},
1105			ArkoorDestination {
1106				total_amount: Amount::from_sat(600),
1107				policy: VtxoPolicy::new_pubkey(bob_public_key())
1108			},
1109		];
1110
1111		let package = ArkoorPackageBuilder::new_with_checkpoints(
1112			[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
1113			outputs,
1114		).expect("Valid package");
1115
1116		let vtxos: Vec<Vtxo<Full>> = package.build_unsigned_vtxos().collect();
1117		assert_eq!(vtxos.len(), 5);
1118		// input[0] 300 -> output[0] 300
1119		assert_eq!(vtxos[0].amount(), Amount::from_sat(300));
1120		// input[1] 700 -> output[0] 200, output[1] 400, output[2] 100
1121		assert_eq!(vtxos[1].amount(), Amount::from_sat(200));
1122		assert_eq!(vtxos[2].amount(), Amount::from_sat(400));
1123		assert_eq!(vtxos[3].amount(), Amount::from_sat(100));
1124		// input[2] 500 -> output[2] 500
1125		assert_eq!(vtxos[4].amount(), Amount::from_sat(500));
1126		let total: Amount = vtxos.iter().map(|v| v.amount()).sum();
1127		assert_eq!(total, Amount::from_sat(1500));
1128	}
1129
1130	#[test]
1131	fn spend_info_correctness_simple_checkpoint() {
1132		// Test spend_info with simple checkpoint case
1133		let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1134			Amount::from_sat(100_000)
1135		);
1136		let input_id = alice_vtxo.id();
1137
1138		let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1139			[alice_vtxo],
1140			ArkoorDestination {
1141				total_amount: Amount::from_sat(100_000),
1142				policy: VtxoPolicy::new_pubkey(bob_public_key()),
1143			},
1144			VtxoPolicy::new_pubkey(alice_public_key())
1145		).expect("Valid package");
1146
1147		// Collect all internal VTXOs
1148		let internal_vtxos: Vec<VtxoId> = package
1149			.build_unsigned_internal_vtxos()
1150			.map(|v| v.id())
1151			.collect();
1152
1153		// Collect all spend_info entries
1154		let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1155
1156		// The spend_info should contain the input and all internal VTXOs
1157		let mut expected_vtxo_ids = vec![input_id];
1158		expected_vtxo_ids.extend(internal_vtxos.iter());
1159
1160		let actual_vtxo_ids: Vec<VtxoId> = spend_info
1161			.iter()
1162			.map(|(id, _)| *id)
1163			.collect();
1164
1165		// Check that all expected IDs are present
1166		for id in &expected_vtxo_ids {
1167			assert!(
1168				actual_vtxo_ids.contains(id),
1169				"Expected VTXO ID {} not found in spend_info",
1170				id
1171			);
1172		}
1173
1174		// Check that no extra IDs are present
1175		assert_eq!(
1176			actual_vtxo_ids.len(),
1177			expected_vtxo_ids.len(),
1178			"spend_info contains unexpected entries"
1179		);
1180	}
1181
1182	#[test]
1183	fn spend_info_correctness_with_dust_isolation() {
1184		// Test spend_info with dust isolation
1185		let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1186			Amount::from_sat(1000)
1187		);
1188		let input_id = alice_vtxo.id();
1189
1190		let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1191			[alice_vtxo],
1192			ArkoorDestination {
1193				total_amount: Amount::from_sat(900),
1194				policy: VtxoPolicy::new_pubkey(bob_public_key()),
1195			},
1196			VtxoPolicy::new_pubkey(alice_public_key())
1197		).expect("Valid package");
1198
1199		// Collect all internal VTXOs (checkpoints + dust isolation)
1200		let internal_vtxos: Vec<VtxoId> = package
1201			.build_unsigned_internal_vtxos()
1202			.map(|v| v.id())
1203			.collect();
1204
1205		// Collect all spend_info entries
1206		let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1207
1208		// The spend_info should contain the input and all internal VTXOs
1209		let mut expected_vtxo_ids = vec![input_id];
1210		expected_vtxo_ids.extend(internal_vtxos.iter());
1211
1212		let actual_vtxo_ids: Vec<VtxoId> = spend_info
1213			.iter()
1214			.map(|(id, _)| *id)
1215			.collect();
1216
1217		// Check that all expected IDs are present
1218		for id in &expected_vtxo_ids {
1219			assert!(
1220				actual_vtxo_ids.contains(id),
1221				"Expected VTXO ID {} not found in spend_info",
1222				id
1223			);
1224		}
1225
1226		// Check that no extra IDs are present
1227		assert_eq!(
1228			actual_vtxo_ids.len(),
1229			expected_vtxo_ids.len(),
1230			"spend_info contains unexpected entries"
1231		);
1232	}
1233
1234	#[test]
1235	fn spend_info_correctness_without_checkpoints() {
1236		// Test spend_info without checkpoints
1237		let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(
1238			Amount::from_sat(100_000)
1239		);
1240		let input_id = alice_vtxo.id();
1241
1242		let package = ArkoorPackageBuilder::new_without_checkpoints(
1243			[alice_vtxo],
1244			vec![
1245				ArkoorDestination {
1246					total_amount: Amount::from_sat(100_000),
1247					policy: VtxoPolicy::new_pubkey(bob_public_key()),
1248				}
1249			]
1250		).expect("Valid package");
1251
1252		// Collect all internal VTXOs
1253		let internal_vtxos: Vec<VtxoId> = package
1254			.build_unsigned_internal_vtxos()
1255			.map(|v| v.id())
1256			.collect();
1257
1258		// Collect all spend_info entries
1259		let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1260
1261		// The spend_info should contain the input and all internal VTXOs
1262		let mut expected_vtxo_ids = vec![input_id];
1263		expected_vtxo_ids.extend(internal_vtxos.iter());
1264
1265		let actual_vtxo_ids: Vec<VtxoId> = spend_info
1266			.iter()
1267			.map(|(id, _)| *id)
1268			.collect();
1269
1270		// Check that all expected IDs are present
1271		for id in &expected_vtxo_ids {
1272			assert!(
1273				actual_vtxo_ids.contains(id),
1274				"Expected VTXO ID {} not found in spend_info",
1275				id
1276			);
1277		}
1278
1279		// Check that no extra IDs are present
1280		assert_eq!(
1281			actual_vtxo_ids.len(),
1282			expected_vtxo_ids.len(),
1283			"spend_info contains unexpected entries"
1284		);
1285	}
1286
1287	#[test]
1288	fn spend_info_correctness_multiple_inputs() {
1289		// Test spend_info with multiple inputs
1290		let (_funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(
1291			Amount::from_sat(10_000)
1292		);
1293		let (_funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(
1294			Amount::from_sat(5_000)
1295		);
1296		let (_funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(
1297			Amount::from_sat(2_000)
1298		);
1299
1300		let input_ids = vec![
1301			alice_vtxo_1.id(),
1302			alice_vtxo_2.id(),
1303			alice_vtxo_3.id(),
1304		];
1305
1306		let package = ArkoorPackageBuilder::new_single_output_with_checkpoints(
1307			[alice_vtxo_1, alice_vtxo_2, alice_vtxo_3],
1308			ArkoorDestination {
1309				total_amount: Amount::from_sat(16_000),
1310				policy: VtxoPolicy::new_pubkey(bob_public_key()),
1311			},
1312			VtxoPolicy::new_pubkey(alice_public_key())
1313		).expect("Valid package");
1314
1315		// Collect all internal VTXOs
1316		let internal_vtxos: Vec<VtxoId> = package
1317			.build_unsigned_internal_vtxos()
1318			.map(|v| v.id())
1319			.collect();
1320
1321		// Collect all spend_info entries
1322		let spend_info: Vec<(VtxoId, Txid)> = package.spend_info().collect();
1323
1324		// The spend_info should contain all inputs and all internal VTXOs
1325		let mut expected_vtxo_ids = input_ids.clone();
1326		expected_vtxo_ids.extend(internal_vtxos.iter());
1327
1328		let actual_vtxo_ids: Vec<VtxoId> = spend_info
1329			.iter()
1330			.map(|(id, _)| *id)
1331			.collect();
1332
1333		// Check that all expected IDs are present
1334		for id in &expected_vtxo_ids {
1335			assert!(
1336				actual_vtxo_ids.contains(id),
1337				"Expected VTXO ID {} not found in spend_info",
1338				id
1339			);
1340		}
1341
1342		// Check that no extra IDs are present
1343		assert_eq!(
1344			actual_vtxo_ids.len(),
1345			expected_vtxo_ids.len(),
1346			"spend_info contains unexpected entries"
1347		);
1348	}
1349}