ark/arkoor/
checkpointed_package.rs

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