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 let input_iter = inputs.into_iter();
22
23 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 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 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 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 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 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 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 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 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 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 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 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 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) => { }
429 Err(e) => panic!("Unexpected error: {:?}", e)
430 }
431 }
432
433 #[test]
434 fn cannot_overprovision_vtxos() {
435 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) => { }
454 Err(e) => panic!("Unexpected error: {:?}", e)
455 }
456 }
457}