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