Skip to main content

ark/arkoor/
package.rs

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