Skip to main content

ark/vtxo/
mod.rs

1//! Representations of VTXOs in an Ark.
2
3
4// # The internal representation of VTXOs.
5//
6// The [Vtxo] type is a struct that exposes a public API through methods, but
7// we have deliberately decided to hide all its internal representation from
8// the user.
9//
10// ## Objectives
11//
12// The objectives of the internal structure of [Vtxo] are the following:
13// - have a stable encoding and decoding through [ProtocolEncoding]
14// - enable constructing all exit transactions required to perform a
15//   unilateral exit for the VTXO
16// - enable a user to validate that the exit transaction chain is safe,
17//   meaning that there are no unexpected spend paths that could break
18//   the exit. this means that
19//   - all transitions between transactions (i.e. where a child spends its
20//     parent) have only known spend paths and no malicious additional ones
21//   - all outputs of all exit transactions are standard, so they can be
22//     relayed on the public relay network
23//   - the necessary fee anchors are in place to allow the user to fund his
24//     exit
25//
26// ## Internal structure
27//
28// Each [Vtxo] has what we call a "chain anchor" and a "genesis". The chain
29// anchor is the transaction that is to be confirmed on-chain to anchor the
30// VTXO's existence into the chain. The genesis represents the data required
31// to "conceive" the [Vtxo]'s UTXO on the chain, connected to the chain anchor.
32// Conceptually, the genesis data consists of two main things:
33// - the output policy data and input witness data for each transition.
34//   This ensures we can validate the policy used for the transition and we have
35//   the necessary data to satisfy it.
36// - the additional output data to reconstruct the transactions in full
37//   (since our own transition is just one of the outputs)
38//
39// Since an exit of N transactions has N times the tx construction data,
40// but N+1 times the transition policy data, we decided to structure the
41// genesis series as follows:
42//
43// The genesis consists of "genesis items", which contain:
44// - the output policy of the previous output (of the parent)
45// - the witness to satisfy this policy
46// - the additional output data to construct an exit tx
47//
48// This means that
49// - there are an equal number of genesis items as there are exit transactions
50// - the first item will hold the output policy of the chain anchor
51// - to construct the output of the exit tx at a certain level, we get the
52//   output policy from the next genesis item
53// - the last tx's output policy is not held in the genesis, but it is held as
54//   the VTXO's own output policy
55
56pub mod policy;
57pub(crate) mod genesis;
58mod validation;
59
60pub use self::validation::VtxoValidationError;
61pub use self::policy::{Policy, VtxoPolicy, VtxoPolicyKind, ServerVtxoPolicy};
62pub(crate) use self::genesis::{GenesisItem, GenesisTransition};
63
64pub use self::policy::{
65	PubkeyVtxoPolicy, CheckpointVtxoPolicy, ExpiryVtxoPolicy,
66	ServerHtlcRecvVtxoPolicy, ServerHtlcSendVtxoPolicy
67};
68pub use self::policy::clause::{
69	VtxoClause, DelayedSignClause, DelayedTimelockSignClause, HashDelaySignClause,
70	TapScriptClause,
71};
72
73/// Type alias for a server-internal VTXO that may have policies without user pubkeys.
74pub type ServerVtxo = Vtxo<ServerVtxoPolicy>;
75
76use std::iter::FusedIterator;
77use std::{fmt, io};
78use std::str::FromStr;
79
80use bitcoin::{
81	taproot, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Weight, Witness
82};
83use bitcoin::absolute::LockTime;
84use bitcoin::hashes::{sha256, Hash};
85use bitcoin::secp256k1::{schnorr, PublicKey, XOnlyPublicKey};
86use bitcoin::taproot::TapTweakHash;
87
88use bitcoin_ext::{fee, BlockDelta, BlockHeight, TxOutExt};
89
90use crate::{musig, scripts};
91use crate::encode::{ProtocolDecodingError, ProtocolEncoding, ReadExt, WriteExt};
92use crate::lightning::PaymentHash;
93use crate::tree::signed::{UnlockHash, UnlockPreimage};
94
95/// The total signed tx weight of a exit tx.
96pub const EXIT_TX_WEIGHT: Weight = Weight::from_vb_unchecked(124);
97
98/// The current version of the vtxo encoding.
99const VTXO_ENCODING_VERSION: u16 = 1;
100
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
103#[error("failed to parse vtxo id, must be 36 bytes")]
104pub struct VtxoIdParseError;
105
106#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
107pub struct VtxoId([u8; 36]);
108
109impl VtxoId {
110	/// Size in bytes of an encoded [VtxoId].
111	pub const ENCODE_SIZE: usize = 36;
112
113	pub fn from_slice(b: &[u8]) -> Result<VtxoId, VtxoIdParseError> {
114		if b.len() == 36 {
115			let mut ret = [0u8; 36];
116			ret[..].copy_from_slice(&b[0..36]);
117			Ok(Self(ret))
118		} else {
119			Err(VtxoIdParseError)
120		}
121	}
122
123	pub fn utxo(self) -> OutPoint {
124		let vout = [self.0[32], self.0[33], self.0[34], self.0[35]];
125		OutPoint::new(Txid::from_slice(&self.0[0..32]).unwrap(), u32::from_le_bytes(vout))
126	}
127
128	pub fn to_bytes(self) -> [u8; 36] {
129		self.0
130	}
131}
132
133impl From<OutPoint> for VtxoId {
134	fn from(p: OutPoint) -> VtxoId {
135		let mut ret = [0u8; 36];
136		ret[0..32].copy_from_slice(&p.txid[..]);
137		ret[32..].copy_from_slice(&p.vout.to_le_bytes());
138		VtxoId(ret)
139	}
140}
141
142impl AsRef<[u8]> for VtxoId {
143	fn as_ref(&self) -> &[u8] {
144		&self.0
145	}
146}
147
148impl fmt::Display for VtxoId {
149	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
150		fmt::Display::fmt(&self.utxo(), f)
151	}
152}
153
154impl fmt::Debug for VtxoId {
155	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
156		fmt::Display::fmt(self, f)
157	}
158}
159
160impl FromStr for VtxoId {
161	type Err = VtxoIdParseError;
162	fn from_str(s: &str) -> Result<Self, Self::Err> {
163		Ok(OutPoint::from_str(s).map_err(|_| VtxoIdParseError)?.into())
164	}
165}
166
167impl serde::Serialize for VtxoId {
168	fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
169		if s.is_human_readable() {
170			s.collect_str(self)
171		} else {
172			s.serialize_bytes(self.as_ref())
173		}
174	}
175}
176
177impl<'de> serde::Deserialize<'de> for VtxoId {
178	fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
179		struct Visitor;
180		impl<'de> serde::de::Visitor<'de> for Visitor {
181			type Value = VtxoId;
182			fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183				write!(f, "a VtxoId")
184			}
185			fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
186				VtxoId::from_slice(v).map_err(serde::de::Error::custom)
187			}
188			fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
189				VtxoId::from_str(v).map_err(serde::de::Error::custom)
190			}
191		}
192		if d.is_human_readable() {
193			d.deserialize_str(Visitor)
194		} else {
195			d.deserialize_bytes(Visitor)
196		}
197	}
198}
199
200impl ProtocolEncoding for VtxoId {
201	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
202		w.emit_slice(&self.0)
203	}
204	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
205		let array: [u8; 36] = r.read_byte_array()
206			.map_err(|_| ProtocolDecodingError::invalid("invalid vtxo id. Expected 36 bytes"))?;
207
208		Ok(VtxoId(array))
209	}
210}
211
212/// Returns the clause to unilaterally spend a VTXO
213pub(crate) fn exit_clause(
214	user_pubkey: PublicKey,
215	exit_delta: BlockDelta,
216) -> ScriptBuf {
217	scripts::delayed_sign(exit_delta, user_pubkey.x_only_public_key().0)
218}
219
220/// Create an exit tx.
221///
222/// When the `signature` argument is provided,
223/// it will be placed in the input witness.
224pub fn create_exit_tx(
225	prevout: OutPoint,
226	output: TxOut,
227	signature: Option<&schnorr::Signature>,
228) -> Transaction {
229	Transaction {
230		version: bitcoin::transaction::Version(3),
231		lock_time: LockTime::ZERO,
232		input: vec![TxIn {
233			previous_output: prevout,
234			script_sig: ScriptBuf::new(),
235			sequence: Sequence::ZERO,
236			witness: {
237				let mut ret = Witness::new();
238				if let Some(sig) = signature {
239					ret.push(&sig[..]);
240				}
241				ret
242			},
243		}],
244		output: vec![output, fee::fee_anchor()],
245	}
246}
247
248/// Enum type used to represent a preimage<>hash relationship
249/// for which the preimage might be known but the hash always
250/// should be known.
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub(crate) enum MaybePreimage {
253	Preimage([u8; 32]),
254	Hash(sha256::Hash),
255}
256
257impl MaybePreimage {
258	/// Get the hash
259	pub fn hash(&self) -> sha256::Hash {
260		match self {
261			Self::Preimage(p) => sha256::Hash::hash(p),
262			Self::Hash(h) => *h,
263		}
264	}
265}
266
267/// Type of the items yielded by [VtxoTxIter], the iterator returned by
268/// [Vtxo::transactions].
269#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
270pub struct VtxoTxIterItem {
271	/// The actual transaction.
272	pub tx: Transaction,
273	/// The index of the relevant output of this tx
274	pub output_idx: usize,
275}
276
277/// Iterator returned by [Vtxo::transactions].
278pub struct VtxoTxIter<'a, P: Policy = VtxoPolicy> {
279	vtxo: &'a Vtxo<P>,
280
281	prev: OutPoint,
282	genesis_idx: usize,
283	current_amount: Amount,
284}
285
286impl<'a, P: Policy> VtxoTxIter<'a, P> {
287	fn new(vtxo: &'a Vtxo<P>) -> VtxoTxIter<'a, P> {
288		// Add all the amounts that go into the other outputs.
289		let onchain_amount = vtxo.amount() + vtxo.genesis.iter().map(|i| {
290			i.other_outputs.iter().map(|o| o.value).sum()
291		}).sum();
292		VtxoTxIter {
293			prev: vtxo.anchor_point,
294			vtxo: vtxo,
295			genesis_idx: 0,
296			current_amount: onchain_amount,
297		}
298	}
299}
300
301impl<'a, P: Policy> Iterator for VtxoTxIter<'a, P> {
302	type Item = VtxoTxIterItem;
303
304	fn next(&mut self) -> Option<Self::Item> {
305		let item = self.vtxo.genesis.get(self.genesis_idx)?;
306		let next_amount = self.current_amount.checked_sub(
307			item.other_outputs.iter().map(|o| o.value).sum()
308		).expect("we calculated this amount beforehand");
309
310		let next_output = if let Some(item) = self.vtxo.genesis.get(self.genesis_idx + 1) {
311			item.transition.input_txout(
312				next_amount,
313				self.vtxo.server_pubkey,
314				self.vtxo.expiry_height,
315				self.vtxo.exit_delta,
316			)
317		} else {
318			// when we reach the end of the chain, we take the eventual output of the vtxo
319			self.vtxo.policy.txout(
320				self.vtxo.amount,
321				self.vtxo.server_pubkey,
322				self.vtxo.exit_delta,
323				self.vtxo.expiry_height,
324			)
325		};
326
327		let tx = item.tx(self.prev, next_output, self.vtxo.server_pubkey, self.vtxo.expiry_height);
328		self.prev = OutPoint::new(tx.compute_txid(), item.output_idx as u32);
329		self.genesis_idx += 1;
330		self.current_amount = next_amount;
331		let output_idx = item.output_idx as usize;
332		Some(VtxoTxIterItem { tx, output_idx })
333	}
334
335	fn size_hint(&self) -> (usize, Option<usize>) {
336		let len = self.vtxo.genesis.len().saturating_sub(self.genesis_idx);
337		(len, Some(len))
338	}
339}
340
341impl<'a, P: Policy> ExactSizeIterator for VtxoTxIter<'a, P> {}
342impl<'a, P: Policy> FusedIterator for VtxoTxIter<'a, P> {}
343
344
345/// Information that specifies a VTXO, independent of its origin.
346#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
347pub struct VtxoSpec<P = VtxoPolicy> {
348	pub policy: P,
349	pub amount: Amount,
350	pub expiry_height: BlockHeight,
351	pub server_pubkey: PublicKey,
352	pub exit_delta: BlockDelta,
353}
354
355impl<P: Policy> VtxoSpec<P> {
356	/// The taproot spend info for the output of this [Vtxo].
357	pub fn output_taproot(&self) -> taproot::TaprootSpendInfo {
358		self.policy.taproot(self.server_pubkey, self.exit_delta, self.expiry_height)
359	}
360
361	/// The scriptPubkey of the output of this [Vtxo].
362	pub fn output_script_pubkey(&self) -> ScriptBuf {
363		self.policy.script_pubkey(self.server_pubkey, self.exit_delta, self.expiry_height)
364	}
365
366	/// The transaction output (eventual UTXO) of this [Vtxo].
367	pub fn txout(&self) -> TxOut {
368		self.policy.txout(self.amount, self.server_pubkey, self.exit_delta, self.expiry_height)
369	}
370}
371
372/// Represents a VTXO in the Ark.
373///
374/// The correctness of the return values of methods on this type is conditional
375/// on the VTXO being valid. For invalid VTXOs, the methods should never panic,
376/// but can return incorrect values.
377/// It is advised to always validate a VTXO upon receipt using [Vtxo::validate].
378///
379/// Be mindful of calling [Clone] on a [Vtxo], as they can be of
380/// non-negligible size. It is advised to use references where possible
381/// or use an [std::rc::Rc] or [std::sync::Arc] if needed.
382///
383/// Implementations of [PartialEq], [Eq], [PartialOrd], [Ord] and [Hash] are
384/// proxied to the implementation on [Vtxo::id].
385#[derive(Debug, Clone)]
386pub struct Vtxo<P = VtxoPolicy> {
387	pub(crate) policy: P,
388	pub(crate) amount: Amount,
389	pub(crate) expiry_height: BlockHeight,
390
391	pub(crate) server_pubkey: PublicKey,
392	pub(crate) exit_delta: BlockDelta,
393
394	pub(crate) anchor_point: OutPoint,
395	pub(crate) genesis: Vec<genesis::GenesisItem>,
396
397	/// The resulting actual "point" of the VTXO. I.e. the output of the last
398	/// exit tx of this VTXO.
399	///
400	/// We keep this for two reasons:
401	/// - the ID is based on this, so it should be cheaply accessible
402	/// - it forms as a good checksum for all the internal genesis data
403	pub(crate) point: OutPoint,
404}
405
406impl<P: Policy> Vtxo<P> {
407	/// Get the identifier for this [Vtxo].
408	///
409	/// This is the same as [Vtxo::point] but encoded as a byte array.
410	pub fn id(&self) -> VtxoId {
411		self.point.into()
412	}
413
414	/// The outpoint from which to build forfeit or arkoor txs.
415	///
416	/// This can be an on-chain utxo or an off-chain vtxo.
417	pub fn point(&self) -> OutPoint {
418		self.point
419	}
420
421	/// The amount of the [Vtxo].
422	pub fn amount(&self) -> Amount {
423		self.amount
424	}
425
426	/// The UTXO that should be confirmed for this [Vtxo] to be valid.
427	///
428	/// It is the very root of the VTXO.
429	pub fn chain_anchor(&self) -> OutPoint {
430		self.anchor_point
431	}
432
433	/// The output policy of this VTXO.
434	pub fn policy(&self) -> &P {
435		&self.policy
436	}
437
438	/// The output policy type of this VTXO.
439	pub fn policy_type(&self) -> VtxoPolicyKind {
440		self.policy.policy_type()
441	}
442
443	/// The expiry height of the [Vtxo].
444	pub fn expiry_height(&self) -> BlockHeight {
445		self.expiry_height
446	}
447
448	/// The server pubkey used in arkoor transitions.
449	pub fn server_pubkey(&self) -> PublicKey {
450		self.server_pubkey
451	}
452
453	/// The relative timelock block delta used for exits.
454	pub fn exit_delta(&self) -> BlockDelta {
455		self.exit_delta
456	}
457
458	/// Returns the total exit depth (including OOR depth) of the vtxo.
459	pub fn exit_depth(&self) -> u16 {
460		self.genesis.len() as u16
461	}
462
463	/// Iterate over all oor transitions in this VTXO
464	///
465	/// The outer `Vec` cointains one element for each transition.
466	/// The inner `Vec` contains all pubkeys within that transition.
467	///
468	/// This does not include the current arkoor pubkey, for that use
469	/// [Vtxo::arkoor_pubkey].
470	pub fn past_arkoor_pubkeys(&self) -> Vec<Vec<PublicKey>> {
471		self.genesis.iter().filter_map(|g| {
472			match &g.transition {
473				// NB in principle, a genesis item's transition MUST have
474				// an arkoor pubkey, otherwise the vtxo is invalid
475				GenesisTransition::Arkoor(inner) => Some(inner.client_cosigners().collect()),
476				_ => None,
477			}
478		}).collect()
479	}
480
481	/// The taproot spend info for the output of this [Vtxo].
482	pub fn output_taproot(&self) -> taproot::TaprootSpendInfo {
483		self.policy.taproot(self.server_pubkey, self.exit_delta, self.expiry_height)
484	}
485
486	/// The scriptPubkey of the output of this [Vtxo].
487	pub fn output_script_pubkey(&self) -> ScriptBuf {
488		self.policy.script_pubkey(self.server_pubkey, self.exit_delta, self.expiry_height)
489	}
490
491	/// The transaction output (eventual UTXO) of this [Vtxo].
492	pub fn txout(&self) -> TxOut {
493		self.policy.txout(self.amount, self.server_pubkey, self.exit_delta, self.expiry_height)
494	}
495
496	/// Whether this VTXO is fully signed
497	///
498	/// It is possible to represent unsigned VTXOs, for which this method
499	/// will return false.
500	pub fn is_fully_signed(&self) -> bool {
501		self.genesis.iter().all(|g| g.transition.is_fully_signed())
502	}
503
504	/// Check if this VTXO is standard for relay purposes
505	///
506	/// A VTXO is standard if:
507	/// - Its own output is standard
508	/// - all sibling outputs in the exit path are standard
509	pub fn is_standard(&self) -> bool {
510		self.txout().is_standard() && self.genesis.iter()
511			.all(|i| i.other_outputs.iter().all(|o| o.is_standard()))
512	}
513
514	/// Returns the "hArk" unlock hash if this is a hArk leaf VTXO
515	pub fn unlock_hash(&self) -> Option<UnlockHash> {
516		match self.genesis.last()?.transition {
517			GenesisTransition::HashLockedCosigned(ref inner) => Some(inner.unlock.hash()),
518			_ => None,
519		}
520	}
521
522	/// Provide the leaf signature for an unfinalized hArk VTXO
523	///
524	/// Returns true if this VTXO was an unfinalized hArk VTXO.
525	pub fn provide_unlock_signature(&mut self, signature: schnorr::Signature) -> bool {
526		match self.genesis.last_mut().map(|g| &mut g.transition) {
527			Some(GenesisTransition::HashLockedCosigned(inner)) => {
528				inner.signature.replace(signature);
529				true
530			},
531			_ => false,
532		}
533	}
534
535	/// Provide the unlock preimage for an unfinalized hArk VTXO
536	///
537	/// Returns true if this VTXO was an unfinalized hArk VTXO and the preimage matched.
538	pub fn provide_unlock_preimage(&mut self, preimage: UnlockPreimage) -> bool {
539		match self.genesis.last_mut().map(|g| &mut g.transition) {
540			Some(GenesisTransition::HashLockedCosigned(ref mut inner)) => {
541				if inner.unlock.hash() == UnlockHash::hash(&preimage) {
542					inner.unlock = MaybePreimage::Preimage(preimage);
543					true
544				} else {
545					false
546				}
547			},
548			_ => false,
549		}
550	}
551
552	/// Get the spec for this VTXO.
553	pub fn spec(&self) -> VtxoSpec<P> {
554		VtxoSpec {
555			policy: self.policy.clone(),
556			amount: self.amount,
557			expiry_height: self.expiry_height,
558			server_pubkey: self.server_pubkey,
559			exit_delta: self.exit_delta,
560		}
561	}
562
563	/// Iterator that constructs all the exit txs for this [Vtxo].
564	pub fn transactions(&self) -> VtxoTxIter<'_, P> {
565		VtxoTxIter::new(self)
566	}
567
568	/// Fully validate this VTXO and its entire transaction chain.
569	///
570	/// The `chain_anchor_tx` must be the tx with txid matching
571	/// [Vtxo::chain_anchor].
572	pub fn validate(
573		&self,
574		chain_anchor_tx: &Transaction,
575	) -> Result<(), VtxoValidationError> {
576		self::validation::validate(self, chain_anchor_tx)
577	}
578
579	/// Validate VTXO structure without checking signatures.
580	pub fn validate_unsigned(
581		&self,
582		chain_anchor_tx: &Transaction,
583	) -> Result<(), VtxoValidationError> {
584		self::validation::validate_unsigned(self, chain_anchor_tx)
585	}
586}
587
588impl Vtxo {
589	/// Returns the user pubkey associated with this [Vtxo].
590	pub fn user_pubkey(&self) -> PublicKey {
591		self.policy.user_pubkey()
592	}
593
594	/// The public key used to cosign arkoor txs spending this [Vtxo].
595	/// This will return [None] if [VtxoPolicy::is_arkoor_compatible] returns false
596	/// for this VTXO's policy.
597	pub fn arkoor_pubkey(&self) -> Option<PublicKey> {
598		self.policy.arkoor_pubkey()
599	}
600
601	/// Return the aggregate pubkey of the user and server pubkey used in
602	/// hArk forfeit transactions
603	pub(crate) fn forfeit_agg_pubkey(&self) -> XOnlyPublicKey {
604		let ret = musig::combine_keys([self.user_pubkey(), self.server_pubkey()]);
605		debug_assert_eq!(ret, self.output_taproot().internal_key());
606		ret
607	}
608
609	/// Shortcut to fully finalize a hark leaf using both keys
610	#[cfg(any(test, feature = "test-util"))]
611	pub fn finalize_hark_leaf(
612		&mut self,
613		user_key: &bitcoin::secp256k1::Keypair,
614		server_key: &bitcoin::secp256k1::Keypair,
615		chain_anchor: &Transaction,
616		unlock_preimage: UnlockPreimage,
617	) {
618		use crate::tree::signed::{LeafVtxoCosignContext, LeafVtxoCosignResponse};
619
620		// first sign and provide the signature
621		let (ctx, req) = LeafVtxoCosignContext::new(self, chain_anchor, user_key);
622		let cosign = LeafVtxoCosignResponse::new_cosign(&req, self, chain_anchor, server_key);
623		assert!(ctx.finalize(self, cosign));
624		// then provide preimage
625		assert!(self.provide_unlock_preimage(unlock_preimage));
626	}
627}
628
629impl Vtxo<ServerVtxoPolicy> {
630	/// Try to convert into a user [Vtxo]
631	///
632	/// Returns the original value on failure.
633	pub fn try_into_user_vtxo(self) -> Result<Vtxo, ServerVtxo> {
634		if let Some(p) = self.policy.clone().into_user_policy() {
635			Ok(Vtxo {
636				policy: p,
637				amount: self.amount,
638				expiry_height: self.expiry_height,
639				server_pubkey: self.server_pubkey,
640				exit_delta: self.exit_delta,
641				anchor_point: self.anchor_point,
642				genesis: self.genesis,
643				point: self.point,
644			})
645		} else {
646			Err(self)
647		}
648	}
649}
650
651impl<P: Policy> PartialEq for Vtxo<P> {
652	fn eq(&self, other: &Self) -> bool {
653		PartialEq::eq(&self.id(), &other.id())
654	}
655}
656
657impl<P: Policy> Eq for Vtxo<P> {}
658
659impl<P: Policy> PartialOrd for Vtxo<P> {
660	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
661		PartialOrd::partial_cmp(&self.id(), &other.id())
662	}
663}
664
665impl<P: Policy> Ord for Vtxo<P> {
666	fn cmp(&self, other: &Self) -> std::cmp::Ordering {
667		Ord::cmp(&self.id(), &other.id())
668	}
669}
670
671impl<P: Policy> std::hash::Hash for Vtxo<P> {
672	fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
673		std::hash::Hash::hash(&self.id(), state)
674	}
675}
676
677impl AsRef<Vtxo> for Vtxo {
678	fn as_ref(&self) -> &Vtxo {
679	    self
680	}
681}
682
683impl From<Vtxo> for ServerVtxo {
684	fn from(vtxo: Vtxo) -> ServerVtxo {
685		ServerVtxo {
686			policy: vtxo.policy.into(),
687			amount: vtxo.amount,
688			expiry_height: vtxo.expiry_height,
689			server_pubkey: vtxo.server_pubkey,
690			exit_delta: vtxo.exit_delta,
691			anchor_point: vtxo.anchor_point,
692			genesis: vtxo.genesis,
693			point: vtxo.point,
694		}
695	}
696}
697
698/// Implemented on anything that is kinda a [Vtxo]
699pub trait VtxoRef {
700	/// The [VtxoId] of the VTXO
701	fn vtxo_id(&self) -> VtxoId;
702
703	/// If the [Vtxo] can be provided, provides it
704	fn vtxo(&self) -> Option<&Vtxo>;
705}
706
707impl VtxoRef for VtxoId {
708	fn vtxo_id(&self) -> VtxoId { *self }
709	fn vtxo(&self) -> Option<&Vtxo> { None }
710}
711
712impl<'a> VtxoRef for &'a VtxoId {
713	fn vtxo_id(&self) -> VtxoId { **self }
714	fn vtxo(&self) -> Option<&Vtxo> { None }
715}
716
717impl VtxoRef for Vtxo {
718	fn vtxo_id(&self) -> VtxoId { self.id() }
719	fn vtxo(&self) -> Option<&Vtxo> { Some(self) }
720}
721
722impl<'a> VtxoRef for &'a Vtxo {
723	fn vtxo_id(&self) -> VtxoId { self.id() }
724	fn vtxo(&self) -> Option<&Vtxo> { Some(*self) }
725}
726
727/// The byte used to encode the [VtxoPolicy::Pubkey] output type.
728const VTXO_POLICY_PUBKEY: u8 = 0x00;
729
730/// The byte used to encode the [VtxoPolicy::ServerHtlcSend] output type.
731const VTXO_POLICY_SERVER_HTLC_SEND: u8 = 0x01;
732
733/// The byte used to encode the [VtxoPolicy::ServerHtlcRecv] output type.
734const VTXO_POLICY_SERVER_HTLC_RECV: u8 = 0x02;
735
736/// The byte used to encode the [ServerVtxoPolicy::Checkpoint] output type.
737const VTXO_POLICY_CHECKPOINT: u8 = 0x03;
738
739/// The byte used to encode the [ServerVtxoPolicy::Expiry] output type.
740const VTXO_POLICY_EXPIRY: u8 = 0x04;
741
742impl ProtocolEncoding for VtxoPolicy {
743	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
744		match self {
745			Self::Pubkey(PubkeyVtxoPolicy { user_pubkey }) => {
746				w.emit_u8(VTXO_POLICY_PUBKEY)?;
747				user_pubkey.encode(w)?;
748			},
749			Self::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, payment_hash, htlc_expiry }) => {
750				w.emit_u8(VTXO_POLICY_SERVER_HTLC_SEND)?;
751				user_pubkey.encode(w)?;
752				payment_hash.to_sha256_hash().encode(w)?;
753				w.emit_u32(*htlc_expiry)?;
754			},
755			Self::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy {
756				user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta,
757			}) => {
758				w.emit_u8(VTXO_POLICY_SERVER_HTLC_RECV)?;
759				user_pubkey.encode(w)?;
760				payment_hash.to_sha256_hash().encode(w)?;
761				w.emit_u32(*htlc_expiry)?;
762				w.emit_u16(*htlc_expiry_delta)?;
763			},
764		}
765		Ok(())
766	}
767
768	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
769		let type_byte = r.read_u8()?;
770		decode_vtxo_policy(type_byte, r)
771	}
772}
773
774/// Decode a [VtxoPolicy] with the given type byte
775///
776/// We have this function so it can be reused in [VtxoPolicy] and [ServerVtxoPolicy].
777fn decode_vtxo_policy<R: io::Read + ?Sized>(
778	type_byte: u8,
779	r: &mut R,
780) -> Result<VtxoPolicy, ProtocolDecodingError> {
781	match type_byte {
782		VTXO_POLICY_PUBKEY => {
783			let user_pubkey = PublicKey::decode(r)?;
784			Ok(VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey }))
785		},
786		VTXO_POLICY_SERVER_HTLC_SEND => {
787			let user_pubkey = PublicKey::decode(r)?;
788			let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
789			let htlc_expiry = r.read_u32()?;
790			Ok(VtxoPolicy::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, payment_hash, htlc_expiry }))
791		},
792		VTXO_POLICY_SERVER_HTLC_RECV => {
793			let user_pubkey = PublicKey::decode(r)?;
794			let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
795			let htlc_expiry = r.read_u32()?;
796			let htlc_expiry_delta = r.read_u16()?;
797			Ok(VtxoPolicy::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy { user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta }))
798		},
799		v => Err(ProtocolDecodingError::invalid(format_args!(
800			"invalid VtxoPolicy type byte: {v:#x}",
801		))),
802	}
803}
804
805impl ProtocolEncoding for ServerVtxoPolicy {
806	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
807		match self {
808			Self::User(p) => p.encode(w)?,
809			Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }) => {
810				w.emit_u8(VTXO_POLICY_CHECKPOINT)?;
811				user_pubkey.encode(w)?;
812			},
813			Self::Expiry(ExpiryVtxoPolicy { internal_key }) => {
814				w.emit_u8(VTXO_POLICY_EXPIRY)?;
815				internal_key.encode(w)?;
816			},
817		}
818		Ok(())
819	}
820
821	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
822		let type_byte = r.read_u8()?;
823		match type_byte {
824			VTXO_POLICY_PUBKEY | VTXO_POLICY_SERVER_HTLC_SEND | VTXO_POLICY_SERVER_HTLC_RECV => {
825				Ok(Self::User(decode_vtxo_policy(type_byte, r)?))
826			},
827			VTXO_POLICY_CHECKPOINT => {
828				let user_pubkey = PublicKey::decode(r)?;
829				Ok(Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }))
830			},
831			VTXO_POLICY_EXPIRY => {
832				let internal_key = XOnlyPublicKey::decode(r)?;
833				Ok(Self::Expiry(ExpiryVtxoPolicy { internal_key }))
834			},
835			v => Err(ProtocolDecodingError::invalid(format_args!(
836				"invalid ServerVtxoPolicy type byte: {v:#x}",
837			))),
838		}
839	}
840}
841
842/// The byte used to encode the [GenesisTransition::Cosigned] gen transition type.
843const GENESIS_TRANSITION_TYPE_COSIGNED: u8 = 1;
844
845/// The byte used to encode the [GenesisTransition::Arkoor] gen transition type.
846const GENESIS_TRANSITION_TYPE_ARKOOR: u8 = 2;
847
848/// The byte used to encode the [GenesisTransition::HashLockedCosigned] gen transition type.
849const GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED: u8 = 3;
850
851impl ProtocolEncoding for GenesisTransition {
852	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
853		match self {
854			Self::Cosigned(t) => {
855				w.emit_u8(GENESIS_TRANSITION_TYPE_COSIGNED)?;
856				w.emit_compact_size(t.pubkeys.len() as u64)?;
857				for pk in t.pubkeys.iter() {
858					pk.encode(w)?;
859				}
860				t.signature.encode(w)?;
861			},
862			Self::HashLockedCosigned(t) => {
863				w.emit_u8(GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED)?;
864				t.user_pubkey.encode(w)?;
865				t.signature.encode(w)?;
866				match t.unlock {
867					MaybePreimage::Preimage(p) => {
868						w.emit_u8(0)?;
869						w.emit_slice(&p[..])?;
870					},
871					MaybePreimage::Hash(h) => {
872						w.emit_u8(1)?;
873						w.emit_slice(&h[..])?;
874					},
875				}
876			},
877			Self::Arkoor(t) => {
878				w.emit_u8(GENESIS_TRANSITION_TYPE_ARKOOR)?;
879				w.emit_compact_size(t.client_cosigners.len() as u64)?;
880				for cosigner in &t.client_cosigners {
881					cosigner.encode(w)?;
882				};
883				t.tap_tweak.encode(w)?;
884				t.signature.encode(w)?;
885			},
886		}
887		Ok(())
888	}
889
890	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
891		match r.read_u8()? {
892			GENESIS_TRANSITION_TYPE_COSIGNED => {
893				let nb_pubkeys = r.read_compact_size()? as usize;
894				let mut pubkeys = Vec::with_capacity(nb_pubkeys);
895				for _ in 0..nb_pubkeys {
896					pubkeys.push(PublicKey::decode(r)?);
897				}
898				let signature = Option::<schnorr::Signature>::decode(r)?;
899				Ok(Self::new_cosigned(pubkeys, signature))
900			},
901			GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED => {
902				let user_pubkey = PublicKey::decode(r)?;
903				let signature = Option::<schnorr::Signature>::decode(r)?;
904				let unlock = match r.read_u8()? {
905					0 => MaybePreimage::Preimage(r.read_byte_array()?),
906					1 => MaybePreimage::Hash(ProtocolEncoding::decode(r)?),
907					v => return Err(ProtocolDecodingError::invalid(format_args!(
908						"invalid MaybePreimage type byte: {v:#x}",
909					))),
910				};
911				Ok(Self::new_hash_locked_cosigned(user_pubkey, signature, unlock))
912			},
913			GENESIS_TRANSITION_TYPE_ARKOOR => {
914				let nb_cosigners = r.read_compact_size()? as usize;
915				let mut cosigners = Vec::with_capacity(nb_cosigners);
916				for _ in 0..nb_cosigners {
917					cosigners.push(PublicKey::decode(r)?);
918				}
919				let taptweak = TapTweakHash::decode(r)?;
920				let signature = Option::<schnorr::Signature>::decode(r)?;
921				Ok(Self::new_arkoor(cosigners, taptweak, signature))
922			},
923			v => Err(ProtocolDecodingError::invalid(format_args!(
924				"invalid GenesisTransistion type byte: {v:#x}",
925			))),
926		}
927	}
928}
929
930impl<P: Policy + ProtocolEncoding> ProtocolEncoding for Vtxo<P> {
931	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
932		w.emit_u16(VTXO_ENCODING_VERSION)?;
933		w.emit_u64(self.amount.to_sat())?;
934		w.emit_u32(self.expiry_height)?;
935		self.server_pubkey.encode(w)?;
936		w.emit_u16(self.exit_delta)?;
937		self.anchor_point.encode(w)?;
938
939		w.emit_compact_size(self.genesis.len() as u64)?;
940		for item in &self.genesis {
941			item.transition.encode(w)?;
942			let nb_outputs = item.other_outputs.len() + 1;
943			w.emit_u8(nb_outputs.try_into()
944				.map_err(|_| io::Error::other("too many outputs on genesis transaction"))?)?;
945			w.emit_u8(item.output_idx)?;
946			for txout in &item.other_outputs {
947				txout.encode(w)?;
948			}
949		}
950
951		self.policy.encode(w)?;
952		self.point.encode(w)?;
953		Ok(())
954	}
955
956	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
957		let version = r.read_u16()?;
958		if version != VTXO_ENCODING_VERSION {
959			return Err(ProtocolDecodingError::invalid(format_args!(
960				"invalid Vtxo encoding version byte: {version:#x}",
961			)));
962		}
963
964		let amount = Amount::from_sat(r.read_u64()?);
965		let expiry_height = r.read_u32()?;
966		let server_pubkey = PublicKey::decode(r)?;
967		let exit_delta = r.read_u16()?;
968		let anchor_point = OutPoint::decode(r)?;
969
970		let nb_genesis_items = r.read_compact_size()? as usize;
971		let mut genesis = Vec::with_capacity(nb_genesis_items);
972		for _ in 0..nb_genesis_items {
973			let transition = GenesisTransition::decode(r)?;
974			let nb_outputs = r.read_u8()? as usize;
975			let output_idx = r.read_u8()?;
976			let nb_other = nb_outputs.checked_sub(1)
977				.ok_or_else(|| ProtocolDecodingError::invalid("genesis item with 0 outputs"))?;
978			let mut other_outputs = Vec::with_capacity(nb_other);
979			for _ in 0..nb_other {
980				other_outputs.push(TxOut::decode(r)?);
981			}
982			genesis.push(GenesisItem { transition, output_idx, other_outputs });
983		}
984
985		let policy = P::decode(r)?;
986		let point = OutPoint::decode(r)?;
987
988		Ok(Self {
989			amount, expiry_height, server_pubkey, exit_delta, anchor_point, genesis, policy, point,
990		})
991	}
992}
993
994
995#[cfg(test)]
996mod test {
997	use bitcoin::consensus::encode::serialize_hex;
998	use bitcoin::hex::DisplayHex;
999
1000	use crate::test_util::encoding_roundtrip;
1001	use crate::test_util::dummy::{DUMMY_SERVER_KEY, DUMMY_USER_KEY};
1002	use crate::test_util::vectors::{generate_vtxo_vectors, VTXO_VECTORS};
1003
1004	use super::*;
1005
1006	#[test]
1007	fn test_generate_vtxo_vectors() {
1008		let g = generate_vtxo_vectors();
1009		// the generation code prints its inner values
1010
1011		println!("\n\ngenerated:");
1012		println!("  anchor_tx: {}", serialize_hex(&g.anchor_tx));
1013		println!("  board_vtxo: {}", g.board_vtxo.serialize().as_hex().to_string());
1014		println!("  arkoor_htlc_out_vtxo: {}", g.arkoor_htlc_out_vtxo.serialize().as_hex().to_string());
1015		println!("  arkoor2_vtxo: {}", g.arkoor2_vtxo.serialize().as_hex().to_string());
1016		println!("  round_tx: {}", serialize_hex(&g.round_tx));
1017		println!("  round1_vtxo: {}", g.round1_vtxo.serialize().as_hex().to_string());
1018		println!("  round2_vtxo: {}", g.round2_vtxo.serialize().as_hex().to_string());
1019		println!("  arkoor3_vtxo: {}", g.arkoor3_vtxo.serialize().as_hex().to_string());
1020
1021
1022		let v = &*VTXO_VECTORS;
1023		println!("\n\nstatic:");
1024		println!("  anchor_tx: {}", serialize_hex(&v.anchor_tx));
1025		println!("  board_vtxo: {}", v.board_vtxo.serialize().as_hex().to_string());
1026		println!("  arkoor_htlc_out_vtxo: {}", v.arkoor_htlc_out_vtxo.serialize().as_hex().to_string());
1027		println!("  arkoor2_vtxo: {}", v.arkoor2_vtxo.serialize().as_hex().to_string());
1028		println!("  round_tx: {}", serialize_hex(&v.round_tx));
1029		println!("  round1_vtxo: {}", v.round1_vtxo.serialize().as_hex().to_string());
1030		println!("  round2_vtxo: {}", v.round2_vtxo.serialize().as_hex().to_string());
1031		println!("  arkoor3_vtxo: {}", v.arkoor3_vtxo.serialize().as_hex().to_string());
1032
1033		assert_eq!(g.anchor_tx, v.anchor_tx, "anchor_tx does not match");
1034		assert_eq!(g.board_vtxo, v.board_vtxo, "board_vtxo does not match");
1035		assert_eq!(g.arkoor_htlc_out_vtxo, v.arkoor_htlc_out_vtxo, "arkoor_htlc_out_vtxo does not match");
1036		assert_eq!(g.arkoor2_vtxo, v.arkoor2_vtxo, "arkoor2_vtxo does not match");
1037		assert_eq!(g.round_tx, v.round_tx, "round_tx does not match");
1038		assert_eq!(g.round1_vtxo, v.round1_vtxo, "round1_vtxo does not match");
1039		assert_eq!(g.round2_vtxo, v.round2_vtxo, "round2_vtxo does not match");
1040		assert_eq!(g.arkoor3_vtxo, v.arkoor3_vtxo, "arkoor3_vtxo does not match");
1041
1042		// this passes because the Eq is based on id which doesn't compare signatures
1043		assert_eq!(g, *v);
1044	}
1045
1046	#[test]
1047	fn exit_depth() {
1048		let vtxos = &*VTXO_VECTORS;
1049		// board
1050		assert_eq!(vtxos.board_vtxo.exit_depth(), 1 /* cosign */);
1051
1052		// round
1053		assert_eq!(vtxos.round1_vtxo.exit_depth(), 3 /* cosign */);
1054
1055		// arkoor
1056		assert_eq!(
1057			vtxos.arkoor_htlc_out_vtxo.exit_depth(),
1058			1 /* cosign */ + 1 /* checkpoint*/ + 1 /* arkoor */,
1059		);
1060		assert_eq!(
1061			vtxos.arkoor2_vtxo.exit_depth(),
1062			1 /* cosign */ + 2 /* checkpoint */ + 2 /* arkoor */,
1063		);
1064		assert_eq!(
1065			vtxos.arkoor3_vtxo.exit_depth(),
1066			3 /* cosign */ + 1 /* checkpoint */ + 1 /* arkoor */,
1067		);
1068	}
1069
1070	#[test]
1071	fn test_genesis_length_257() {
1072		let vtxo = Vtxo {
1073			policy: VtxoPolicy::new_pubkey(DUMMY_USER_KEY.public_key()),
1074			amount: Amount::from_sat(10_000),
1075			expiry_height: 101_010,
1076			server_pubkey: DUMMY_SERVER_KEY.public_key(),
1077			exit_delta: 2016,
1078			anchor_point: OutPoint::new(Txid::from_slice(&[1u8; 32]).unwrap(), 1),
1079			genesis: (0..257).map(|_| {
1080				GenesisItem {
1081					transition: GenesisTransition::new_cosigned(
1082						vec![DUMMY_USER_KEY.public_key()],
1083						Some(schnorr::Signature::from_slice(&[2u8; 64]).unwrap()),
1084					),
1085					output_idx: 0,
1086					other_outputs: vec![],
1087				}
1088			}).collect(),
1089			point: OutPoint::new(Txid::from_slice(&[3u8; 32]).unwrap(), 3),
1090		};
1091		assert_eq!(vtxo.genesis.len(), 257);
1092		encoding_roundtrip(&vtxo);
1093	}
1094
1095	mod genesis_transition_encoding {
1096		use bitcoin::hashes::{sha256, Hash};
1097		use bitcoin::secp256k1::{Keypair, PublicKey};
1098		use bitcoin::taproot::TapTweakHash;
1099		use std::str::FromStr;
1100
1101		use crate::test_util::encoding_roundtrip;
1102		use super::genesis::{
1103			GenesisTransition, CosignedGenesis, HashLockedCosignedGenesis, ArkoorGenesis,
1104		};
1105		use super::MaybePreimage;
1106
1107		fn test_pubkey() -> PublicKey {
1108			Keypair::from_str(
1109				"916da686cedaee9a9bfb731b77439f2a3f1df8664e16488fba46b8d2bfe15e92"
1110			).unwrap().public_key()
1111		}
1112
1113		fn test_signature() -> bitcoin::secp256k1::schnorr::Signature {
1114			"cc8b93e9f6fbc2506bb85ae8bbb530b178daac49704f5ce2e3ab69c266fd5932\
1115			 0b28d028eef212e3b9fdc42cfd2e0760a0359d3ea7d2e9e8cfe2040e3f1b71ea"
1116				.parse().unwrap()
1117		}
1118
1119		#[test]
1120		fn cosigned_with_signature() {
1121			let transition = GenesisTransition::Cosigned(CosignedGenesis {
1122				pubkeys: vec![test_pubkey()],
1123				signature: Some(test_signature()),
1124			});
1125			encoding_roundtrip(&transition);
1126		}
1127
1128		#[test]
1129		fn cosigned_without_signature() {
1130			let transition = GenesisTransition::Cosigned(CosignedGenesis {
1131				pubkeys: vec![test_pubkey()],
1132				signature: None,
1133			});
1134			encoding_roundtrip(&transition);
1135		}
1136
1137		#[test]
1138		fn cosigned_multiple_pubkeys() {
1139			let pk1 = test_pubkey();
1140			let pk2 = Keypair::from_str(
1141				"fab9e598081a3e74b2233d470c4ad87bcc285b6912ed929568e62ac0e9409879"
1142			).unwrap().public_key();
1143
1144			let transition = GenesisTransition::Cosigned(CosignedGenesis {
1145				pubkeys: vec![pk1, pk2],
1146				signature: Some(test_signature()),
1147			});
1148			encoding_roundtrip(&transition);
1149		}
1150
1151		#[test]
1152		fn hash_locked_cosigned_with_preimage() {
1153			let preimage = [0x42u8; 32];
1154			let transition = GenesisTransition::HashLockedCosigned(HashLockedCosignedGenesis {
1155				user_pubkey: test_pubkey(),
1156				signature: Some(test_signature()),
1157				unlock: MaybePreimage::Preimage(preimage),
1158			});
1159			encoding_roundtrip(&transition);
1160		}
1161
1162		#[test]
1163		fn hash_locked_cosigned_with_hash() {
1164			let hash = sha256::Hash::hash(b"test preimage");
1165			let transition = GenesisTransition::HashLockedCosigned(HashLockedCosignedGenesis {
1166				user_pubkey: test_pubkey(),
1167				signature: Some(test_signature()),
1168				unlock: MaybePreimage::Hash(hash),
1169			});
1170			encoding_roundtrip(&transition);
1171		}
1172
1173		#[test]
1174		fn hash_locked_cosigned_without_signature() {
1175			let preimage = [0x42u8; 32];
1176			let transition = GenesisTransition::HashLockedCosigned(HashLockedCosignedGenesis {
1177				user_pubkey: test_pubkey(),
1178				signature: None,
1179				unlock: MaybePreimage::Preimage(preimage),
1180			});
1181			encoding_roundtrip(&transition);
1182		}
1183
1184		#[test]
1185		fn arkoor_with_signature() {
1186			let tap_tweak = TapTweakHash::from_slice(&[0xabu8; 32]).unwrap();
1187			let transition = GenesisTransition::Arkoor(ArkoorGenesis {
1188				client_cosigners: vec![test_pubkey()],
1189				tap_tweak,
1190				signature: Some(test_signature()),
1191			});
1192			encoding_roundtrip(&transition);
1193		}
1194
1195		#[test]
1196		fn arkoor_without_signature() {
1197			let tap_tweak = TapTweakHash::from_slice(&[0xabu8; 32]).unwrap();
1198			let transition = GenesisTransition::Arkoor(ArkoorGenesis {
1199				client_cosigners: vec![test_pubkey()],
1200				tap_tweak,
1201				signature: None,
1202			});
1203			encoding_roundtrip(&transition);
1204		}
1205	}
1206}