ark/arkoor/
checkpoint.rs

1//! Utilities to create out-of-round transactions using
2//! checkpoint transactions.
3//!
4//! # Checkpoints keep users and the server safe
5//!
6//! When an Ark transaction is spent out-of-round a new
7//! transaction is added on top of that. In the naive
8//! approach we just keep adding transactions and the
9//! chain becomes longer.
10//!
11//! A first problem is that this can become unsafe for the server.
12//! If a client performs a partial exit attack the server
13//! will have to broadcast a long chain of transactions
14//! to get the forfeit published.
15//!
16//! A second problem is that if one user exits it affects everyone.
17//! In their chunk of the tree. The server cannot sweep the funds
18//! anymore and all other users are forced to collect their funds
19//! from the chain (which can be expensive).
20//!
21//! # How do they work
22//!
23//! The core idea is that each out-of-round spent will go through
24//! a checkpoint transaction. The checkpoint transaction has the policy
25//! `A + S or S after expiry`.
26//!
27//! Note, that the `A+S` path is fast and will always take priority.
28//! Users will still be able to exit their funds at any time.
29//! But if a partial exit occurs, the server can just broadcast
30//! a single checkpoint transaction and continue like nothing happened.
31//!
32//! Other users will be fully unaffected by this. Their [Vtxo] will now
33//! be anchored in the checkpoint which can be swept after expiry.
34//!
35//! # Usage
36//!
37//! This module creates a checkpoint transaction that originates
38//! from a single [Vtxo]. It is a low-level construct and the developer
39//! has to compute the paid amount, change and fees themselves.
40//!
41//! The core construct is [CheckpointedArkoorBuilder] which can be
42//! used to build arkoor transactions. The struct is designed to be
43//! used by both the client and the server.
44//!
45//! [CheckpointedArkoorBuilder::new]  is a constructor that validates
46//! the intended transaction. At this point, all transactions that
47//! will be constructed are fully designed. You can
48//! use [CheckpointedArkoorBuilder::build_unsigned_vtxos] to construct the
49//! vtxos but they will still lack signatures.
50//!
51//! Constructing the signatures is an interactive process in which the
52//! client signs first.
53//!
54//! The client will call [CheckpointedArkoorBuilder::generate_user_nonces]
55//! which will update the builder-state to  [state::UserGeneratedNonces].
56//! The client will create a [CosignRequest] which contains the details
57//! about the arkoor payment including the user nonces. The server will
58//! respond with a [CosignResponse] which can be used to finalize all
59//! signatures. At the end the client can call [CheckpointedArkoorBuilder::build_signed_vtxos]
60//! to get their fully signed VTXOs.
61//!
62//! The server will also use [CheckpointedArkoorBuilder::from_cosign_request]
63//! to construct a builder. The [CheckpointedArkoorBuilder::server_cosign]
64//! will construct the [CosignResponse] which is sent to the client.
65//!
66
67use 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	/// There are two paths that a can be followed
153	///
154	/// 1. [Initial] -> [UserGeneratedNonces] -> [UserSigned]
155	/// 2. [Initial] -> [ServerCanCosign] -> [ServerSigned]
156	///
157	/// The first option is taken by the user and the second by the server
158
159	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	// The initial state of the builder
171	pub struct Initial;
172	impl BuilderState for Initial {}
173
174	// The user has generated their nonces
175	pub struct UserGeneratedNonces;
176	impl BuilderState for UserGeneratedNonces {}
177
178	// The user can sign
179	pub struct UserSigned;
180	impl BuilderState for UserSigned {}
181
182	// The server can cosign
183	pub struct ServerCanCosign;
184	impl BuilderState for ServerCanCosign {}
185
186
187	/// The server has signed and knows the partial signatures
188	pub struct ServerSigned;
189	impl BuilderState for ServerSigned {}
190}
191
192pub struct CheckpointedArkoorBuilder<'a, S: state::BuilderState> {
193	// These variables are provided by the user
194	/// The input vtxo to be spent
195	input: &'a Vtxo,
196	/// `n` [VtxoRequest]s that the user wants to receive
197	outputs: Vec<VtxoRequest>,
198
199	// These can be computed in the constructor
200	/// The unsigned checkpoint transaction
201	unsigned_checkpoint_tx: Transaction,
202	/// The unsigned arkoor transactions
203	unsigned_arkoor_txs: Vec<Transaction>,
204	/// The sighashes that must be signed
205	sighashes: Vec<TapSighash>,
206	/// The taptweak to sign the checkpoint tx
207	checkpoint_taptweak: TapTweakHash,
208	/// The taptweak to sign the arkoor tx
209	arkoor_taptweak: TapTweakHash,
210	/// The [VtxoId]s of all new [Vtxo]s that will be created
211	new_vtxo_ids: Vec<VtxoId>,
212
213	//  These variables are filled in when the state progresses
214	/// We need 1 signature for the checkpoint transaction
215	/// We need n signatures. This is one for each arkoor tx
216	/// `1+n` public nonces created by the user
217	user_pub_nonces: Option<Vec<musig::PublicNonce>>,
218	/// `1+n` secret nonces created by the user
219	user_sec_nonces: Option<Vec<musig::SecretNonce>>,
220	/// `1+n` public nonces created by the server
221	server_pub_nonces: Option<Vec<musig::PublicNonce>>,
222	/// `1+n` partial signatures created by the server
223	server_partial_sigs: Option<Vec<musig::PartialSignature>>,
224	/// `1+n` signatures that are signed by the user and server
225	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	/// Construct all unsigned vtxos that will be created by this builder.
277	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		// All outputs on the checkpoint transaction will use exactly the same policy.
304		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		// Check if inputs and outputs are balanced
355		// We need to build transactions that pay exactly 0 in onchain fees
356		// to ensure our transaction with an ephemeral anchor is standard.
357		// We need `==` for standardness and we can't be lenient
358		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		// Check if we have any subdust outputs
368		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	/// Create a new checkpointed arkoor builder
399	pub fn new(input: &'a Vtxo, outputs: Vec<VtxoRequest>) -> Result<Self, ArkoorConstructionError> {
400		// Do some validation on the amounts
401		Self::validate_amounts(input, &outputs)?;
402
403		// Construct the checkpoint and arkoor transactions
404		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		// Compute all vtx-ids
408		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		// Compute all sighashes
414		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		// For the checkpoint
422		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	/// Generates the user nonces and moves the builder to the [state::UserGeneratedNonces] state
445	/// This is the path that is used by the user
446	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	/// Sets the pub nonces that a user has generated.
465	/// When this has happened the server can cosign.
466	///
467	/// If you are implementing a client, use [Self::generate_user_nonces] instead.
468	/// If you are implementing a server you should look at [CheckpointedArkoorBuilder::from_cosign_request]
469	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		// Verify that the provided keypair is correct
495		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		// Check if the correct number of nonces is provided
563		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		// Check if the partial signatures is valid
578		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		// Verify that the correct user keypair is provided
602		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		// Verify that the server cosign data is valid
610		self.validate_server_cosign_response(&server_cosign_data)?;
611
612		let mut sigs = Vec::with_capacity(self.nb_sigs());
613
614		// Takes the secret nonces out of the [CheckpointedArkoorBuilder].
615		// Note, that we can't clone nonces so we can only sign once
616		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		// Validate Alice her vtxo
696		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		// The user generates their nonces
710		let user_builder = CheckpointedArkoorBuilder::new(
711			&alice_vtxo,
712			vtxo_request.clone(),
713		).expect("Valid arkoor request");
714
715		// At this point all out-of-round transactions are fully defined.
716		// They are just missing the required signatures.
717		// We are already able to compute the vtxos and validate them
718		let _unsigned_vtxos = user_builder.build_unsigned_vtxos();
719
720
721		// The user generates their nonces
722		let user_builder =user_builder.generate_user_nonces(alice_keypair);
723		let cosign_request = user_builder.cosign_request();
724
725		// The server will cosign the request
726		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		// The user will cosign the request and construct their vtxos
732		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			// Check if the vtxo is considered valid
739			vtxo.validate(&funding_tx).expect("Invalid VTXO");
740
741			// Check all transactions using libbitcoin-kernel
742			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}