ark/arkoor/
checkpointed_package.rs

1use bitcoin::secp256k1::Keypair;
2use bitcoin_ext::P2TR_DUST;
3
4
5use crate::arkoor::checkpoint::{CheckpointedArkoorBuilder, ArkoorConstructionError, state, CosignResponse, ArkoorSigningError, CosignRequest};
6use crate::{Vtxo, VtxoRequest, VtxoPolicy, PublicKey, Amount};
7
8pub struct CheckpointedPackageBuilder<'a, S: state::BuilderState> {
9	builders: Vec<CheckpointedArkoorBuilder<'a, S>>,
10}
11
12impl<'a> CheckpointedPackageBuilder<'a, state::Initial> {
13
14	pub fn new(
15		inputs: impl IntoIterator<Item = &'a Vtxo>,
16		output: VtxoRequest,
17		change_pubkey: PublicKey,
18	) -> Result<Self, ArkoorConstructionError> {
19		// Some of the algorithms read a bit awkward.
20		// The key problem is that we can only iterate over the inputs once.
21		let input_iter = inputs.into_iter();
22
23		// Constructs a package for each input vtxo
24		let mut packages = Vec::with_capacity(input_iter.size_hint().0);
25		let mut to_be_paid = output.amount;
26		for input in input_iter {
27			if to_be_paid >= input.amount() {
28				let package = CheckpointedArkoorBuilder::new(
29					&input,
30					vec![VtxoRequest { amount: input.amount(), policy: output.policy.clone() }]
31				)?;
32
33				packages.push(package);
34				to_be_paid = to_be_paid - input.amount();
35			} else if to_be_paid >  Amount::ZERO {
36				// If change_amount is less than P2TR we don't do change
37				// We will send the left-overs as a tip
38				let change_amount = input.amount() - to_be_paid;
39				let requests = if change_amount < P2TR_DUST {
40					vec![VtxoRequest { amount: input.amount(), policy: output.policy.clone() }]
41				} else {
42					vec![
43						VtxoRequest { amount: to_be_paid, policy: output.policy.clone() },
44						VtxoRequest { amount: change_amount, policy: VtxoPolicy::new_pubkey(change_pubkey) }
45					]
46				};
47
48				let package = CheckpointedArkoorBuilder::new(
49					&input,
50					requests,
51				)?;
52
53				to_be_paid = Amount::ZERO;
54				packages.push(package);
55			} else {
56				// In this case we aren't using all the inputs.
57				return Err(ArkoorConstructionError::TooManyInputs)
58			}
59		}
60
61		if to_be_paid != Amount::ZERO {
62			return Err(ArkoorConstructionError::Unbalanced {
63				input: output.amount - to_be_paid,
64				output: output.amount,
65			})
66		}
67
68		Ok(Self { builders: packages })
69	}
70
71	pub fn generate_user_nonces(self, user_keypairs: &[Keypair]) -> Result<CheckpointedPackageBuilder<'a, state::UserGeneratedNonces>, ArkoorSigningError> {
72		if user_keypairs.len() != self.builders.len() {
73			return Err(ArkoorSigningError::InvalidNbKeypairs { expected: self.builders.len(), got: user_keypairs.len() })
74		}
75
76		let mut builder = Vec::with_capacity(self.builders.len());
77		for (idx, package) in self.builders.into_iter().enumerate() {
78			builder.push(package.generate_user_nonces(user_keypairs[idx]));
79		}
80		Ok(CheckpointedPackageBuilder { builders: builder })
81	}
82}
83
84impl<'a> CheckpointedPackageBuilder<'a, state::UserGeneratedNonces> {
85	pub fn user_cosign(self, user_keypair: &[Keypair], server_cosign_response: &[CosignResponse]) -> Result<CheckpointedPackageBuilder<'a, state::UserSigned>, ArkoorSigningError> {
86		if server_cosign_response.len() != self.builders.len() {
87			return Err(ArkoorSigningError::InvalidNbPackages {
88				expected: self.builders.len(),
89				got: server_cosign_response.len()
90			})
91		}
92
93		if user_keypair.len() != self.builders.len() {
94			return Err(ArkoorSigningError::InvalidNbKeypairs { expected: self.builders.len(), got: user_keypair.len()})
95		}
96
97		let mut packages = Vec::with_capacity(self.builders.len());
98
99		for (idx, pkg) in self.builders.into_iter().enumerate() {
100			packages.push(pkg.user_cosign(&user_keypair[idx], &server_cosign_response[idx])?);
101		}
102		Ok(CheckpointedPackageBuilder { builders: packages })
103	}
104
105	pub fn cosign_requests(&'a self) -> Vec<CosignRequest<'a>> {
106		self.builders.iter()
107			.map(|package| package.cosign_request())
108			.collect::<Vec<_>>()
109	}
110}
111
112impl<'a> CheckpointedPackageBuilder<'a, state::UserSigned> {
113	pub fn build_signed_vtxos(self) -> Vec<Vtxo> {
114		self.builders.into_iter()
115			.map(|package| package.build_signed_vtxos())
116			.flatten()
117			.collect::<Vec<_>>()
118	}
119}
120
121impl<'a> CheckpointedPackageBuilder<'a, state::ServerCanCosign> {
122	pub fn from_cosign_requests(cosign_requests: &'a[CosignRequest<'a>]) -> Result<Self, ArkoorSigningError> {
123		let request_iter = cosign_requests.into_iter();
124		let mut packages = Vec::with_capacity(request_iter.size_hint().0);
125		for request in request_iter {
126			packages.push(CheckpointedArkoorBuilder::<'a>::from_cosign_request(&request)?);
127		}
128		Ok(Self { builders: packages })
129	}
130
131	pub fn server_cosign(self, server_keypair: Keypair) -> Result<CheckpointedPackageBuilder<'a, state::ServerSigned>, ArkoorSigningError> {
132		let mut packages = Vec::with_capacity(self.builders.len());
133		for package in self.builders.into_iter() {
134			packages.push(package.server_cosign(server_keypair)?);
135		}
136		Ok(CheckpointedPackageBuilder { builders: packages })
137	}
138}
139
140impl<'a> CheckpointedPackageBuilder<'a, state::ServerSigned> {
141	pub fn cosign_responses(&self) -> Vec<CosignResponse> {
142		self.builders.iter()
143			.map(|package| package.cosign_response())
144			.collect::<Vec<_>>()
145	}
146}
147
148impl<'a, S: state::BuilderState> CheckpointedPackageBuilder<'a, S> {
149
150	pub fn build_unsigned_vtxos(&self) -> Vec<Vtxo> {
151		self.builders.iter()
152			.map(|package| package.build_unsigned_vtxos())
153			.flatten()
154			.collect::<Vec<_>>()
155	}
156}
157
158#[cfg(test)]
159mod test {
160	use std::collections::HashMap;
161	use std::str::FromStr;
162
163	use bitcoin::{Transaction, Txid};
164	use bitcoin::secp256k1::Keypair;
165	use super::*;
166	use crate::test::dummy::DummyTestVtxoSpec;
167
168	fn server_keypair() -> Keypair {
169		Keypair::from_str("f7a2a5d150afb575e98fff9caeebf6fbebbaeacfdfa7433307b208b39f1155f2").expect("Invalid key")
170	}
171
172	fn alice_keypair() -> Keypair {
173		Keypair::from_str("9b4382c8985f12e4bd8d1b51e63615bf0187843630829f4c5e9c45ef2cf994a4").expect("Invalid key")
174	}
175
176	fn bob_keypair() -> Keypair {
177		Keypair::from_str("c86435ba7e30d7afd7c5df9f3263ce2eb86b3ff9866a16ccd22a0260496ddf0f").expect("Invalid key")
178	}
179
180
181	fn alice_public_key() -> PublicKey {
182		alice_keypair().public_key()
183	}
184
185	fn bob_public_key() -> PublicKey {
186		bob_keypair().public_key()
187	}
188
189	fn dummy_vtxo_for_amount(amount: Amount) -> (Transaction, Vtxo) {
190		DummyTestVtxoSpec {
191			amount: amount,
192			expiry_height: 1000,
193			exit_delta: 128,
194			user_keypair: alice_keypair(),
195			server_keypair: server_keypair()
196		}.build()
197	}
198
199	fn verify_package_builder(builder: CheckpointedPackageBuilder<state::Initial>, keypairs: &[Keypair], funding_tx_map: HashMap<Txid, Transaction>) {
200		let user_builder = builder.generate_user_nonces(keypairs).expect("Valid nb of keypairs");
201		let cosign_requests = user_builder.cosign_requests();
202
203		let cosign_responses = CheckpointedPackageBuilder::from_cosign_requests(&cosign_requests)
204			.expect("Invalid cosign requests")
205			.server_cosign(server_keypair())
206			.expect("Wrong server key")
207			.cosign_responses();
208
209
210		let vtxos = user_builder.user_cosign(keypairs, &cosign_responses)
211			.expect("Invalid cosign responses")
212			.build_signed_vtxos();
213
214		for vtxo in vtxos {
215			let funding_txid = vtxo.chain_anchor().txid;
216			let funding_tx = funding_tx_map.get(&funding_txid).expect("Funding tx not found");
217			vtxo.validate(&funding_tx).expect("Invalid vtxo");
218
219			let mut prev_tx = funding_tx.clone();
220			for tx in vtxo.transactions().map(|item| item.tx) {
221				crate::test::verify_tx(
222					&[prev_tx.output[vtxo.chain_anchor().vout as usize].clone()],
223					0,
224					&tx).expect("Invalid transaction");
225				prev_tx = tx;
226			}
227		}
228	}
229
230	#[test]
231	fn send_full_vtxo() {
232		// Alice sends 100_000 sat to Bob
233		// She owns a single vtxo and fully spends it
234		let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(100_000));
235
236		let package_builder = CheckpointedPackageBuilder::new(
237			[&alice_vtxo],
238			VtxoRequest { amount: Amount::from_sat(100_000), policy: VtxoPolicy::new_pubkey(bob_public_key()) },
239			alice_public_key()
240		).expect("Valid package");
241
242		let funding_map = HashMap::from([(funding_tx.compute_txid(), funding_tx)]);
243		verify_package_builder(package_builder, &[alice_keypair()], funding_map);
244	}
245
246	#[test]
247	fn arkoor_no_dust_change() {
248		// Alice tries to send 900 sats to Bob
249		// She only has a vtxo worth a 1000 sats
250		// She will send the subdust remainder to Bob as well
251		let (funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
252		let package_builder = CheckpointedPackageBuilder::new(
253			[&alice_vtxo],
254			VtxoRequest { amount: Amount::from_sat(900), policy: VtxoPolicy::new_pubkey(bob_public_key()) },
255			alice_public_key()
256		).expect("Valid package");
257
258		// We should generate one vtxo for an amount of 1000 sat to bob
259		let vtxos = package_builder.build_unsigned_vtxos();
260		assert_eq!(vtxos.len(), 1);
261		assert_eq!(vtxos[0].amount(), Amount::from_sat(1000));
262		assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
263
264
265		// Verify if it produces valid vtxos
266		let funding_map = HashMap::from([(funding_tx.compute_txid(), funding_tx)]);
267		verify_package_builder(package_builder, &[alice_keypair()], funding_map);
268	}
269
270	#[test]
271	fn can_send_multiple_inputs() {
272		// Alice has a vtxo of 10_000, 5_000 and 2_000 sats
273		// Seh can make a payment of 17_000 sats to Bob and spend all her money
274		let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
275		let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
276		let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
277
278		let package = CheckpointedPackageBuilder::new(
279			[&alice_vtxo_1, &alice_vtxo_2, &alice_vtxo_3],
280			VtxoRequest { amount: Amount::from_sat(17_000), policy: VtxoPolicy::new_pubkey(bob_public_key()) },
281			alice_public_key()
282		).expect("Valid package");
283
284		let vtxos = package.build_unsigned_vtxos();
285		assert_eq!(vtxos.len(), 3);
286		assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
287		assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
288		assert_eq!(vtxos[2].amount(), Amount::from_sat(2_000));
289		assert_eq!(vtxos.iter().map(|v| v.policy().user_pubkey()).collect::<Vec<_>>(), vec![bob_public_key(); 3]);
290
291		let funding_map = HashMap::from([
292			(funding_tx_1.compute_txid(), funding_tx_1),
293			(funding_tx_2.compute_txid(), funding_tx_2),
294			(funding_tx_3.compute_txid(), funding_tx_3),
295		]);
296		verify_package_builder(package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map);
297
298	}
299
300	#[test]
301	fn can_send_multiple_inputs_with_change() {
302		// Alice has a vtxo of 10_000, 5_000 and 2_000 sats
303		// She can make a payment of 16_000 sats to Bob
304		// She will also get a vtxo with 1_000 sats as change
305		let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
306		let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
307		let (funding_tx_3, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
308
309		let package = CheckpointedPackageBuilder::new(
310			[&alice_vtxo_1, &alice_vtxo_2, &alice_vtxo_3],
311			VtxoRequest { amount: Amount::from_sat(16_000), policy: VtxoPolicy::new_pubkey(bob_public_key()) },
312			alice_public_key()
313		).expect("Valid package");
314
315		let vtxos = package.build_unsigned_vtxos();
316		assert_eq!(vtxos.len(), 4);
317		assert_eq!(vtxos[0].amount(), Amount::from_sat(10_000));
318		assert_eq!(vtxos[1].amount(), Amount::from_sat(5_000));
319		assert_eq!(vtxos[2].amount(), Amount::from_sat(1_000));
320		assert_eq!(vtxos[3].amount(), Amount::from_sat(1_000), "Alice should receive a 1000 sats as change");
321
322		assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
323		assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
324		assert_eq!(vtxos[2].policy().user_pubkey(), bob_public_key());
325		assert_eq!(vtxos[3].policy().user_pubkey(), alice_public_key());
326
327		let funding_map = HashMap::from([
328			(funding_tx_1.compute_txid(), funding_tx_1),
329			(funding_tx_2.compute_txid(), funding_tx_2),
330			(funding_tx_3.compute_txid(), funding_tx_3),
331		]);
332		verify_package_builder(package, &[alice_keypair(), alice_keypair(), alice_keypair()], funding_map);
333	}
334
335	#[test]
336	fn can_send_multiple_vtxos_and_dust_change_will_be_tipped() {
337		// Alice has a vtxo of 5_000 sat and one of 1_000 sat
338		// Alice will send 5_700 sats to Bob
339		// Because the 300 sat change is subdust it will be tipped to Bob
340		let (funding_tx_1, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
341		let (funding_tx_2, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1_000));
342
343		let package = CheckpointedPackageBuilder::new(
344			[&alice_vtxo_1, &alice_vtxo_2],
345			VtxoRequest { amount: Amount::from_sat(5_700), policy: VtxoPolicy::new_pubkey(bob_public_key()) },
346			alice_public_key()
347		).expect("Valid package");
348
349		let vtxos = package.build_unsigned_vtxos();
350		assert_eq!(vtxos.len(), 2);
351		assert_eq!(vtxos[0].amount(), Amount::from_sat(5_000));
352		assert_eq!(vtxos[1].amount(), Amount::from_sat(1_000));
353
354		assert_eq!(vtxos[0].policy().user_pubkey(), bob_public_key());
355		assert_eq!(vtxos[1].policy().user_pubkey(), bob_public_key());
356
357		let funding_map = HashMap::from([
358			(funding_tx_1.compute_txid(), funding_tx_1),
359			(funding_tx_2.compute_txid(), funding_tx_2),
360		]);
361		verify_package_builder(package, &[alice_keypair(), alice_keypair()], funding_map);
362	}
363
364	#[test]
365	fn not_enough_money() {
366		// Alice tries to send 1000 sats to Bob
367		// She only has a vtxo worth a 900 sats
368		// She will not be able to send the payment
369		let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(900));
370		let result = CheckpointedPackageBuilder::new(
371			[&alice_vtxo],
372			VtxoRequest { amount: Amount::from_sat(1000), policy: VtxoPolicy::new_pubkey(bob_public_key()) },
373			alice_public_key()
374		);
375
376		match result {
377			Ok(_) => panic!("Package should be invalid"),
378			Err(ArkoorConstructionError::Unbalanced { input, output }) => {
379				assert_eq!(input, Amount::from_sat(900));
380				assert_eq!(output, Amount::from_sat(1000));
381			}
382			Err(e) => panic!("Unexpected error: {:?}", e),
383		}
384	}
385
386	#[test]
387	fn not_enough_money_with_multiple_inputs() {
388		// Alice has a vtxo of 10_000, 5_000 and 2_000 sats
389		// She tries to send 20_000 sats to Bob
390		// She will not be able to send the payment
391		let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(10_000));
392		let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(5_000));
393		let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(2_000));
394
395		let package = CheckpointedPackageBuilder::new(
396			[&alice_vtxo_1, &alice_vtxo_2, &alice_vtxo_3],
397			VtxoRequest { amount: Amount::from_sat(20_000), policy: VtxoPolicy::new_pubkey(bob_public_key()) },
398			alice_public_key()
399		);
400
401		match package {
402			Ok(_) => panic!("Package should be invalid"),
403			Err(ArkoorConstructionError::Unbalanced { input, output }) => {
404				assert_eq!(input, Amount::from_sat(17_000));
405				assert_eq!(output, Amount::from_sat(20_000));
406			}
407			Err(e) => panic!("Unexpected error: {:?}", e)
408		}
409	}
410
411	#[test]
412	fn cannot_send_dust() {
413		// Alice has a vtxo worth a 1000 sats
414		// Alice tries to send 100 sats to Bob
415		// Sending subdust amounts is not allowed
416		let (_funding_tx, alice_vtxo) = dummy_vtxo_for_amount(Amount::from_sat(1000));
417		let result = CheckpointedPackageBuilder::new(
418			[&alice_vtxo],
419			VtxoRequest {
420				amount: Amount::from_sat(100),
421				policy: VtxoPolicy::new_pubkey(bob_public_key())
422			},
423			alice_public_key()
424		);
425
426		match result {
427			Ok(_) => panic!("Should not allow sending dust amounts"),
428			Err(ArkoorConstructionError::Dust) => { /* ok */ }
429			Err(e) => panic!("Unexpected error: {:?}", e)
430		}
431	}
432
433	#[test]
434	fn cannot_overprovision_vtxos() {
435		// Alice has 4 vtxos of a thousand sats each
436		// She will try to make a payment of 2000 sats to Bob
437		// She will include all of these vtxos as input to the arkoor builder
438		// The arkoor builder will refuse to make the payment because
439		// alice has overprovisioned her vtxos.
440		let (_funding_tx, alice_vtxo_1) = dummy_vtxo_for_amount(Amount::from_sat(1000));
441		let (_funding_tx, alice_vtxo_2) = dummy_vtxo_for_amount(Amount::from_sat(1000));
442		let (_funding_tx, alice_vtxo_3) = dummy_vtxo_for_amount(Amount::from_sat(1000));
443		let (_funding_tx, alice_vtxo_4) = dummy_vtxo_for_amount(Amount::from_sat(1000));
444
445		let package = CheckpointedPackageBuilder::new(
446			[&alice_vtxo_1, &alice_vtxo_2, &alice_vtxo_3, &alice_vtxo_4],
447			VtxoRequest { amount: Amount::from_sat(2000), policy: VtxoPolicy::new_pubkey(bob_public_key()) },
448			alice_public_key()
449		);
450
451		match package {
452			Ok(_) => panic!("Package should be invalid"),
453			Err(ArkoorConstructionError::TooManyInputs) => { /* ok */ }
454			Err(e) => panic!("Unexpected error: {:?}", e)
455		}
456	}
457}