Skip to main content

ark/arkoor/
package.rs

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