1use std::borrow::Cow;
68use std::marker::PhantomData;
69
70use bitcoin::hashes::Hash;
71use bitcoin::{Amount, OutPoint, TapSighash, Transaction, Txid, TxIn, TxOut, ScriptBuf, Sequence, Witness};
72use bitcoin::taproot::TapTweakHash;
73use bitcoin::secp256k1::{schnorr, Keypair, PublicKey};
74use bitcoin_ext::{fee, P2TR_DUST, TxOutExt};
75use secp256k1_musig::musig::PublicNonce;
76
77use crate::vtxo::{GenesisItem, GenesisTransition};
78use crate::arkoor::arkoor_sighash;
79use crate::{Vtxo, VtxoId};
80use crate::VtxoRequest;
81use crate::scripts;
82use crate::musig;
83use crate::vtxo::VtxoPolicy;
84
85#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
86pub enum ArkoorConstructionError {
87 #[error("Input amount of {input} does not match output amount of {output}")]
88 Unbalanced {
89 input: Amount,
90 output: Amount,
91 },
92 #[error("An output is below the dust threshold")]
93 Dust,
94 #[error("Too many inputs provided")]
95 TooManyInputs,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
99pub enum ArkoorSigningError {
100 #[error("An error occurred while building arkoor: {0}")]
101 ArkoorConstructionError(ArkoorConstructionError),
102 #[error("Wrong number of user nonces provided. Expected {expected}, got {got}")]
103 InvalidNbUserNonces {
104 expected: usize,
105 got: usize,
106 },
107 #[error("Wrong number of server nonces provided. Expected {expected}, got {got}")]
108 InvalidNbServerNonces {
109 expected: usize,
110 got: usize,
111 },
112 #[error("Incorrect signing key provided. Expected {expected}, got {got}")]
113 IncorrectKey {
114 expected: PublicKey,
115 got: PublicKey,
116 },
117 #[error("Wrong number of server partial sigs. Expected {expected}, got {got}")]
118 InvalidNbServerPartialSigs {
119 expected: usize,
120 got: usize
121 },
122 #[error("Invalid partial signature at index {index}")]
123 InvalidPartialSignature {
124 index: usize,
125 },
126 #[error("Wrong number of packages. Expected {expected}, got {got}")]
127 InvalidNbPackages {
128 expected: usize,
129 got: usize,
130 },
131 #[error("Wrong number of keypairs. Expected {expected}, got {got}")]
132 InvalidNbKeypairs {
133 expected: usize,
134 got: usize,
135 },
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct CosignResponse {
140 pub server_pub_nonces: Vec<musig::PublicNonce>,
141 pub server_partial_sigs: Vec<musig::PartialSignature>,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct CosignRequest<'a> {
146 pub user_pub_nonces: Vec<musig::PublicNonce>,
147 pub input: std::borrow::Cow<'a, Vtxo>,
148 pub outputs: Vec<VtxoRequest>,
149}
150
151pub mod state {
152 mod sealed {
160 pub trait Sealed {}
161 impl Sealed for super::Initial {}
162 impl Sealed for super::UserGeneratedNonces {}
163 impl Sealed for super::UserSigned {}
164 impl Sealed for super::ServerCanCosign {}
165 impl Sealed for super::ServerSigned {}
166 }
167
168 pub trait BuilderState: sealed::Sealed {}
169
170 pub struct Initial;
172 impl BuilderState for Initial {}
173
174 pub struct UserGeneratedNonces;
176 impl BuilderState for UserGeneratedNonces {}
177
178 pub struct UserSigned;
180 impl BuilderState for UserSigned {}
181
182 pub struct ServerCanCosign;
184 impl BuilderState for ServerCanCosign {}
185
186
187 pub struct ServerSigned;
189 impl BuilderState for ServerSigned {}
190}
191
192pub struct CheckpointedArkoorBuilder<'a, S: state::BuilderState> {
193 input: &'a Vtxo,
196 outputs: Vec<VtxoRequest>,
198
199 unsigned_checkpoint_tx: Transaction,
202 unsigned_arkoor_txs: Vec<Transaction>,
204 sighashes: Vec<TapSighash>,
206 checkpoint_taptweak: TapTweakHash,
208 arkoor_taptweak: TapTweakHash,
210 new_vtxo_ids: Vec<VtxoId>,
212
213 user_pub_nonces: Option<Vec<musig::PublicNonce>>,
218 user_sec_nonces: Option<Vec<musig::SecretNonce>>,
220 server_pub_nonces: Option<Vec<musig::PublicNonce>>,
222 server_partial_sigs: Option<Vec<musig::PartialSignature>>,
224 full_signatures: Option<Vec<schnorr::Signature>>,
226
227 _state: PhantomData<S>,
228}
229
230impl<'a, S: state::BuilderState> CheckpointedArkoorBuilder<'a, S> {
231
232 fn vtxo_at(
233 &self,
234 output_idx: usize,
235 checkpoint_sig: Option<schnorr::Signature>,
236 arkoor_sig: Option<schnorr::Signature>,
237 ) -> Vtxo {
238 let output = &self.outputs[output_idx];
239 let checkpoint_policy = VtxoPolicy::new_checkpoint(self.input.user_pubkey());
240
241 Vtxo {
242 amount: output.amount,
243 policy: output.policy.clone(),
244 expiry_height: self.input.expiry_height,
245 server_pubkey: self.input.server_pubkey,
246 exit_delta: self.input.exit_delta,
247 anchor_point: self.input.anchor_point,
248 genesis: self.input.genesis.iter().cloned().chain([
249 GenesisItem {
250 transition: GenesisTransition::Arkoor { policy: self.input.policy.clone(), signature: checkpoint_sig },
251 output_idx: output_idx as u8,
252 other_outputs: self.unsigned_checkpoint_tx.output
253 .iter().enumerate()
254 .filter_map(|(iii, txout)| if iii == (output_idx as usize) || txout.is_p2a_fee_anchor() { None } else { Some(txout.clone()) })
255 .collect(),
256 },
257 GenesisItem {
258 transition: GenesisTransition::Arkoor { policy: checkpoint_policy, signature: arkoor_sig },
259 output_idx: 0,
260 other_outputs: vec![]
261 }
262 ]).collect(),
263 point: self.new_vtxo_ids[output_idx].utxo()
264 }
265 }
266
267
268 fn nb_sigs(&self) -> usize {
269 self.outputs.len() + 1
270 }
271
272 fn nb_outputs(&self) -> usize {
273 self.outputs.len()
274 }
275
276 pub fn build_unsigned_vtxos(&self) -> Vec<Vtxo> {
278 (0..self.nb_outputs()).map(|i| self.vtxo_at(i, None, None)).collect()
279 }
280
281 fn taptweak_at(&self, idx: usize) -> TapTweakHash {
282 if idx == 0 {
283 self.checkpoint_taptweak
284 }
285 else {
286 self.arkoor_taptweak
287 }
288
289 }
290
291 fn user_pubkey(&self) -> PublicKey {
292 self.input.user_pubkey()
293 }
294
295 fn server_pubkey(&self) -> PublicKey {
296 self.input.server_pubkey()
297 }
298
299 fn construct_unsigned_checkpoint_tx(
300 input: &Vtxo,
301 outputs: &[VtxoRequest],
302 ) -> Transaction {
303 let output_policy = VtxoPolicy::new_checkpoint(input.user_pubkey());
305 let checkpoint_spk = output_policy.script_pubkey(input.server_pubkey(), input.exit_delta(), input.expiry_height());
306
307 Transaction {
308 version: bitcoin::transaction::Version(3),
309 lock_time: bitcoin::absolute::LockTime::ZERO,
310 input: vec![TxIn {
311 previous_output: input.point(),
312 script_sig: ScriptBuf::new(),
313 sequence: Sequence::ZERO,
314 witness: Witness::new(),
315 }],
316 output: outputs.iter().map(|o| {
317 TxOut {
318 value: o.amount,
319 script_pubkey: checkpoint_spk.clone(),
320 }
321 }).chain(Some(fee::fee_anchor())).collect()
322 }
323 }
324
325 fn construct_unsigned_arkoor_txs(
326 input: &Vtxo,
327 outputs: &[VtxoRequest],
328 checkpoint_txid: Txid,
329 ) -> Vec<Transaction> {
330 let mut arkoor_txs = Vec::with_capacity(outputs.len());
331
332 for (vout, output) in outputs.iter().enumerate() {
333 let transaction = Transaction {
334 version: bitcoin::transaction::Version(3),
335 lock_time: bitcoin::absolute::LockTime::ZERO,
336 input: vec![TxIn {
337 previous_output: OutPoint::new(checkpoint_txid, vout as u32),
338 script_sig: ScriptBuf::new(),
339 sequence: Sequence::ZERO,
340 witness: Witness::new(),
341 }],
342 output: vec![
343 output.policy.txout(output.amount, input.server_pubkey(), input.exit_delta(), input.expiry_height()),
344 fee::fee_anchor(),
345 ]
346 };
347 arkoor_txs.push(transaction);
348 }
349
350 arkoor_txs
351 }
352
353 fn validate_amounts(input: &Vtxo, outputs: &[VtxoRequest]) -> Result<(), ArkoorConstructionError> {
354 let input_amount = input.amount();
359 let output_amount = outputs.iter().map(|o| o.amount).sum::<Amount>();
360 if input_amount != output_amount {
361 return Err(ArkoorConstructionError::Unbalanced {
362 input: input_amount,
363 output: output_amount,
364 })
365 }
366
367 if outputs.iter().any(|o| o.amount < P2TR_DUST) {
369 return Err(ArkoorConstructionError::Dust)
370 }
371
372 Ok(())
373 }
374
375
376 fn to_state<S2: state::BuilderState>(self) -> CheckpointedArkoorBuilder<'a, S2> {
377 CheckpointedArkoorBuilder {
378 input: self.input,
379 outputs: self.outputs,
380 unsigned_checkpoint_tx: self.unsigned_checkpoint_tx,
381 unsigned_arkoor_txs: self.unsigned_arkoor_txs,
382 new_vtxo_ids: self.new_vtxo_ids,
383 sighashes: self.sighashes,
384 checkpoint_taptweak: self.checkpoint_taptweak,
385 arkoor_taptweak: self.arkoor_taptweak,
386 user_pub_nonces: self.user_pub_nonces,
387 user_sec_nonces: self.user_sec_nonces,
388 server_pub_nonces: self.server_pub_nonces,
389 server_partial_sigs: self.server_partial_sigs,
390 full_signatures: self.full_signatures,
391 _state: PhantomData,
392 }
393 }
394}
395
396impl<'a> CheckpointedArkoorBuilder<'a, state::Initial> {
397
398 pub fn new(input: &'a Vtxo, outputs: Vec<VtxoRequest>) -> Result<Self, ArkoorConstructionError> {
400 Self::validate_amounts(input, &outputs)?;
402
403 let unsigned_checkpoint_tx = Self::construct_unsigned_checkpoint_tx(input, &outputs);
405 let unsigned_arkoor_txs = Self::construct_unsigned_arkoor_txs(input, &outputs, unsigned_checkpoint_tx.compute_txid());
406
407 let new_vtxo_ids = unsigned_arkoor_txs.iter()
409 .map(|tx| OutPoint::new(tx.compute_txid(), 0))
410 .map(|outpoint| VtxoId::from(outpoint))
411 .collect();
412
413 let mut sighashes = Vec::with_capacity(outputs.len() + 1);
415 sighashes.push(arkoor_sighash(&input.txout(), &unsigned_checkpoint_tx));
416 for vout in 0..outputs.len() {
417 let prevout = unsigned_checkpoint_tx.output[vout].clone();
418 sighashes.push(arkoor_sighash(&prevout, &unsigned_arkoor_txs[vout]));
419 }
420
421 let checkpoint_taptweak = input.output_taproot().tap_tweak();
423 let policy = VtxoPolicy::new_checkpoint(input.user_pubkey());
424 let arkoor_taptweak = policy.taproot(input.server_pubkey(), input.exit_delta(), input.expiry_height()).tap_tweak();
425
426 Ok(Self {
427 input: input,
428 outputs: outputs,
429 sighashes: sighashes,
430 checkpoint_taptweak: checkpoint_taptweak,
431 arkoor_taptweak: arkoor_taptweak,
432 unsigned_checkpoint_tx: unsigned_checkpoint_tx,
433 unsigned_arkoor_txs: unsigned_arkoor_txs,
434 new_vtxo_ids: new_vtxo_ids,
435 user_pub_nonces: None,
436 user_sec_nonces: None,
437 server_pub_nonces: None,
438 server_partial_sigs: None,
439 full_signatures: None,
440 _state: PhantomData,
441 })
442 }
443
444 pub fn generate_user_nonces(mut self, user_keypair: Keypair) -> CheckpointedArkoorBuilder<'a, state::UserGeneratedNonces> {
447 let mut user_pub_nonces = Vec::with_capacity(self.nb_sigs());
448 let mut user_sec_nonces = Vec::with_capacity(self.nb_sigs());
449
450 for idx in 0..self.nb_sigs() {
451 let sighash = &self.sighashes[idx].to_byte_array();
452 let (sec_nonce, pub_nonce) = musig::nonce_pair_with_msg(&user_keypair, sighash);
453
454 user_pub_nonces.push(pub_nonce);
455 user_sec_nonces.push(sec_nonce);
456 }
457
458 self.user_pub_nonces = Some(user_pub_nonces);
459 self.user_sec_nonces = Some(user_sec_nonces);
460
461 self.to_state::<state::UserGeneratedNonces>()
462 }
463
464 fn set_user_pub_nonces(mut self, user_pub_nonces: Vec<musig::PublicNonce>) -> Result<CheckpointedArkoorBuilder<'a, state::ServerCanCosign>, ArkoorSigningError> {
470 if user_pub_nonces.len() != self.nb_sigs() {
471 return Err(ArkoorSigningError::InvalidNbUserNonces {
472 expected: self.nb_sigs(),
473 got: user_pub_nonces.len()
474 })
475 }
476
477 self.user_pub_nonces = Some(user_pub_nonces);
478 Ok(self.to_state::<state::ServerCanCosign>())
479 }
480}
481
482impl<'a> CheckpointedArkoorBuilder<'a, state::ServerCanCosign> {
483
484 pub fn from_cosign_request(cosign_request: &'a CosignRequest) -> Result<CheckpointedArkoorBuilder<'a, state::ServerCanCosign>, ArkoorSigningError> {
485 CheckpointedArkoorBuilder::new(
486 &cosign_request.input,
487 cosign_request.outputs.clone())
488 .map_err(ArkoorSigningError::ArkoorConstructionError)?
489 .set_user_pub_nonces(cosign_request.user_pub_nonces.clone())
490
491 }
492
493 pub fn server_cosign(mut self, server_keypair: Keypair) -> Result<CheckpointedArkoorBuilder<'a, state::ServerSigned>, ArkoorSigningError> {
494 if server_keypair.public_key() != self.input.server_pubkey() {
496 return Err(ArkoorSigningError::IncorrectKey {
497 expected: self.input.server_pubkey(),
498 got: server_keypair.public_key(),
499 });
500 }
501
502 let mut server_pub_nonces = Vec::with_capacity(self.outputs.len() + 1);
503 let mut server_partial_sigs = Vec::with_capacity(self.outputs.len() + 1);
504
505 for idx in 0..self.nb_sigs() {
506 let (server_pub_nonce, server_partial_sig) = musig::deterministic_partial_sign(
507 &server_keypair,
508 [self.input.user_pubkey()],
509 &[&self.user_pub_nonces.as_ref().expect("state-invariant")[idx]],
510 self.sighashes[idx].to_byte_array(),
511 Some(self.taptweak_at(idx).to_byte_array()),
512 );
513
514 server_pub_nonces.push(server_pub_nonce);
515 server_partial_sigs.push(server_partial_sig);
516 };
517
518 self.server_pub_nonces = Some(server_pub_nonces);
519 self.server_partial_sigs = Some(server_partial_sigs);
520 Ok(self.to_state::<state::ServerSigned>())
521 }
522
523}
524
525impl<'a> CheckpointedArkoorBuilder<'a, state::ServerSigned> {
526
527 pub fn user_pub_nonces(&self) -> Vec<musig::PublicNonce> {
528 self.user_pub_nonces.as_ref().expect("state invariant").clone()
529 }
530
531 pub fn server_partial_signatures(&self) -> Vec<musig::PartialSignature> {
532 self.server_partial_sigs.as_ref().expect("state invariant").clone()
533 }
534
535 pub fn cosign_response(&self) -> CosignResponse {
536 CosignResponse {
537 server_pub_nonces: self.server_pub_nonces.as_ref().expect("state invariant").clone(),
538 server_partial_sigs: self.server_partial_sigs.as_ref().expect("state invariant").clone(),
539 }
540 }
541}
542
543impl<'a> CheckpointedArkoorBuilder<'a, state::UserGeneratedNonces> {
544
545 pub fn user_pub_nonces(&self) -> &[PublicNonce] {
546 self.user_pub_nonces.as_ref().expect("State invariant")
547 }
548
549 pub fn cosign_request(&'a self) -> CosignRequest<'a> {
550 CosignRequest {
551 user_pub_nonces: self.user_pub_nonces.as_ref().expect("state invariant").clone(),
552 input: Cow::Borrowed(self.input),
553 outputs: self.outputs.clone(),
554 }
555 }
556
557 fn validate_server_cosign_response(
558 &self,
559 data: &CosignResponse,
560 ) -> Result<(), ArkoorSigningError> {
561
562 if data.server_pub_nonces.len() != self.nb_sigs() {
564 return Err(ArkoorSigningError::InvalidNbServerNonces {
565 expected: self.nb_sigs(),
566 got: data.server_pub_nonces.len(),
567 });
568 }
569
570 if data.server_partial_sigs.len() != self.nb_sigs() {
571 return Err(ArkoorSigningError::InvalidNbServerPartialSigs {
572 expected: self.nb_sigs(),
573 got: data.server_partial_sigs.len(),
574 })
575 }
576
577 for idx in 0..self.nb_sigs() {
579 let is_valid_sig = scripts::verify_partial_sig(
580 self.sighashes[idx],
581 self.taptweak_at(idx),
582 (self.input.server_pubkey(), &data.server_pub_nonces[idx]),
583 (self.input.user_pubkey(), &self.user_pub_nonces()[idx]),
584 &data.server_partial_sigs[idx]
585 );
586
587 if !is_valid_sig {
588 return Err(ArkoorSigningError::InvalidPartialSignature {
589 index: idx,
590 });
591 }
592 }
593 Ok(())
594 }
595
596 pub fn user_cosign(
597 mut self,
598 user_keypair: &Keypair,
599 server_cosign_data: &CosignResponse,
600 ) -> Result<CheckpointedArkoorBuilder<'a, state::UserSigned>, ArkoorSigningError> {
601 if user_keypair.public_key() != self.input.user_pubkey() {
603 return Err(ArkoorSigningError::IncorrectKey {
604 expected: self.input.user_pubkey(),
605 got: user_keypair.public_key(),
606 });
607 }
608
609 self.validate_server_cosign_response(&server_cosign_data)?;
611
612 let mut sigs = Vec::with_capacity(self.nb_sigs());
613
614 let user_sec_nonces = self.user_sec_nonces.take().expect("state invariant");
617
618 for (idx, user_sec_nonce) in user_sec_nonces.into_iter().enumerate() {
619 let user_pub_nonce = self.user_pub_nonces()[idx];
620 let server_pub_nonce = server_cosign_data.server_pub_nonces[idx];
621 let agg_nonce = musig::nonce_agg(&[&user_pub_nonce, &server_pub_nonce]);
622
623 let (_partial, maybe_sig) = musig::partial_sign(
624 [self.user_pubkey(), self.server_pubkey()],
625 agg_nonce,
626 &user_keypair,
627 user_sec_nonce,
628 self.sighashes[idx].to_byte_array(),
629 Some(self.taptweak_at(idx).to_byte_array()),
630 Some(&[&server_cosign_data.server_partial_sigs[idx]])
631 );
632
633 let sig = maybe_sig.expect("The full signature exists. The server did sign first");
634 sigs.push(sig);
635 }
636
637
638 self.full_signatures = Some(sigs);
639
640 Ok(self.to_state::<state::UserSigned>())
641 }
642}
643
644
645impl<'a> CheckpointedArkoorBuilder<'a, state::UserSigned> {
646
647 pub fn build_signed_vtxos(&self) -> Vec<Vtxo> {
648 let checkpoint_sig = self.full_signatures.as_ref().expect("state invariant")[0];
649 let arkoor_sigs = &self.full_signatures.as_ref().expect("state invariant")[1..];
650
651 (0..self.nb_outputs()).map(|i| {
652 self.vtxo_at(i, Some(checkpoint_sig), Some(arkoor_sigs[i]))
653 }).collect()
654 }
655}
656
657impl<'a, S: state::BuilderState> CheckpointedArkoorBuilder<'a, S> {
658}
659
660
661
662
663#[cfg(test)]
664mod test {
665 use super::*;
666
667 use bitcoin::Amount;
668 use bitcoin::secp256k1::Keypair;
669 use bitcoin::secp256k1::rand;
670
671 use crate::SECP;
672 use crate::VtxoRequest;
673 use crate::test::dummy::DummyTestVtxoSpec;
674
675
676 #[test]
677 fn build_checkpointed_arkoor() {
678 let alice_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
679 let bob_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
680 let server_keypair = Keypair::new(&SECP, &mut rand::thread_rng());
681
682 println!("Alice keypair: {}", alice_keypair.public_key());
683 println!("Bob keypair: {}", bob_keypair.public_key());
684 println!("Server keypair: {}", server_keypair.public_key());
685 println!("-----------------------------------------------");
686
687 let (funding_tx, alice_vtxo) = DummyTestVtxoSpec {
688 amount: Amount::from_sat(100_000),
689 expiry_height: 1000,
690 exit_delta : 128,
691 user_keypair: alice_keypair.clone(),
692 server_keypair: server_keypair.clone()
693 }.build();
694
695 alice_vtxo.validate(&funding_tx).expect("The unsigned vtxo is valid");
697
698 let vtxo_request = vec![
699 VtxoRequest {
700 amount: Amount::from_sat(96_000),
701 policy: VtxoPolicy::new_pubkey(bob_keypair.public_key())
702 },
703 VtxoRequest {
704 amount: Amount::from_sat(4_000),
705 policy: VtxoPolicy::new_pubkey(alice_keypair.public_key())
706 }
707 ];
708
709 let user_builder = CheckpointedArkoorBuilder::new(
711 &alice_vtxo,
712 vtxo_request.clone(),
713 ).expect("Valid arkoor request");
714
715 let _unsigned_vtxos = user_builder.build_unsigned_vtxos();
719
720
721 let user_builder =user_builder.generate_user_nonces(alice_keypair);
723 let cosign_request = user_builder.cosign_request();
724
725 let server_builder = CheckpointedArkoorBuilder::from_cosign_request(&cosign_request).expect("Invalid cosign request")
727 .server_cosign(server_keypair).expect("Incorrect key");
728
729 let cosign_data = server_builder.cosign_response();
730
731 let vtxos = user_builder
733 .user_cosign(&alice_keypair, &cosign_data)
734 .expect("Valid cosign data and correct key")
735 .build_signed_vtxos();
736
737 for vtxo in vtxos.into_iter() {
738 vtxo.validate(&funding_tx).expect("Invalid VTXO");
740
741 let mut prev_tx = funding_tx.clone();
743 for tx in vtxo.transactions().map(|item| item.tx) {
744 let prev_outpoint: OutPoint = tx.input[0].previous_output;
745 let prev_txout: TxOut = prev_tx.output[prev_outpoint.vout as usize].clone();
746 crate::test::verify_tx(&[prev_txout], 0, &tx).expect("Valid transaction");
747 prev_tx = tx;
748 }
749 }
750
751 }
752}