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 let input_iter = inputs.into_iter();
59
60 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![], )?;
71
72 packages.push(package);
73 to_be_paid = to_be_paid - input_amount;
74 } else if to_be_paid > Amount::ZERO {
75 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![], )?;
92
93 to_be_paid = Amount::ZERO;
94 packages.push(package);
95 } else {
96 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 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 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 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 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 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 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 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 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 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 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 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) => { }
487 Err(e) => panic!("Unexpected error: {:?}", e)
488 }
489 }
490
491 #[test]
492 fn cannot_overprovision_vtxos() {
493 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) => { }
512 Err(e) => panic!("Unexpected error: {:?}", e)
513 }
514 }
515}