ark/
arkoor.rs

1
2
3use std::borrow::{Borrow, Cow};
4use std::collections::HashMap;
5use std::iter;
6
7use bitcoin::hex::DisplayHex;
8use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Weight, Witness};
9use bitcoin::hashes::Hash;
10use bitcoin::secp256k1::{schnorr, Keypair, PublicKey};
11use bitcoin::sighash::{self, SighashCache, TapSighash, TapSighashType};
12
13use bitcoin_ext::{fee, P2TR_DUST, TAPROOT_KEYSPEND_WEIGHT};
14
15use crate::error::IncorrectSigningKeyError;
16use crate::{musig, scripts, ProtocolEncoding, Vtxo, VtxoId, VtxoPolicy, VtxoRequest, SECP};
17use crate::vtxo::{GenesisItem, GenesisTransition};
18
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
21pub enum ArkoorError {
22	#[error("output amount of {input} exceeds input amount of {output}")]
23	Unbalanced {
24		input: Amount,
25		output: Amount,
26	},
27	#[error("arkoor output amounts cannot be below the p2tr dust threshold")]
28	Dust,
29	#[error("arkoor cannot have more than 2 outputs")]
30	TooManyOutputs,
31}
32
33pub fn arkoor_sighash(input_vtxo: &Vtxo, arkoor_tx: &Transaction) -> TapSighash {
34	let prev = input_vtxo.txout();
35	let mut shc = SighashCache::new(arkoor_tx);
36
37	shc.taproot_key_spend_signature_hash(
38		0, &sighash::Prevouts::All(&[prev]), TapSighashType::Default,
39	).expect("sighash error")
40}
41
42pub fn unsigned_arkoor_tx(input: &Vtxo, outputs: &[TxOut]) -> Transaction {
43	Transaction {
44		version: bitcoin::transaction::Version(3),
45		lock_time: bitcoin::absolute::LockTime::ZERO,
46		input: vec![TxIn {
47			previous_output: input.point(),
48			script_sig: ScriptBuf::new(),
49			sequence: Sequence::ZERO,
50			witness: Witness::new(),
51		}],
52		output: outputs.into_iter().cloned().chain([fee::fee_anchor()]).collect(),
53	}
54}
55
56/// Inner utility method to construct the arkoor vtxos.
57fn build_arkoor_vtxos<T: Borrow<VtxoRequest>>(
58	input: &Vtxo,
59	outputs: &[T],
60	txouts: &[TxOut],
61	arkoor_txid: Txid,
62	arkoor_signature: Option<schnorr::Signature>,
63) -> Vec<Vtxo> {
64	outputs.iter().enumerate().map(|(idx, output)| {
65		Vtxo {
66			amount: output.borrow().amount,
67			expiry_height: input.expiry_height,
68			server_pubkey: input.server_pubkey,
69			exit_delta: input.exit_delta,
70			anchor_point: input.anchor_point,
71			genesis: input.genesis.iter().cloned().chain([GenesisItem {
72				transition: GenesisTransition::Arkoor {
73					policy: input.policy.clone(),
74					signature: arkoor_signature,
75				},
76				output_idx: idx as u8,
77				// filter out our index from the txouts
78				other_outputs: txouts.iter().enumerate()
79					.filter(|(i, _)| *i != idx)
80					.map(|(_, o)| o).cloned().collect(),
81			}]).collect(),
82			policy: output.borrow().policy.clone(),
83			point: OutPoint::new(arkoor_txid, idx as u32),
84		}
85	}).collect()
86}
87
88/// Build oor tx and signs it
89///
90/// ## Panic
91///
92/// Will panic if inputs and signatures don't have the same length,
93/// or if some input witnesses are not empty
94pub fn signed_arkoor_tx(
95	input: &Vtxo,
96	signature: schnorr::Signature,
97	outputs: &[TxOut],
98) -> Transaction {
99	let mut tx = unsigned_arkoor_tx(input, outputs);
100	scripts::fill_taproot_sigs(&mut tx, &[signature]);
101	tx
102}
103
104/// The cosignature details received from the Ark server.
105#[derive(Debug)]
106pub struct ArkoorCosignResponse {
107	pub pub_nonce: musig::PublicNonce,
108	pub partial_signature: musig::PartialSignature,
109}
110
111/// This types helps both the client and server with building arkoor txs in
112/// a synchronized way. It's purely a functional type, initialized with
113/// the parameters that will make up the arkoor: the input vtxo to be spent
114/// and the desired outputs.
115///
116/// The flow works as follows:
117/// - sender uses the constructor to check the request for validity
118/// - server uses the contructor to check the request for validity
119/// - server uses [ArkoorBuilder::server_cosign] to construct a
120///   [ArkoorCosignResponse] to send back to the sender
121/// - sender passes the response into [ArkoorBuilder::build_vtxos] to construct
122///   the signed resulting VTXOs
123pub struct ArkoorBuilder<'a, T: Clone> {
124	pub input: &'a Vtxo,
125	pub user_nonce: &'a musig::PublicNonce,
126	pub outputs: Cow<'a, [T]>,
127}
128
129impl<'a, T: Borrow<VtxoRequest> + Clone> ArkoorBuilder<'a, T> {
130	/// Construct a generic arkoor builder for the given input and outputs.
131	pub fn new(
132		input: &'a Vtxo,
133		user_nonce: &'a musig::PublicNonce,
134		outputs: impl Into<Cow<'a, [T]>>,
135	) -> Result<Self, ArkoorError> {
136		let outputs = outputs.into();
137		if outputs.iter().any(|o| o.borrow().amount < P2TR_DUST) {
138			return Err(ArkoorError::Dust);
139		}
140		let output_amount = outputs.as_ref().iter().map(|o| o.borrow().amount).sum::<Amount>();
141		if output_amount > input.amount() {
142			return Err(ArkoorError::Unbalanced {
143				input: input.amount(),
144				output: output_amount,
145			});
146		}
147
148		if outputs.len() > 2 {
149			return Err(ArkoorError::TooManyOutputs);
150		}
151
152		Ok(Self {
153			input,
154			user_nonce,
155			outputs,
156		})
157	}
158
159	/// Construct the transaction outputs of the resulting arkoor tx.
160	pub fn txouts(&self) -> Vec<TxOut> {
161		self.outputs.iter().map(|out| {
162			out.borrow().policy.txout(
163				out.borrow().amount,
164				self.input.server_pubkey(),
165				self.input.exit_delta(),
166			)
167		}).collect()
168	}
169
170	pub fn unsigned_transaction(&self) -> Transaction {
171		unsigned_arkoor_tx(&self.input, &self.txouts())
172	}
173
174	pub fn sighash(&self) -> TapSighash {
175		arkoor_sighash(&self.input, &self.unsigned_transaction())
176	}
177
178	pub fn total_weight(&self) -> Weight {
179		let spend_weight = Weight::from_wu(TAPROOT_KEYSPEND_WEIGHT as u64);
180		self.unsigned_transaction().weight() + spend_weight
181	}
182
183	/// Used by the Ark server to cosign the arkoor request.
184	pub fn server_cosign(&self, keypair: &Keypair) -> ArkoorCosignResponse {
185		let (pub_nonce, partial_signature) = musig::deterministic_partial_sign(
186			keypair,
187			[self.input.user_pubkey()],
188			&[&self.user_nonce],
189			self.sighash().to_byte_array(),
190			Some(self.input.output_taproot().tap_tweak().to_byte_array()),
191		);
192		ArkoorCosignResponse { pub_nonce, partial_signature }
193	}
194
195	/// Validate the server's partial signature.
196	pub fn verify_cosign_response(
197		&self,
198		server_cosign: &ArkoorCosignResponse,
199	) -> bool {
200		scripts::verify_partial_sig(
201			self.sighash(),
202			self.input.output_taproot().tap_tweak(),
203			(self.input.server_pubkey(), &server_cosign.pub_nonce),
204			(self.input.user_pubkey(), &self.user_nonce),
205			&server_cosign.partial_signature,
206		)
207	}
208
209	/// Construct the vtxos of the outputs of this OOR tx.
210	///
211	/// These vtxos are not valid vtxos because they lack the signature.
212	pub fn unsigned_output_vtxos(&self) -> Vec<Vtxo> {
213		let txouts = self.txouts();
214		let tx = unsigned_arkoor_tx(&self.input, &txouts);
215		build_arkoor_vtxos(&self.input, self.outputs.as_ref(), &txouts, tx.compute_txid(), None)
216	}
217
218	/// Finish the arkoor process.
219	///
220	/// Returns the resulting vtxos and the signed arkoor tx.
221	pub fn build_vtxos(
222		&self,
223		user_sec_nonce: musig::SecretNonce,
224		user_key: &Keypair,
225		cosign_resp: &ArkoorCosignResponse,
226	) -> Result<Vec<Vtxo>, IncorrectSigningKeyError> {
227		if user_key.public_key() != self.input.user_pubkey() {
228			return Err(IncorrectSigningKeyError {
229				required: Some(self.input.user_pubkey()),
230				provided: user_key.public_key(),
231			});
232		}
233
234		let txouts = self.txouts();
235		let tx = unsigned_arkoor_tx(&self.input, &txouts);
236		let sighash = arkoor_sighash(&self.input, &tx);
237		let taptweak = self.input.output_taproot().tap_tweak();
238
239		let agg_nonce = musig::nonce_agg(&[&self.user_nonce, &cosign_resp.pub_nonce]);
240		let (_part_sig, final_sig) = musig::partial_sign(
241			[self.input.user_pubkey(), self.input.server_pubkey()],
242			agg_nonce,
243			user_key,
244			user_sec_nonce,
245			sighash.to_byte_array(),
246			Some(taptweak.to_byte_array()),
247			Some(&[&cosign_resp.partial_signature]),
248		);
249		let final_sig = final_sig.expect("we provided the other sig");
250		debug_assert!(
251			scripts::verify_partial_sig(
252				sighash,
253				taptweak,
254				(self.input.user_pubkey(), &self.user_nonce),
255				(self.input.server_pubkey(), &cosign_resp.pub_nonce),
256				&_part_sig,
257			),
258			"invalid partial signature produced",
259		);
260		debug_assert!(
261			SECP.verify_schnorr(
262				&final_sig,
263				&sighash.into(),
264				&self.input.output_taproot().output_key().to_x_only_public_key(),
265			).is_ok(),
266			"invalid arkoor tx signature produced: input={}, outputs={:?}",
267			self.input.serialize().as_hex(), &txouts,
268		);
269
270		Ok(build_arkoor_vtxos(
271			&self.input,
272			self.outputs.as_ref(),
273			&txouts,
274			tx.compute_txid(),
275			Some(final_sig),
276		))
277	}
278}
279
280/// This type helps both the client and server with building multiple arkoor transactions
281/// in a synchronized way. It's purely a functional type, initialized with
282/// the parameters that will make up the arkoor package: the input vtxos to be spent
283/// and the desired payment request with optional change.
284///
285/// The flow works as follows:
286/// - sender uses the constructor to check the payment request for validity
287/// - server uses the constructor to check the payment request for validity
288/// - server uses [ArkoorPackageBuilder::server_cosign] to construct a vector of
289///   [ArkoorCosignResponse] to send back to the sender
290/// - sender passes the responses into [ArkoorPackageBuilder::build_vtxos] to construct
291///   the signed resulting VTXOs and optional change VTXO
292///
293/// The package can handle multiple input VTXOs to fulfill a single payment request,
294/// automatically creating change outputs when necessary.
295pub struct ArkoorPackageBuilder<'a, T: Clone> {
296	/// Each transition from one input VTXO to one or two output VTXOs
297	pub arkoors: Vec<ArkoorBuilder<'a, T>>,
298	spending_tx_by_input: HashMap<VtxoId, Transaction>,
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
302pub enum ArkoorPackageError {
303	#[error("Payment has non-null change amount but no change pubkey provided")]
304	MissingChangePk,
305	#[error("Invalid length of cosignature response")]
306	InvalidLength,
307	#[error("No vtxo created")]
308	MissingVtxo,
309	#[error("Invalid spk for revocation")]
310	InvalidRevocationSpk,
311	#[error("Invalid length of user nonces")]
312	InvalidUserNoncesLength,
313	#[error("Htlc amount does not match invoice amount")]
314	InvalidHtlcAmount,
315	#[error("An error occurred while building arkoor: {0}")]
316	ArkoorError(ArkoorError),
317	#[error("Too many outputs")]
318	TooManyOutputs,
319	#[error("incorrect signing key provided")]
320	Signing(#[from] IncorrectSigningKeyError),
321}
322
323impl<'a> ArkoorPackageBuilder<'a, VtxoRequest> {
324	pub fn new(
325		inputs: impl IntoIterator<Item = &'a Vtxo>,
326		user_nonces: &'a [musig::PublicNonce],
327		vtxo_request: VtxoRequest,
328		change_pubkey: Option<PublicKey>,
329	) -> Result<Self, ArkoorPackageError> {
330		let mut remaining_amount = vtxo_request.amount;
331		let mut arkoors = vec![];
332		let mut spending_tx_by_input = HashMap::new();
333
334		for (idx, input) in inputs.into_iter().enumerate() {
335			let user_nonce = user_nonces.get(idx).ok_or(ArkoorPackageError::InvalidUserNoncesLength)?;
336
337			let change_amount = input.amount().checked_sub(remaining_amount);
338			let (output_amount, change) = if let Some(change_amount) = change_amount {
339				// NB: If change amount is less than the dust amount, we don't add any change output
340				let change = if change_amount < P2TR_DUST {
341					None
342				} else {
343					Some(VtxoRequest {
344						amount: change_amount,
345						policy: VtxoPolicy::new_pubkey(change_pubkey.ok_or(ArkoorPackageError::MissingChangePk)?),
346					})
347				};
348
349				(remaining_amount, change)
350			} else {
351				(input.amount(), None)
352			};
353
354			let output = VtxoRequest {
355				amount: output_amount,
356				policy: vtxo_request.policy.clone(),
357			};
358
359			let pay_reqs = iter::once(output.clone()).chain(change).collect::<Vec<_>>();
360
361			let arkoor = ArkoorBuilder::new(&input, user_nonce, pay_reqs)
362				.map_err(ArkoorPackageError::ArkoorError)?;
363
364			spending_tx_by_input.insert(input.id(), arkoor.unsigned_transaction());
365			arkoors.push(arkoor);
366
367			remaining_amount = remaining_amount - output_amount;
368			if remaining_amount == Amount::ZERO {
369				break;
370			}
371		}
372
373		Ok(Self {
374			arkoors,
375			spending_tx_by_input,
376		})
377	}
378
379	pub fn new_htlc_revocation(
380		htlc_vtxos: &'a [Vtxo],
381		user_nonces: &'a [musig::PublicNonce],
382	) -> Result<Self, ArkoorPackageError> {
383		let arkoors = htlc_vtxos.iter().zip(user_nonces).map(|(v, u)| {
384			if !matches!(v.policy(), VtxoPolicy::ServerHtlcSend { .. }) {
385				return Err(ArkoorPackageError::InvalidRevocationSpk);
386			}
387
388			let refund = VtxoRequest {
389				amount: v.amount(),
390				policy: VtxoPolicy::new_pubkey(v.user_pubkey()),
391			};
392			ArkoorBuilder::new(v, u, vec![refund])
393				.map_err(ArkoorPackageError::ArkoorError)
394		}).collect::<Result<Vec<_>, ArkoorPackageError>>()?;
395
396		Self::from_arkoors(arkoors)
397	}
398
399	pub fn from_arkoors(
400		arkoors: Vec<ArkoorBuilder<'a, VtxoRequest>>,
401	) -> Result<Self, ArkoorPackageError> {
402		let mut spending_tx_by_input = HashMap::new();
403
404		for arkoor in arkoors.iter() {
405			spending_tx_by_input.insert(arkoor.input.id(), arkoor.unsigned_transaction());
406		}
407
408		Ok(Self {
409			arkoors,
410			spending_tx_by_input,
411		})
412	}
413
414	pub fn inputs(&self) -> Vec<&'a Vtxo> {
415		self.arkoors.iter().map(|a| a.input).collect::<Vec<_>>()
416	}
417
418	pub fn spending_tx(&self, input_id: VtxoId) -> Option<&Transaction> {
419		self.spending_tx_by_input.get(&input_id)
420	}
421
422	pub fn build_vtxos<'b>(
423		self,
424		sigs: impl IntoIterator<Item = &'a ArkoorCosignResponse>,
425		keypairs: impl IntoIterator<Item = &'a Keypair>,
426		sec_nonces: impl IntoIterator<Item = musig::SecretNonce>,
427	) -> Result<(Vec<Vtxo>, Option<Vtxo>), ArkoorPackageError> {
428		let mut sent_vtxos = vec![];
429		let mut change_vtxo = None;
430
431		let expected_len = self.arkoors.len();
432
433		let iter = self.arkoors.into_iter().zip(sigs).zip(keypairs).zip(sec_nonces);
434		for (((arkoor, cosign), keypair), sec_nonce) in iter {
435			let vtxos = arkoor.build_vtxos(sec_nonce, keypair, cosign)?;
436
437			// The first one is of the recipient, we will post it to their mailbox.
438			let mut vtxo_iter = vtxos.into_iter();
439			let user_vtxo = vtxo_iter.next().ok_or(ArkoorPackageError::MissingVtxo)?;
440			sent_vtxos.push(user_vtxo);
441
442			if let Some(vtxo) = vtxo_iter.next() {
443				assert!(change_vtxo.replace(vtxo).is_none(), "change vtxo already set");
444			}
445		}
446
447		if sent_vtxos.len() != expected_len {
448			return Err(ArkoorPackageError::InvalidLength);
449		}
450
451		Ok((sent_vtxos, change_vtxo))
452	}
453
454	pub fn new_vtxos(&self) -> Vec<Vec<Vtxo>> {
455		self.arkoors.iter().map(|arkoor| {
456			let txouts = arkoor.txouts();
457			let tx = unsigned_arkoor_tx(&arkoor.input, &txouts);
458			build_arkoor_vtxos(&arkoor.input, &arkoor.outputs, &txouts, tx.compute_txid(), None) //TODO(stevenroose) signaature
459		}).collect::<Vec<Vec<_>>>()
460	}
461
462	/// Used by the Ark server to cosign the arkoor request.
463	pub fn server_cosign(&self, keypair: &Keypair) -> Vec<ArkoorCosignResponse> {
464		let mut cosign = vec![];
465
466		for arkoor in self.arkoors.iter() {
467			cosign.push(arkoor.server_cosign(keypair));
468		}
469
470		cosign
471	}
472
473	pub fn verify_cosign_response<T: Borrow<ArkoorCosignResponse>>(
474		&self,
475		server_cosign: &[T],
476	) -> bool {
477		for (idx, builder) in self.arkoors.iter().enumerate() {
478			if let Some(cosign) = server_cosign.get(idx) {
479				if !builder.verify_cosign_response(cosign.borrow()) {
480					return false;
481				}
482			} else {
483				return false;
484			}
485		}
486		true
487	}
488}
489