Skip to main content

ark/vtxo/
genesis.rs

1use std::fmt;
2
3use bitcoin::secp256k1::{schnorr, PublicKey};
4use bitcoin::{Amount, OutPoint, Sequence, ScriptBuf, Transaction, TxIn, TxOut, Witness};
5use bitcoin::hashes::{sha256, Hash};
6use bitcoin::key::TweakedPublicKey;
7use bitcoin::sighash;
8use bitcoin::taproot::{self, TapLeafHash, LeafVersion, TapTweakHash};
9
10use bitcoin_ext::{fee, BlockDelta, BlockHeight, TaprootSpendInfoExt};
11
12use crate::SECP;
13use crate::musig;
14use crate::tree::signed::{
15	cosign_taproot, leaf_cosign_taproot, leaf_cosign_taproot_v0, unlock_clause, unlock_clause_v0,
16};
17use crate::vtxo::MaybePreimage;
18
19/// Represents the kind of [GenesisTransition]
20pub enum TransitionKind {
21	Cosigned,
22	HashLockedCosigned,
23	#[allow(non_camel_case_types)]
24	HashLockedCosigned_v0,
25	Arkoor,
26}
27
28impl TransitionKind {
29	pub const fn as_str(&self) -> &'static str {
30		match self {
31			Self::Cosigned => "cosigned",
32			Self::HashLockedCosigned => "hash-locked-cosigned-v1",
33			Self::HashLockedCosigned_v0 => "hash-locked-cosigned",
34			Self::Arkoor => "arkoor",
35		}
36	}
37}
38
39impl fmt::Display for TransitionKind {
40	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41		f.write_str(self.as_str())
42	}
43}
44
45impl fmt::Debug for TransitionKind {
46	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47		fmt::Display::fmt(self, f)
48	}
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct CosignedGenesis {
53	/// All the cosign pubkeys signing the node.
54	///
55	/// Has to include server's cosign pubkey because it differs
56	/// from its regular pubkey.
57	pub pubkeys: Vec<PublicKey>,
58	pub signature: Option<schnorr::Signature>,
59}
60
61impl CosignedGenesis {
62
63	/// Taproot that this transition is satisfying.
64	pub fn input_taproot(
65		&self,
66		server_pubkey: PublicKey,
67		expiry_height: BlockHeight,
68	) -> taproot::TaprootSpendInfo {
69		let agg_pk = musig::combine_keys(self.pubkeys.iter().copied())
70			.x_only_public_key().0;
71		cosign_taproot(agg_pk, server_pubkey, expiry_height)
72	}
73
74	pub fn input_txout(
75		&self,
76		amount: Amount,
77		server_pubkey: PublicKey,
78		expiry_height: BlockHeight,
79	) -> TxOut {
80		TxOut {
81			value: amount,
82			script_pubkey: self.input_taproot(server_pubkey, expiry_height).script_pubkey(),
83		}
84	}
85
86	pub fn witness(&self) -> Witness {
87		match self.signature {
88			Some(ref sig) => Witness::from_slice(&[&sig[..]]),
89			None => Witness::new(),
90		}
91	}
92
93	/// Whether all transaction witnesses are present
94	pub fn has_all_witnesses(&self) -> bool {
95		self.signature.is_some()
96	}
97
98	pub fn validate_sigs(
99		&self,
100		tx: &Transaction,
101		input_idx: usize,
102		prev_txout: &TxOut,
103		server_pubkey: PublicKey,
104		expiry_height: BlockHeight,
105	) -> Result<(), &'static str> {
106		let signature = match self.signature {
107			Some(sig) => sig,
108			None => return Err("missing cosigned signature"),
109		};
110
111		let mut shc = sighash::SighashCache::new(tx);
112
113		let tapsighash = shc.taproot_key_spend_signature_hash(
114			input_idx,
115			&sighash::Prevouts::All(&[prev_txout]),
116			sighash::TapSighashType::Default
117		).expect("correct prevouts");
118
119		let pubkey = self.input_taproot(server_pubkey, expiry_height)
120			.output_key()
121			.to_x_only_public_key();
122
123		SECP.verify_schnorr(&signature, &tapsighash.into(), &pubkey)
124			.map_err(|_| "invalid signature")
125	}
126}
127
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct HashLockedCosignedGenesis {
131	/// User pubkey that is combined with the server pubkey
132	pub user_pubkey: PublicKey,
133	/// The script-spend signature
134	pub signature: Option<schnorr::Signature>,
135	/// The unlock preimage or the unlock hash
136	pub unlock: MaybePreimage,
137}
138
139impl HashLockedCosignedGenesis {
140	pub fn input_taproot(
141		&self,
142		server_pubkey: PublicKey,
143		expiry_height: BlockHeight,
144	) -> taproot::TaprootSpendInfo {
145		leaf_cosign_taproot(self.user_pubkey, server_pubkey, expiry_height, self.unlock.hash())
146	}
147
148	pub fn input_txout(
149		&self,
150		amount: Amount,
151		server_pubkey: PublicKey,
152		expiry_height: BlockHeight,
153	) -> TxOut {
154		TxOut {
155			value: amount,
156			script_pubkey: self.input_taproot(server_pubkey, expiry_height).script_pubkey(),
157		}
158	}
159
160	pub fn witness(
161		&self,
162		server_pubkey: PublicKey,
163		expiry_height: BlockHeight,
164	) -> Witness {
165		// No witness if the preimage or sig is missing
166		let preimage = match self.unlock {
167			MaybePreimage::Preimage(p) => p,
168			MaybePreimage::Hash(_) => return Witness::new(),
169		};
170
171		let sig = match self.signature {
172			Some(sig) => sig,
173			None => return Witness::new(),
174		};
175
176		let unlock_hash = sha256::Hash::hash(&preimage);
177		let taproot = leaf_cosign_taproot(
178			self.user_pubkey, server_pubkey, expiry_height, unlock_hash,
179		);
180
181		let clause = unlock_clause(taproot.internal_key(), unlock_hash);
182		let script_leaf = (clause, LeafVersion::TapScript);
183		let cb = taproot.control_block(&script_leaf)
184			.expect("unlock clause not found in hArk taproot");
185		Witness::from_slice(&[
186			&sig.serialize()[..],
187			&preimage[..],
188			&script_leaf.0.as_bytes(),
189			&cb.serialize()[..],
190		])
191	}
192
193	/// Whether all transaction witnesses are present
194	pub fn has_all_witnesses(&self) -> bool {
195		match self.unlock {
196			MaybePreimage::Preimage(_) => {},
197			MaybePreimage::Hash(_) => return false,
198		};
199
200		match self.signature {
201			Some(_) => true,
202			None => false,
203		}
204	}
205
206	pub fn validate_sigs(
207		&self,
208		tx: &Transaction,
209		input_idx: usize,
210		prev_txout: &TxOut,
211		server_pubkey: PublicKey,
212		expiry_height: BlockHeight,
213	) -> Result<(), &'static str> {
214		match self.unlock {
215			MaybePreimage::Preimage(_) => {},
216			MaybePreimage::Hash(_) => return Err("missing preimage")
217		};
218
219		let mut shc = sighash::SighashCache::new(tx);
220		let agg_pk = musig::combine_keys([self.user_pubkey, server_pubkey])
221			.x_only_public_key().0;
222		let script = unlock_clause(agg_pk, self.unlock.hash());
223		let leaf = TapLeafHash::from_script(&script, bitcoin::taproot::LeafVersion::TapScript);
224		let tapsighash = shc.taproot_script_spend_signature_hash(
225			input_idx, &sighash::Prevouts::All(&[prev_txout]), leaf, sighash::TapSighashType::Default,
226		).expect("correct prevouts");
227
228		let pk = self.input_taproot(server_pubkey, expiry_height)
229			.internal_key();
230
231		match self.signature {
232			None => return Err("missing signature"),
233			Some(sig) => {
234				SECP.verify_schnorr(&sig, &tapsighash.into(), &pk)
235				.map_err(|_| "invalid signature")
236			}
237		}
238	}
239}
240
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243#[allow(non_camel_case_types)]
244pub struct HashLockedCosignedGenesis_v0 {
245	/// User pubkey that is combined with the server pubkey
246	pub user_pubkey: PublicKey,
247	/// The script-spend signature
248	pub signature: Option<schnorr::Signature>,
249	/// The unlock preimage or the unlock hash
250	pub unlock: MaybePreimage,
251}
252
253impl HashLockedCosignedGenesis_v0 {
254	pub fn input_taproot(
255		&self,
256		server_pubkey: PublicKey,
257		expiry_height: BlockHeight,
258	) -> taproot::TaprootSpendInfo {
259		leaf_cosign_taproot_v0(self.user_pubkey, server_pubkey, expiry_height, self.unlock.hash())
260	}
261
262	pub fn input_txout(
263		&self,
264		amount: Amount,
265		server_pubkey: PublicKey,
266		expiry_height: BlockHeight,
267	) -> TxOut {
268		TxOut {
269			value: amount,
270			script_pubkey: self.input_taproot(server_pubkey, expiry_height).script_pubkey(),
271		}
272	}
273
274	pub fn witness(
275		&self,
276		server_pubkey: PublicKey,
277		expiry_height: BlockHeight,
278	) -> Witness {
279		// No witness if the preimage or sig is missing
280		let preimage = match self.unlock {
281			MaybePreimage::Preimage(p) => p,
282			MaybePreimage::Hash(_) => return Witness::new(),
283		};
284
285		let sig = match self.signature {
286			Some(sig) => sig,
287			None => return Witness::new(),
288		};
289
290		let unlock_hash = sha256::Hash::hash(&preimage);
291		let taproot = leaf_cosign_taproot_v0(
292			self.user_pubkey, server_pubkey, expiry_height, unlock_hash,
293		);
294
295		let clause = unlock_clause_v0(taproot.internal_key(), unlock_hash);
296		let script_leaf = (clause, LeafVersion::TapScript);
297		let cb = taproot.control_block(&script_leaf)
298			.expect("unlock clause not found in hArk taproot");
299		Witness::from_slice(&[
300			&sig.serialize()[..],
301			&preimage[..],
302			&script_leaf.0.as_bytes(),
303			&cb.serialize()[..],
304		])
305	}
306
307	/// Whether all transaction witnesses are present
308	pub fn has_all_witnesses(&self) -> bool {
309		match self.unlock {
310			MaybePreimage::Preimage(_) => {},
311			MaybePreimage::Hash(_) => return false,
312		};
313
314		match self.signature {
315			Some(_) => true,
316			None => false,
317		}
318	}
319
320	pub fn validate_sigs(
321		&self,
322		tx: &Transaction,
323		input_idx: usize,
324		prev_txout: &TxOut,
325		server_pubkey: PublicKey,
326		expiry_height: BlockHeight,
327	) -> Result<(), &'static str> {
328		match self.unlock {
329			MaybePreimage::Preimage(_) => {},
330			MaybePreimage::Hash(_) => return Err("missing preimage")
331		};
332
333		let mut shc = sighash::SighashCache::new(tx);
334		let agg_pk = musig::combine_keys([self.user_pubkey, server_pubkey])
335			.x_only_public_key().0;
336		let script = unlock_clause_v0(agg_pk, self.unlock.hash());
337		let leaf = TapLeafHash::from_script(&script, bitcoin::taproot::LeafVersion::TapScript);
338		let tapsighash = shc.taproot_script_spend_signature_hash(
339			input_idx, &sighash::Prevouts::All(&[prev_txout]), leaf, sighash::TapSighashType::Default,
340		).expect("correct prevouts");
341
342		let pk = self.input_taproot(server_pubkey, expiry_height)
343			.internal_key();
344
345		match self.signature {
346			None => return Err("missing signature"),
347			Some(sig) => {
348				SECP.verify_schnorr(&sig, &tapsighash.into(), &pk)
349				.map_err(|_| "invalid signature")
350			}
351		}
352	}
353}
354
355
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub struct ArkoorGenesis {
358	/// The keys that are used for cosiging the keyspend.
359	/// This excludes the server_pubkey
360	pub client_cosigners: Vec<PublicKey>,
361	pub tap_tweak: taproot::TapTweakHash,
362	pub signature: Option<schnorr::Signature>,
363}
364
365impl ArkoorGenesis {
366	pub fn client_cosigners(&self) -> impl Iterator<Item = PublicKey> + '_ {
367		self.client_cosigners.iter().copied()
368	}
369
370	pub fn cosigners<'a>(&'a self, server_pubkey: PublicKey) -> impl Iterator<Item = PublicKey> + 'a {
371		self.client_cosigners.iter().cloned().chain([server_pubkey])
372	}
373
374	pub fn input_txout(&self, amount: Amount, server_pubkey: PublicKey) -> TxOut {
375		TxOut {
376			value: amount,
377			script_pubkey: ScriptBuf::new_p2tr_tweaked(self.output_key(server_pubkey))
378		}
379	}
380
381	pub fn output_key(&self, server_pubkey: PublicKey) -> TweakedPublicKey {
382		let (_, agg_pk) = musig::tweaked_key_agg(self.cosigners(server_pubkey), self.tap_tweak.to_byte_array());
383		TweakedPublicKey::dangerous_assume_tweaked(agg_pk.x_only_public_key().0)
384	}
385
386	pub fn witness(&self) -> Witness {
387		match self.signature {
388			Some(sig) => Witness::from_slice(&[&sig[..]]),
389			None => Witness::new(),
390		}
391	}
392
393	// Not fully signed if we don't know the preimage
394	pub fn has_all_witnesses(&self) -> bool {
395		self.signature.is_some()
396	}
397
398	pub fn validate_sigs(
399		&self,
400		tx: &Transaction,
401		input_idx: usize,
402		prev_txout: &TxOut,
403		server_pubkey: PublicKey,
404	) -> Result<(), &'static str> {
405		let signature = match self.signature {
406			Some(sig) => sig,
407			None => return Err("missing signature"),
408		};
409
410		let mut shc = sighash::SighashCache::new(tx);
411
412		let tapsighash = shc.taproot_key_spend_signature_hash(
413			input_idx,
414			&sighash::Prevouts::All(&[prev_txout]),
415			sighash::TapSighashType::Default
416		).expect("correct prevouts");
417
418
419		SECP.verify_schnorr(
420			&signature,
421			&tapsighash.into(),
422			&self.output_key(server_pubkey).to_x_only_public_key(),
423		).map_err(|_| "invalid signature")
424	}
425}
426
427/// A transition from one genesis tx to the next.
428///
429/// See private module-level documentation for more info.
430#[derive(Debug, Clone, PartialEq, Eq)]
431pub enum GenesisTransition {
432	/// A transition based on a cosignature.
433	///
434	/// This can be either the result of a cosigned "clArk" tree branch transition
435	/// or a board which is cosigned just with the server.
436	Cosigned(CosignedGenesis),
437	/// A transition based on a cosignature and a hash lock
438	///
439	/// This is the transition type for hArk leaf policy outputs,
440	/// that spend into the leaf transaction.
441	///
442	/// Refraining from any optimizations, this type is implemented the naive way:
443	/// - the keyspend path is currently unused, could be used later
444	/// - witness will always contain the cosignature and preimage in the script spend
445	HashLockedCosigned(HashLockedCosignedGenesis),
446	/// A transition based on a cosignature and a hash lock
447	///
448	/// This is the transition type for hArk leaf policy outputs,
449	/// that spend into the leaf transaction.
450	#[allow(non_camel_case_types)]
451	HashLockedCosigned_v0(HashLockedCosignedGenesis_v0),
452	/// A regular arkoor spend, using the co-signed p2tr key-spend path.
453	Arkoor(ArkoorGenesis),
454}
455
456impl GenesisTransition {
457	pub fn new_cosigned(pubkeys: Vec<PublicKey>, signature: Option<schnorr::Signature>) -> Self {
458		Self::Cosigned(CosignedGenesis { pubkeys, signature })
459	}
460
461	pub fn new_hash_locked_cosigned(
462		user_pubkey: PublicKey,
463		signature: Option<schnorr::Signature>,
464		unlock: MaybePreimage
465	) -> Self {
466		Self::HashLockedCosigned(
467			HashLockedCosignedGenesis { user_pubkey, signature, unlock }
468		)
469	}
470
471
472	pub fn new_arkoor(
473		cosigners: Vec<PublicKey>,
474		tap_tweak: TapTweakHash,
475		signature: Option<schnorr::Signature>
476	) -> Self {
477		Self::Arkoor(ArkoorGenesis { client_cosigners: cosigners, tap_tweak, signature })
478	}
479
480	/// Output that this transition is spending.
481	pub fn input_txout(
482		&self,
483		amount: Amount,
484		server_pubkey: PublicKey,
485		expiry_height: BlockHeight,
486		_exit_delta: BlockDelta,
487	) -> TxOut {
488		match self {
489			Self::Cosigned(inner) => inner.input_txout(amount, server_pubkey, expiry_height),
490			Self::HashLockedCosigned(inner) => inner.input_txout(amount, server_pubkey, expiry_height),
491			Self::HashLockedCosigned_v0(inner) => inner.input_txout(amount, server_pubkey, expiry_height),
492			Self::Arkoor(inner) => inner.input_txout(amount, server_pubkey),
493		}
494	}
495
496	/// The transaction witness for this transition.
497	pub fn witness(
498		&self,
499		server_pubkey: PublicKey,
500		expiry_height: BlockHeight,
501	) -> Witness {
502		match self {
503			Self::Cosigned(inner) => inner.witness(),
504			Self::HashLockedCosigned(inner) => inner.witness(server_pubkey, expiry_height),
505			Self::HashLockedCosigned_v0(inner) => inner.witness(server_pubkey, expiry_height),
506			Self::Arkoor(inner) => inner.witness(),
507		}
508	}
509
510
511	/// Whether the transition is fully signed
512	pub fn has_all_witnesses(&self) -> bool {
513		match self {
514			Self::Cosigned(inner) => inner.has_all_witnesses(),
515			Self::HashLockedCosigned(inner) => inner.has_all_witnesses(),
516			Self::HashLockedCosigned_v0(inner) => inner.has_all_witnesses(),
517			Self::Arkoor(inner) => inner.has_all_witnesses(),
518		}
519	}
520
521	/// String of the transition kind, for error reporting
522	pub fn kind(&self) -> TransitionKind {
523		match self {
524			Self::Cosigned { .. } => TransitionKind::Cosigned,
525			Self::HashLockedCosigned { .. } => TransitionKind::HashLockedCosigned,
526			Self::HashLockedCosigned_v0 { .. } => TransitionKind::HashLockedCosigned_v0,
527			Self::Arkoor { .. } => TransitionKind::Arkoor,
528		}
529	}
530}
531
532/// An item in a VTXO's genesis.
533///
534/// See private module-level documentation for more info.
535#[derive(Debug, Clone, PartialEq, Eq)]
536pub struct GenesisItem {
537	/// The transition from the previous tx to this one.
538	pub transition: GenesisTransition,
539	/// The output index ("vout") of the output going to the next genesis item.
540	pub output_idx: u8,
541	/// The other outputs to construct the exit tx.
542	pub other_outputs: Vec<TxOut>,
543	/// The fee to apply to the P2A (pay-to-anchor) output of the exit tx. Likely to be a value of
544	/// zero, however, fees for certain operations such as boarding can go here if applicable.
545	pub fee_amount: Amount,
546}
547
548impl GenesisItem {
549	/// Construct the P2A (pay-to-anchor) output for the exit tx.
550	pub fn fee_anchor(&self) -> TxOut {
551		fee::fee_anchor_with_amount(self.fee_amount)
552	}
553
554	/// The total sum of sibling tx outputs including the P2A fee output.
555	pub fn other_output_sum(&self) -> Option<Amount> {
556		let mut result = self.fee_amount;
557		for o in &self.other_outputs {
558			result = result.checked_add(o.value)?;
559		}
560		Some(result)
561	}
562
563	/// Construct the exit transaction at this level of the genesis.
564	pub fn tx(&self,
565		prev: OutPoint,
566		next: TxOut,
567		server_pubkey: PublicKey,
568		expiry_height: BlockHeight,
569	) -> Transaction {
570		Transaction {
571			version: bitcoin::transaction::Version(3),
572			lock_time: bitcoin::absolute::LockTime::ZERO,
573			input: vec![TxIn {
574				previous_output: prev,
575				script_sig: ScriptBuf::new(),
576				sequence: Sequence::ZERO,
577				witness: self.transition.witness(server_pubkey, expiry_height),
578			}],
579			output: {
580				let mut out = Vec::with_capacity(self.other_outputs.len().saturating_add(2));
581				out.extend(self.other_outputs.iter().take(self.output_idx as usize).cloned());
582				out.push(next);
583				out.extend(self.other_outputs.iter().skip(self.output_idx as usize).cloned());
584				out.push(self.fee_anchor());
585				out
586			},
587		}
588	}
589}