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 mod raw;
58pub(crate) mod genesis;
59mod validation;
60
61pub use self::validation::VtxoValidationError;
62pub use self::policy::{Policy, VtxoPolicy, VtxoPolicyKind, ServerVtxoPolicy};
63pub(crate) use self::genesis::{GenesisItem, GenesisTransition};
64pub use self::genesis::TransitionKind;
65
66pub use self::policy::{
67	PubkeyVtxoPolicy, CheckpointVtxoPolicy, ExpiryVtxoPolicy, HarkLeafVtxoPolicy,
68	HarkLeaf_v0_VtxoPolicy,
69	ServerHtlcRecv_v0_VtxoPolicy, ServerHtlcSend_v0_VtxoPolicy, ServerHtlcRecvVtxoPolicy,
70	ServerHtlcSendVtxoPolicy,
71};
72pub use self::policy::clause::{
73	VtxoClause, DelayedSignClause, DelayedTimelockSignClause, HashDelaySignClause,
74	HashDelaySignClause_v0, TapScriptClause,
75};
76
77/// Type alias for a server-internal VTXO that may have policies without user pubkeys.
78pub type ServerVtxo<G = Bare> = Vtxo<G, ServerVtxoPolicy>;
79
80use std::borrow::Cow;
81use std::iter::FusedIterator;
82use std::{fmt, io};
83use std::str::FromStr;
84
85use bitcoin::{
86	taproot, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Weight, Witness
87};
88use bitcoin::absolute::LockTime;
89use bitcoin::hashes::{sha256, Hash};
90use bitcoin::secp256k1::{schnorr, PublicKey, XOnlyPublicKey};
91use bitcoin::taproot::TapTweakHash;
92
93use bitcoin_ext::{fee, BlockDelta, BlockHeight, NonStandardOutput, TxOutExt, P2TR_DUST, P2TR_DUST_SAT};
94
95use crate::vtxo::policy::{
96	check_block_delta, check_block_height, HarkForfeitVtxoPolicy, HarkForfeit_v0_VtxoPolicy,
97};
98use crate::encode::{
99	LengthPrefixedVector, MAX_VEC_SIZE, OversizedVectorError, ProtocolDecodingError,
100	ProtocolEncoding, ReadExt, WriteExt,
101};
102use crate::lightning::PaymentHash;
103use crate::tree::signed::{UnlockHash, UnlockPreimage};
104
105/// VTXO dust is the same as [P2TR_DUST_SAT] because all outputs are P2TR
106pub const VTXO_DUST_SAT: u64 = P2TR_DUST_SAT;
107/// VTXO dust is the same as [P2TR_DUST] because all outputs are P2TR
108pub const VTXO_DUST: Amount = P2TR_DUST;
109
110/// The total signed tx weight of a exit tx.
111pub const EXIT_TX_WEIGHT: Weight = Weight::from_vb_unchecked(124);
112
113/// The current version of the vtxo encoding.
114const VTXO_ENCODING_VERSION: u16 = 2;
115/// The version before a fee amount was added to each genesis item.
116const VTXO_NO_FEE_AMOUNT_VERSION: u16 = 1;
117
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
120#[error("failed to parse vtxo id, must be 36 bytes")]
121pub struct VtxoIdParseError;
122
123/// Reason a [Vtxo] failed the [Vtxo::check_standard] check.
124///
125/// A VTXO is standard if and only if every output in its exit chain — its
126/// own output plus all sibling outputs of every exit transaction — uses a
127/// known script type *and* carries a value at or above that script's dust
128/// limit. Each variant identifies the first violation encountered.
129///
130/// Sibling-typed variants ([VtxoStandardnessError::DustSibling] and
131/// [VtxoStandardnessError::ScriptSibling]) locate the offending output by
132/// genesis item index plus the index inside that item's `other_outputs`
133/// list; with `item_count` they tell you how deep along the chain the
134/// problem sits.
135#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
136pub enum VtxoStandardnessError {
137	/// The VTXO's own output value is below the dust limit for its script
138	/// type, so the final exit transaction cannot be relayed.
139	#[error("the VTXO's own output is below the dust limit for its script type")]
140	Dusty,
141
142	/// A sibling output produced somewhere along the exit chain is below
143	/// the dust limit. The current VTXO can clear dust on its own and
144	/// still trip this variant — the exit transaction containing the
145	/// sub-dust sibling is the part that won't relay.
146	///
147	/// # Example: a small total dust-isolation can't rescue
148	///
149	/// Suppose Alice owns a 600-sat VTXO and pays Bob 200 sat. The
150	/// arkoor builder produces:
151	///
152	/// ```text
153	/// 600 sat  ->  200 sat   // Bob (sub-dust — below P2TR_DUST = 330)
154	///              400 sat   // Alice's change (above dust)
155	/// ```
156	///
157	/// Even when the builder is asked to apply dust isolation (see
158	/// [`ArkoorBuilder::new_with_checkpoint_isolate_dust`](crate::arkoor::ArkoorBuilder::new_with_checkpoint_isolate_dust)),
159	/// the total is too small to fix. An isolation output has to be
160	/// at least `P2TR_DUST` (330 sat) to clear the relay limit. Bob's
161	/// 200 sat already lives in the dust pool; to reach 330 the builder
162	/// would have to split Alice's change and pull another 130 sat into
163	/// the pool. That leaves 270 sat as the leftover piece of Alice's
164	/// change — still sub-dust. Splitting trades one sub-dust output
165	/// for two, so the builder falls through and emits the 200/400
166	/// outputs as-is.
167	#[error("dust sibling output at genesis item {item_idx}/{item_count}, output {output_idx}")]
168	DustSibling {
169		item_idx: usize,
170		item_count: usize,
171		output_idx: usize,
172	},
173
174	/// The VTXO's own output uses an unrecognised script type, or an
175	/// OP_RETURN longer than the 83-byte standardness ceiling.
176	#[error("the VTXO's own output uses a non-standard script type")]
177	Script,
178
179	/// A sibling output along the exit chain uses an unrecognised script
180	/// type (or an over-long OP_RETURN). Same locator semantics as
181	/// [VtxoStandardnessError::DustSibling].
182	#[error("non-standard script in sibling output at genesis item {item_idx}/{item_count}, output {output_idx}")]
183	ScriptSibling {
184		item_idx: usize,
185		item_count: usize,
186		output_idx: usize,
187	},
188}
189
190#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
191pub struct VtxoId([u8; 36]);
192
193impl VtxoId {
194	/// Size in bytes of an encoded [VtxoId].
195	pub const ENCODE_SIZE: usize = 36;
196
197	/// Parse from bytes
198	pub fn from_slice(b: &[u8]) -> Result<VtxoId, VtxoIdParseError> {
199		if b.len() == 36 {
200			let mut ret = [0u8; 36];
201			ret[..].copy_from_slice(&b[0..36]);
202			Ok(Self(ret))
203		} else {
204			Err(VtxoIdParseError)
205		}
206	}
207
208	/// Get the [OutPoint] representation of this [VtxoId]
209	pub fn to_point(&self) -> OutPoint {
210		let txid = Txid::from_byte_array(self.0[0..32].try_into().expect("32 bytes"));
211		let vout_bytes = [self.0[32], self.0[33], self.0[34], self.0[35]];
212		let vout = u32::from_le_bytes(vout_bytes);
213		OutPoint::new(txid, vout)
214	}
215
216	#[deprecated(since = "0.1.3", note = "use to_point instead")]
217	pub fn utxo(self) -> OutPoint {
218		self.to_point()
219	}
220
221	/// Serialize to bytes
222	pub fn to_bytes(self) -> [u8; 36] {
223		self.0
224	}
225}
226
227impl From<OutPoint> for VtxoId {
228	fn from(p: OutPoint) -> VtxoId {
229		let mut ret = [0u8; 36];
230		ret[0..32].copy_from_slice(&p.txid[..]);
231		ret[32..].copy_from_slice(&p.vout.to_le_bytes());
232		VtxoId(ret)
233	}
234}
235
236impl AsRef<[u8]> for VtxoId {
237	fn as_ref(&self) -> &[u8] {
238		&self.0
239	}
240}
241
242impl fmt::Display for VtxoId {
243	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
244		fmt::Display::fmt(&self.to_point(), f)
245	}
246}
247
248impl fmt::Debug for VtxoId {
249	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
250		fmt::Display::fmt(self, f)
251	}
252}
253
254impl FromStr for VtxoId {
255	type Err = VtxoIdParseError;
256	fn from_str(s: &str) -> Result<Self, Self::Err> {
257		Ok(OutPoint::from_str(s).map_err(|_| VtxoIdParseError)?.into())
258	}
259}
260
261impl serde::Serialize for VtxoId {
262	fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
263		if s.is_human_readable() {
264			s.collect_str(self)
265		} else {
266			s.serialize_bytes(self.as_ref())
267		}
268	}
269}
270
271impl<'de> serde::Deserialize<'de> for VtxoId {
272	fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
273		struct Visitor;
274		impl<'de> serde::de::Visitor<'de> for Visitor {
275			type Value = VtxoId;
276			fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277				write!(f, "a VtxoId")
278			}
279			fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
280				VtxoId::from_slice(v).map_err(serde::de::Error::custom)
281			}
282			fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
283				VtxoId::from_str(v).map_err(serde::de::Error::custom)
284			}
285		}
286		if d.is_human_readable() {
287			d.deserialize_str(Visitor)
288		} else {
289			d.deserialize_bytes(Visitor)
290		}
291	}
292}
293
294impl ProtocolEncoding for VtxoId {
295	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
296		w.emit_slice(&self.0)
297	}
298	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
299		let array: [u8; 36] = r.read_byte_array()
300			.map_err(|_| ProtocolDecodingError::invalid("invalid vtxo id. Expected 36 bytes"))?;
301
302		Ok(VtxoId(array))
303	}
304}
305
306/// Create an exit tx.
307///
308/// When the `signature` argument is provided,
309/// it will be placed in the input witness.
310pub fn create_exit_tx(
311	prevout: OutPoint,
312	output: TxOut,
313	signature: Option<&schnorr::Signature>,
314	fee: Amount,
315) -> Transaction {
316	Transaction {
317		version: bitcoin::transaction::Version(3),
318		lock_time: LockTime::ZERO,
319		input: vec![TxIn {
320			previous_output: prevout,
321			script_sig: ScriptBuf::new(),
322			sequence: Sequence::ZERO,
323			witness: {
324				let mut ret = Witness::new();
325				if let Some(sig) = signature {
326					ret.push(&sig[..]);
327				}
328				ret
329			},
330		}],
331		output: vec![output, fee::fee_anchor_with_amount(fee)],
332	}
333}
334
335/// Enum type used to represent a preimage<>hash relationship
336/// for which the preimage might be known but the hash always
337/// should be known.
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339pub(crate) enum MaybePreimage {
340	Preimage([u8; 32]),
341	Hash(sha256::Hash),
342}
343
344impl MaybePreimage {
345	/// Get the hash
346	pub fn hash(&self) -> sha256::Hash {
347		match self {
348			Self::Preimage(p) => sha256::Hash::hash(p),
349			Self::Hash(h) => *h,
350		}
351	}
352}
353
354/// Type of the items yielded by [VtxoTxIter], the iterator returned by
355/// [Vtxo::transactions].
356#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
357pub struct VtxoTxIterItem {
358	/// The actual transaction.
359	pub tx: Transaction,
360	/// The index of the relevant output of this tx
361	pub output_idx: usize,
362}
363
364/// Iterator returned by [Vtxo::transactions].
365pub struct VtxoTxIter<'a, P: Policy = VtxoPolicy> {
366	vtxo: &'a Vtxo<Full, P>,
367
368	prev: OutPoint,
369	genesis_idx: usize,
370	current_amount: Amount,
371}
372
373impl<'a, P: Policy> VtxoTxIter<'a, P> {
374	fn new(vtxo: &'a Vtxo<Full, P>) -> VtxoTxIter<'a, P> {
375		// Add all the amounts that go into the other outputs.
376		let onchain_amount = vtxo.chain_anchor_amount()
377			.expect("This should only fail if the VTXO is invalid.");
378		VtxoTxIter {
379			prev: vtxo.anchor_point,
380			vtxo: vtxo,
381			genesis_idx: 0,
382			current_amount: onchain_amount,
383		}
384	}
385}
386
387impl<'a, P: Policy> Iterator for VtxoTxIter<'a, P> {
388	type Item = VtxoTxIterItem;
389
390	fn next(&mut self) -> Option<Self::Item> {
391		let item = self.vtxo.genesis.items.get(self.genesis_idx)?;
392		let next_amount = self.current_amount.checked_sub(
393			item.other_output_sum().expect("we calculated this amount beforehand")
394		).expect("we calculated this amount beforehand");
395
396		let next_output = if let Some(item) = self.vtxo.genesis.items.get(self.genesis_idx.saturating_add(1)) {
397			item.transition.input_txout(
398				next_amount,
399				self.vtxo.server_pubkey,
400				self.vtxo.expiry_height,
401				self.vtxo.exit_delta,
402			)
403		} else {
404			// when we reach the end of the chain, we take the eventual output of the vtxo
405			self.vtxo.policy.txout(
406				self.vtxo.amount,
407				self.vtxo.server_pubkey,
408				self.vtxo.exit_delta,
409				self.vtxo.expiry_height,
410			)
411		};
412
413		let tx = item.tx(self.prev, next_output, self.vtxo.server_pubkey, self.vtxo.expiry_height);
414		self.prev = OutPoint::new(tx.compute_txid(), item.output_idx as u32);
415		self.genesis_idx = self.genesis_idx.saturating_add(1);
416		self.current_amount = next_amount;
417		let output_idx = item.output_idx as usize;
418		Some(VtxoTxIterItem { tx, output_idx })
419	}
420
421	fn size_hint(&self) -> (usize, Option<usize>) {
422		let len = self.vtxo.genesis.items.len().saturating_sub(self.genesis_idx);
423		(len, Some(len))
424	}
425}
426
427impl<'a, P: Policy> ExactSizeIterator for VtxoTxIter<'a, P> {}
428impl<'a, P: Policy> FusedIterator for VtxoTxIter<'a, P> {}
429
430/// Representing "bare" VTXOs that are just output details without genesis
431#[derive(Debug, Clone)]
432pub struct Bare;
433
434/// Representing "full" VTXOs that contain the full genesis
435#[derive(Debug, Clone)]
436pub struct Full {
437	pub(crate) items: Vec<genesis::GenesisItem>,
438}
439
440/// Represents a VTXO in the Ark.
441///
442/// The correctness of the return values of methods on this type is conditional
443/// on the VTXO being valid. For invalid VTXOs, the methods should never panic,
444/// but can return incorrect values.
445/// It is advised to always validate a VTXO upon receipt using [Vtxo::validate].
446///
447/// Be mindful of calling [Clone] on a [Vtxo], as they can be of
448/// non-negligible size. It is advised to use references where possible
449/// or use an [std::rc::Rc] or [std::sync::Arc] if needed.
450///
451/// Implementations of [PartialEq], [Eq], [PartialOrd], [Ord] and [Hash] are
452/// proxied to the implementation on [Vtxo::id].
453#[derive(Debug, Clone)]
454pub struct Vtxo<G = Full, P = VtxoPolicy> {
455	pub(crate) policy: P,
456	pub(crate) amount: Amount,
457	pub(crate) expiry_height: BlockHeight,
458
459	pub(crate) server_pubkey: PublicKey,
460	pub(crate) exit_delta: BlockDelta,
461
462	pub(crate) anchor_point: OutPoint,
463	/// The genesis is generic and can be either present or not
464	pub(crate) genesis: G,
465
466	/// The resulting actual "point" of the VTXO. I.e. the output of the last
467	/// exit tx of this VTXO.
468	///
469	/// We keep this for two reasons:
470	/// - the ID is based on this, so it should be cheaply accessible
471	/// - it forms as a good checksum for all the internal genesis data
472	pub(crate) point: OutPoint,
473}
474
475impl<G, P: Policy> Vtxo<G, P> {
476	/// Get the identifier for this [Vtxo].
477	///
478	/// This is the same as [Vtxo::point] but encoded as a byte array.
479	pub fn id(&self) -> VtxoId {
480		self.point.into()
481	}
482
483	/// The outpoint from which to build forfeit or arkoor txs.
484	///
485	/// This can be an on-chain utxo or an off-chain vtxo.
486	pub fn point(&self) -> OutPoint {
487		self.point
488	}
489
490	/// The amount of the [Vtxo].
491	pub fn amount(&self) -> Amount {
492		self.amount
493	}
494
495	/// The UTXO that should be confirmed for this [Vtxo] to be valid.
496	///
497	/// It is the very root of the VTXO.
498	pub fn chain_anchor(&self) -> OutPoint {
499		self.anchor_point
500	}
501
502	/// The output policy of this VTXO.
503	pub fn policy(&self) -> &P {
504		&self.policy
505	}
506
507	/// The output policy type of this VTXO.
508	pub fn policy_type(&self) -> VtxoPolicyKind {
509		self.policy.policy_type()
510	}
511
512	/// The expiry height of the [Vtxo].
513	pub fn expiry_height(&self) -> BlockHeight {
514		self.expiry_height
515	}
516
517	/// The server pubkey used in arkoor transitions.
518	pub fn server_pubkey(&self) -> PublicKey {
519		self.server_pubkey
520	}
521
522	/// The relative timelock block delta used for exits.
523	pub fn exit_delta(&self) -> BlockDelta {
524		self.exit_delta
525	}
526
527	/// The taproot spend info for the output of this [Vtxo].
528	pub fn output_taproot(&self) -> taproot::TaprootSpendInfo {
529		self.policy.taproot(self.server_pubkey, self.exit_delta, self.expiry_height)
530	}
531
532	/// The scriptPubkey of the output of this [Vtxo].
533	pub fn output_script_pubkey(&self) -> ScriptBuf {
534		self.policy.script_pubkey(self.server_pubkey, self.exit_delta, self.expiry_height)
535	}
536
537	/// The transaction output (eventual UTXO) of this [Vtxo].
538	pub fn txout(&self) -> TxOut {
539		self.policy.txout(self.amount, self.server_pubkey, self.exit_delta, self.expiry_height)
540	}
541
542	/// Convert to a bare VTXO, `Vtxo<Bare>`
543	pub fn to_bare(&self) -> Vtxo<Bare, P> {
544		Vtxo {
545			point: self.point,
546			policy: self.policy.clone(),
547			amount: self.amount,
548			expiry_height: self.expiry_height,
549			server_pubkey: self.server_pubkey,
550			exit_delta: self.exit_delta,
551			anchor_point: self.anchor_point,
552			genesis: Bare,
553		}
554	}
555
556	/// Convert into a bare VTXO, `Vtxo<Bare>`
557	pub fn into_bare(self) -> Vtxo<Bare, P> {
558		Vtxo {
559			point: self.point,
560			policy: self.policy,
561			amount: self.amount,
562			expiry_height: self.expiry_height,
563			server_pubkey: self.server_pubkey,
564			exit_delta: self.exit_delta,
565			anchor_point: self.anchor_point,
566			genesis: Bare,
567		}
568	}
569}
570
571impl<P: Policy> Vtxo<Bare, P> {
572	/// Construct a bare VTXO from its individual fields.
573	pub fn new(
574		point: OutPoint,
575		policy: P,
576		amount: Amount,
577		expiry_height: BlockHeight,
578		server_pubkey: PublicKey,
579		exit_delta: BlockDelta,
580		anchor_point: OutPoint,
581	) -> Self {
582		Vtxo { point, policy, amount, expiry_height, server_pubkey, exit_delta, anchor_point, genesis: Bare }
583	}
584
585	/// Upgrade this bare VTXO to a [Vtxo<Full, P>] by attaching a previously
586	/// stripped or decoded genesis chain.
587	///
588	/// Field-by-field copy mirroring the inverse of [Vtxo::into_bare]. The
589	/// VTXO's `point` is a deterministic checksum of the genesis chain, so a
590	/// caller passing a mismatched `genesis` would simply produce an invalid
591	/// VTXO.
592	///
593	/// A [VtxoValidationError::MissingGenesisItems] will be returned if the provided `genesis`
594	/// contains no genesis transitions and the [Vtxo::point] and [Vtxo::chain_anchor] are not
595	/// equal.
596	///
597	/// No further validation of the VTXO will be performed. It's recommended to run
598	/// [Vtxo::validate] to ensure the VTXO data is consistent with the provided `genesis`.
599	pub fn with_genesis(self, genesis: Full) -> Result<Vtxo<Full, P>, VtxoValidationError> {
600		// Allow VTXOs with no genesis items if the chain anchor is equal to the VTXO point. This
601		// is effectively a virtual representation of a UTXO.
602		if self.point() != self.chain_anchor() {
603			if genesis.items.is_empty() {
604				return Err(VtxoValidationError::MissingGenesisItems);
605			}
606		}
607		else {
608			if !genesis.items.is_empty() {
609				return Err(VtxoValidationError::UnexpectedGenesisItems);
610			}
611		}
612		Ok(Vtxo {
613			policy: self.policy,
614			amount: self.amount,
615			expiry_height: self.expiry_height,
616			server_pubkey: self.server_pubkey,
617			exit_delta: self.exit_delta,
618			anchor_point: self.anchor_point,
619			genesis,
620			point: self.point,
621		})
622	}
623}
624
625// Pins the invariant `exit_depth` relies on: `Full::decode` caps the genesis item
626// count at `MAX_VEC_SIZE / size_of::<GenesisItem>()` via `OversizedVectorError`, so
627// the count must stay within u16 or the cast below could panic.
628const _: () = assert!(
629	MAX_VEC_SIZE / core::mem::size_of::<GenesisItem>() <= u16::MAX as usize,
630	"genesis decode cap must keep items.len() within u16 for Vtxo::exit_depth",
631);
632
633impl<P: Policy> Vtxo<Full, P> {
634	/// Returns the total exit depth (including OOR depth) of the vtxo.
635	pub fn exit_depth(&self) -> u16 {
636		// The genesis item count is the VTXO's exit depth, bounded far below
637		// u16::MAX both by construction and, on decode, by the allocation cap
638		// enforced via OversizedVectorError on the genesis vector.
639		u16::try_from(self.genesis.items.len())
640			.expect("genesis item count fits in u16")
641	}
642
643	/// Iterate over all oor transitions in this VTXO
644	///
645	/// The outer `Vec` cointains one element for each transition.
646	/// The inner `Vec` contains all pubkeys within that transition.
647	///
648	/// This does not include the current arkoor pubkey, for that use
649	/// [Vtxo::arkoor_pubkey].
650	pub fn past_arkoor_pubkeys(&self) -> Vec<Vec<PublicKey>> {
651		self.genesis.items.iter().filter_map(|g| {
652			match &g.transition {
653				// NB in principle, a genesis item's transition MUST have
654				// an arkoor pubkey, otherwise the vtxo is invalid
655				GenesisTransition::Arkoor(inner) => Some(inner.client_cosigners().collect()),
656				_ => None,
657			}
658		}).collect()
659	}
660
661	/// Whether all transaction witnesses are present
662	///
663	/// It is possible to represent unsigned or otherwise unfinished VTXOs,
664	/// for which this method will return false.
665	pub fn has_all_witnesses(&self) -> bool {
666		self.genesis.items.iter().all(|g| g.transition.has_all_witnesses())
667	}
668
669	/// Check if this VTXO is standard for relay purposes
670	///
671	/// A VTXO is standard if:
672	/// - Its own output is standard
673	/// - all sibling outputs in the exit path are standard
674	/// - each part of the exit path should have a P2A output
675	///
676	/// See [Vtxo::check_standard] for a variant returning a descriptive
677	/// error instead of a bool.
678	pub fn is_standard(&self) -> bool {
679		self.check_standard().is_ok()
680	}
681
682	/// Like [Vtxo::is_standard] but returns a [VtxoStandardnessError]
683	/// describing the first standardness violation along the exit chain.
684	///
685	/// The check is short-circuited: it returns the *first* offending
686	/// output rather than enumerating every problem. The VTXO's own
687	/// output is checked before its siblings.
688	pub fn check_standard(&self) -> Result<(), VtxoStandardnessError> {
689		if let Err(kind) = self.txout().check_standard() {
690			return Err(match kind {
691				NonStandardOutput::Dust => VtxoStandardnessError::Dusty,
692				NonStandardOutput::Script => VtxoStandardnessError::Script,
693			});
694		}
695		let item_count = self.genesis.items.len();
696		for (item_idx, item) in self.genesis.items.iter().enumerate() {
697			for (output_idx, out) in item.other_outputs.iter().enumerate() {
698				if let Err(kind) = out.check_standard() {
699					return Err(match kind {
700						NonStandardOutput::Dust => VtxoStandardnessError::DustSibling {
701							item_idx, item_count, output_idx,
702						},
703						NonStandardOutput::Script => VtxoStandardnessError::ScriptSibling {
704							item_idx, item_count, output_idx,
705						},
706					});
707				}
708			}
709		}
710		Ok(())
711	}
712
713	/// Returns the "hArk" unlock hash if this is a hArk leaf VTXO
714	pub fn unlock_hash(&self) -> Option<UnlockHash> {
715		match self.genesis.items.last()?.transition {
716			GenesisTransition::HashLockedCosigned(ref inner) => Some(inner.unlock.hash()),
717			GenesisTransition::HashLockedCosigned_v0(ref inner) => Some(inner.unlock.hash()),
718			_ => None,
719		}
720	}
721
722	/// Provide the leaf signature for an unfinalized hArk VTXO
723	///
724	/// Returns true if this VTXO was an unfinalized hArk VTXO.
725	pub fn provide_unlock_signature(&mut self, signature: schnorr::Signature) -> bool {
726		match self.genesis.items.last_mut().map(|g| &mut g.transition) {
727			Some(GenesisTransition::HashLockedCosigned(inner)) => {
728				inner.signature.replace(signature);
729				true
730			},
731			Some(GenesisTransition::HashLockedCosigned_v0(inner)) => {
732				inner.signature.replace(signature);
733				true
734			},
735			_ => false,
736		}
737	}
738
739	/// Provide the unlock preimage for an unfinalized hArk VTXO
740	///
741	/// Returns true if this VTXO was an unfinalized hArk VTXO and the preimage matched.
742	pub fn provide_unlock_preimage(&mut self, preimage: UnlockPreimage) -> bool {
743		match self.genesis.items.last_mut().map(|g| &mut g.transition) {
744			Some(GenesisTransition::HashLockedCosigned(ref mut inner)) => {
745				if inner.unlock.hash() == UnlockHash::hash(&preimage) {
746					inner.unlock = MaybePreimage::Preimage(preimage);
747					true
748				} else {
749					false
750				}
751			},
752			Some(GenesisTransition::HashLockedCosigned_v0(ref mut inner)) => {
753				if inner.unlock.hash() == UnlockHash::hash(&preimage) {
754					inner.unlock = MaybePreimage::Preimage(preimage);
755					true
756				} else {
757					false
758				}
759			},
760			_ => false,
761		}
762	}
763
764	/// Iterator that constructs all the exit txs for this [Vtxo].
765	pub fn transactions(&self) -> VtxoTxIter<'_, P> {
766		VtxoTxIter::new(self)
767	}
768
769	/// Encode just the genesis chain.
770	///
771	/// The wire format is the same as the genesis section embedded inside a
772	/// full VTXO encoding at `VTXO_ENCODING_VERSION`, so callers that already
773	/// store a `Vtxo<Bare>` alongside this blob can reassemble the full VTXO
774	/// via [Vtxo::deserialize_with_genesis].
775	pub fn encode_genesis<W: io::Write + ?Sized>(
776		&self,
777		w: &mut W,
778	) -> Result<(), io::Error> {
779		Full::encode(&self.genesis, w, VTXO_ENCODING_VERSION)
780	}
781
782	/// Similar to `Vtxo::deserialize` but it takes two byte splices, one containing `Vtxo<Bare>`
783	/// data and one for the `Full` genesis data.
784	pub fn deserialize_with_genesis(
785		mut vtxo_bytes: &[u8],
786		mut genesis_bytes: &[u8],
787	) -> Result<Self, ProtocolDecodingError>
788	where
789		P: ProtocolEncoding,
790	{
791		let (vtxo, version) = vtxo_decode_inner::<Bare, P, _>(&mut vtxo_bytes)?;
792		let genesis = Full::decode(&mut genesis_bytes, version)?;
793		vtxo.with_genesis(genesis)
794			.map_err(|e| ProtocolDecodingError::invalid_err(
795				e, "unable to decode VTXO with genesis",
796			))
797	}
798
799	/// Serialize the genesis chain into a fresh `Vec<u8>`.
800	pub fn serialize_genesis(&self) -> Vec<u8> {
801		let mut out = Vec::new();
802		self.encode_genesis(&mut out).expect("writing to a Vec doesn't fail");
803		out
804	}
805
806	/// Fully validate this VTXO and its entire transaction chain.
807	///
808	/// The `chain_anchor_tx` must be the tx with txid matching
809	/// [Vtxo::chain_anchor].
810	pub fn validate(
811		&self,
812		chain_anchor_tx: &Transaction,
813	) -> Result<(), VtxoValidationError> {
814		self::validation::validate(self, chain_anchor_tx)
815	}
816
817	/// Validate VTXO structure without checking signatures.
818	pub fn validate_unsigned(
819		&self,
820		chain_anchor_tx: &Transaction,
821	) -> Result<(), VtxoValidationError> {
822		self::validation::validate_unsigned(self, chain_anchor_tx)
823	}
824
825	/// Calculates the onchain amount for the [Vtxo].
826	///
827	/// Returns `None` if any overflow occurs. This should be impossible for any VTXO that is valid.
828	pub(crate) fn chain_anchor_amount(&self) -> Option<Amount> {
829		self.amount.checked_add(self.genesis.items.iter().try_fold(Amount::ZERO, |sum, i| {
830			i.other_output_sum().and_then(|amt| sum.checked_add(amt))
831		})?)
832	}
833
834	/// The ids of every intermediate output in this VTXO's genesis chain — a
835	/// *superset* of the ancestor VTXOs it (directly or transitively) spent.
836	///
837	/// [`Vtxo::transactions`] walks the chain from the anchor down to this VTXO;
838	/// we return the output of every tx but the last (the last produces this VTXO
839	/// itself). The chain also holds intermediate transition outputs (e.g.
840	/// checkpoints) that were never owned VTXOs, hence a superset: it contains
841	/// every owned ancestor id, but not every id it returns is one.
842	pub fn ancestor_ids(&self) -> Vec<VtxoId> {
843		let items = self.transactions().collect::<Vec<_>>();
844		// The last item is this VTXO itself, so we don't need to include it
845		let ancestor_count = items.len().saturating_sub(1);
846		items.iter()
847			.take(ancestor_count)
848			.map(|item| OutPoint::new(item.tx.compute_txid(), item.output_idx as u32).into())
849			.collect()
850	}
851}
852
853impl<G> Vtxo<G, VtxoPolicy> {
854	/// Returns the user pubkey associated with this [Vtxo].
855	pub fn user_pubkey(&self) -> PublicKey {
856		self.policy.user_pubkey()
857	}
858
859	/// The public key used to cosign arkoor txs spending this [Vtxo].
860	/// This will return [None] if [VtxoPolicy::is_arkoor_compatible] returns false
861	/// for this VTXO's policy.
862	pub fn arkoor_pubkey(&self) -> Option<PublicKey> {
863		self.policy.arkoor_pubkey()
864	}
865}
866
867impl Vtxo<Full, VtxoPolicy> {
868	/// Shortcut to fully finalize a hark leaf using both keys
869	#[cfg(any(test, feature = "test-util"))]
870	pub fn finalize_hark_leaf(
871		&mut self,
872		user_key: &bitcoin::secp256k1::Keypair,
873		server_key: &bitcoin::secp256k1::Keypair,
874		chain_anchor: &Transaction,
875		unlock_preimage: UnlockPreimage,
876	) {
877		use crate::tree::signed::{LeafVtxoCosignContext, LeafVtxoCosignResponse};
878
879		// first sign and provide the signature
880		let (ctx, req) = LeafVtxoCosignContext::new(self, chain_anchor, user_key);
881		let cosign = LeafVtxoCosignResponse::new_cosign(&req, self, chain_anchor, server_key);
882		assert!(ctx.finalize(self, cosign));
883		// then provide preimage
884		assert!(self.provide_unlock_preimage(unlock_preimage));
885	}
886}
887
888impl<G> Vtxo<G, ServerVtxoPolicy> {
889	/// Try to convert into a user [Vtxo]
890	///
891	/// Returns the original value on failure.
892	pub fn try_into_user_vtxo(self) -> Result<Vtxo<G, VtxoPolicy>, ServerVtxo<G>> {
893		if let Some(p) = self.policy.clone().into_user_policy() {
894			Ok(Vtxo {
895				policy: p,
896				amount: self.amount,
897				expiry_height: self.expiry_height,
898				server_pubkey: self.server_pubkey,
899				exit_delta: self.exit_delta,
900				anchor_point: self.anchor_point,
901				genesis: self.genesis,
902				point: self.point,
903			})
904		} else {
905			Err(self)
906		}
907	}
908}
909
910impl<G, P: Policy> PartialEq for Vtxo<G, P> {
911	fn eq(&self, other: &Self) -> bool {
912		PartialEq::eq(&self.id(), &other.id())
913	}
914}
915
916impl<G, P: Policy> Eq for Vtxo<G, P> {}
917
918impl<G, P: Policy> PartialOrd for Vtxo<G, P> {
919	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
920		PartialOrd::partial_cmp(&self.id(), &other.id())
921	}
922}
923
924impl<G, P: Policy> Ord for Vtxo<G, P> {
925	fn cmp(&self, other: &Self) -> std::cmp::Ordering {
926		Ord::cmp(&self.id(), &other.id())
927	}
928}
929
930impl<G, P: Policy> std::hash::Hash for Vtxo<G, P> {
931	fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
932		std::hash::Hash::hash(&self.id(), state)
933	}
934}
935
936impl<G, P: Policy> AsRef<Vtxo<G, P>> for Vtxo<G, P> {
937	fn as_ref(&self) -> &Vtxo<G, P> {
938	    self
939	}
940}
941
942impl<G> From<Vtxo<G>> for ServerVtxo<G> {
943	fn from(vtxo: Vtxo<G>) -> ServerVtxo<G> {
944		ServerVtxo {
945			policy: vtxo.policy.into(),
946			amount: vtxo.amount,
947			expiry_height: vtxo.expiry_height,
948			server_pubkey: vtxo.server_pubkey,
949			exit_delta: vtxo.exit_delta,
950			anchor_point: vtxo.anchor_point,
951			genesis: vtxo.genesis,
952			point: vtxo.point,
953		}
954	}
955}
956
957/// Implemented on anything that is kinda a [Vtxo]
958pub trait VtxoRef<P: Policy = VtxoPolicy> {
959	/// The [VtxoId] of the VTXO
960	fn vtxo_id(&self) -> VtxoId;
961
962	/// If the bare [Vtxo] can be provided, provides it by reference
963	fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { None }
964
965	/// If the full [Vtxo] can be provided, provides it by reference
966	fn as_full_vtxo(&self) -> Option<&Vtxo<Full, P>> { None }
967
968	/// If the full [Vtxo] can be provided, provides it by value, either directly or via cloning
969	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> where Self: Sized;
970}
971
972impl<P: Policy> VtxoRef<P> for VtxoId {
973	fn vtxo_id(&self) -> VtxoId { *self }
974	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
975}
976
977impl<'a, P: Policy> VtxoRef<P> for &'a VtxoId {
978	fn vtxo_id(&self) -> VtxoId { **self }
979	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
980}
981
982impl<P: Policy> VtxoRef<P> for Vtxo<Bare, P> {
983	fn vtxo_id(&self) -> VtxoId { self.id() }
984	fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Borrowed(self)) }
985	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
986}
987
988impl<'a, P: Policy> VtxoRef<P> for &'a Vtxo<Bare, P> {
989	fn vtxo_id(&self) -> VtxoId { self.id() }
990	fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Borrowed(*self)) }
991	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
992}
993
994impl<P: Policy> VtxoRef<P> for Vtxo<Full, P> {
995	fn vtxo_id(&self) -> VtxoId { self.id() }
996	fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Owned(self.to_bare())) }
997	fn as_full_vtxo(&self) -> Option<&Vtxo<Full, P>> { Some(self) }
998	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { Some(self) }
999}
1000
1001impl<'a, P: Policy> VtxoRef<P> for &'a Vtxo<Full, P> {
1002	fn vtxo_id(&self) -> VtxoId { self.id() }
1003	fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Owned(self.to_bare())) }
1004	fn as_full_vtxo(&self) -> Option<&Vtxo<Full, P>> { Some(*self) }
1005	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { Some(self.clone()) }
1006}
1007
1008/// The byte used to encode the [VtxoPolicy::Pubkey] output type.
1009const VTXO_POLICY_PUBKEY: u8 = 0x00;
1010
1011/// The byte used to encode the [VtxoPolicy::ServerHtlcSend_v0] output type.
1012const VTXO_POLICY_SERVER_HTLC_SEND_V0: u8 = 0x01;
1013
1014/// The byte used to encode the [VtxoPolicy::ServerHtlcRecv_v0] output type.
1015const VTXO_POLICY_SERVER_HTLC_RECV_V0: u8 = 0x02;
1016
1017/// The byte used to encode the [ServerVtxoPolicy::Checkpoint] output type.
1018const VTXO_POLICY_CHECKPOINT: u8 = 0x03;
1019
1020/// The byte used to encode the [ServerVtxoPolicy::Expiry] output type.
1021const VTXO_POLICY_EXPIRY: u8 = 0x04;
1022
1023/// The byte used to encode the [ServerVtxoPolicy::HarkLeaf_v0] output type.
1024const VTXO_POLICY_HARK_LEAF_V0: u8 = 0x05;
1025
1026/// The byte used to encode the [ServerVtxoPolicy::HarkForfeit_v0] output type.
1027const VTXO_POLICY_HARK_FORFEIT_V0: u8 = 0x06;
1028
1029/// The byte used to encode the [ServerVtxoPolicy::ServerOwned] output type.
1030const VTXO_POLICY_SERVER_OWNED: u8 = 0x07;
1031
1032/// The byte used to encode the [VtxoPolicy::ServerHtlcRecv] output type.
1033const VTXO_POLICY_SERVER_HTLC_RECV: u8 = 0x08;
1034
1035/// The byte used to encode the [VtxoPolicy::ServerHtlcSend] output type.
1036const VTXO_POLICY_SERVER_HTLC_SEND: u8 = 0x09;
1037
1038/// The byte used to encode the [ServerVtxoPolicy::HarkLeaf] output type.
1039const VTXO_POLICY_HARK_LEAF: u8 = 0x0a;
1040
1041/// The byte used to encode the [ServerVtxoPolicy::HarkForfeit] output type.
1042const VTXO_POLICY_HARK_FORFEIT: u8 = 0x0b;
1043
1044impl ProtocolEncoding for VtxoPolicy {
1045	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
1046		match self {
1047			Self::Pubkey(PubkeyVtxoPolicy { user_pubkey }) => {
1048				w.emit_u8(VTXO_POLICY_PUBKEY)?;
1049				user_pubkey.encode(w)?;
1050			},
1051			Self::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, payment_hash, htlc_expiry }) => {
1052				w.emit_u8(VTXO_POLICY_SERVER_HTLC_SEND)?;
1053				user_pubkey.encode(w)?;
1054				payment_hash.to_sha256_hash().encode(w)?;
1055				w.emit_u32(*htlc_expiry)?;
1056			},
1057			Self::ServerHtlcSend_v0(ServerHtlcSend_v0_VtxoPolicy { user_pubkey, payment_hash, htlc_expiry }) => {
1058				w.emit_u8(VTXO_POLICY_SERVER_HTLC_SEND_V0)?;
1059				user_pubkey.encode(w)?;
1060				payment_hash.to_sha256_hash().encode(w)?;
1061				w.emit_u32(*htlc_expiry)?;
1062			},
1063			Self::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy {
1064				user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta,
1065			}) => {
1066				w.emit_u8(VTXO_POLICY_SERVER_HTLC_RECV)?;
1067				user_pubkey.encode(w)?;
1068				payment_hash.to_sha256_hash().encode(w)?;
1069				w.emit_u32(*htlc_expiry)?;
1070				w.emit_u16(*htlc_expiry_delta)?;
1071			},
1072			Self::ServerHtlcRecv_v0(ServerHtlcRecv_v0_VtxoPolicy {
1073				user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta,
1074			}) => {
1075				w.emit_u8(VTXO_POLICY_SERVER_HTLC_RECV_V0)?;
1076				user_pubkey.encode(w)?;
1077				payment_hash.to_sha256_hash().encode(w)?;
1078				w.emit_u32(*htlc_expiry)?;
1079				w.emit_u16(*htlc_expiry_delta)?;
1080			},
1081		}
1082		Ok(())
1083	}
1084
1085	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
1086		let type_byte = r.read_u8()?;
1087		decode_vtxo_policy(type_byte, r)
1088	}
1089}
1090
1091/// Decode a [VtxoPolicy] with the given type byte
1092///
1093/// We have this function so it can be reused in [VtxoPolicy] and [ServerVtxoPolicy].
1094fn decode_vtxo_policy<R: io::Read + ?Sized>(
1095	type_byte: u8,
1096	r: &mut R,
1097) -> Result<VtxoPolicy, ProtocolDecodingError> {
1098	match type_byte {
1099		VTXO_POLICY_PUBKEY => {
1100			let user_pubkey = PublicKey::decode(r)?;
1101			Ok(VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey }))
1102		},
1103		VTXO_POLICY_SERVER_HTLC_SEND => {
1104			let user_pubkey = PublicKey::decode(r)?;
1105			let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
1106			let htlc_expiry = check_block_height(r.read_u32()?)
1107				.map_err(|e| ProtocolDecodingError::invalid_err(e, "htlc_expiry"))?;
1108			Ok(VtxoPolicy::ServerHtlcSend(ServerHtlcSendVtxoPolicy {
1109				user_pubkey, payment_hash, htlc_expiry,
1110			}))
1111		},
1112		VTXO_POLICY_SERVER_HTLC_SEND_V0 => {
1113			let user_pubkey = PublicKey::decode(r)?;
1114			let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
1115			let htlc_expiry = check_block_height(r.read_u32()?)
1116				.map_err(|e| ProtocolDecodingError::invalid_err(e, "htlc_expiry"))?;
1117			Ok(VtxoPolicy::ServerHtlcSend_v0(ServerHtlcSend_v0_VtxoPolicy { user_pubkey, payment_hash, htlc_expiry }))
1118		},
1119		VTXO_POLICY_SERVER_HTLC_RECV => {
1120			let user_pubkey = PublicKey::decode(r)?;
1121			let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
1122			let htlc_expiry = check_block_height(r.read_u32()?)
1123				.map_err(|e| ProtocolDecodingError::invalid_err(e, "htlc_expiry"))?;
1124			let htlc_expiry_delta = check_block_delta(r.read_u16()?)
1125				.map_err(|e| ProtocolDecodingError::invalid_err(e, "htlc_expiry_delta"))?;
1126			Ok(VtxoPolicy::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy {
1127				user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta,
1128			}))
1129		},
1130		VTXO_POLICY_SERVER_HTLC_RECV_V0 => {
1131			let user_pubkey = PublicKey::decode(r)?;
1132			let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
1133			let htlc_expiry = check_block_height(r.read_u32()?)
1134				.map_err(|e| ProtocolDecodingError::invalid_err(e, "htlc_expiry"))?;
1135			let htlc_expiry_delta = check_block_delta(r.read_u16()?)
1136				.map_err(|e| ProtocolDecodingError::invalid_err(e, "htlc_expiry_delta"))?;
1137			Ok(VtxoPolicy::ServerHtlcRecv_v0(ServerHtlcRecv_v0_VtxoPolicy { user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta }))
1138		},
1139
1140		// IMPORTANT:
1141		// When adding a new user vtxo policy variant, don't forget
1142		// to also add it to the ServerVtxoPolicy decode match arm.
1143
1144		v => Err(ProtocolDecodingError::invalid(format_args!(
1145			"invalid VtxoPolicy type byte: {v:#x}",
1146		))),
1147	}
1148}
1149
1150impl ProtocolEncoding for ServerVtxoPolicy {
1151	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
1152		match self {
1153			Self::User(p) => p.encode(w)?,
1154			Self::ServerOwned => {
1155				w.emit_u8(VTXO_POLICY_SERVER_OWNED)?;
1156			},
1157			Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }) => {
1158				w.emit_u8(VTXO_POLICY_CHECKPOINT)?;
1159				user_pubkey.encode(w)?;
1160			},
1161			Self::Expiry(ExpiryVtxoPolicy { internal_key }) => {
1162				w.emit_u8(VTXO_POLICY_EXPIRY)?;
1163				internal_key.encode(w)?;
1164			},
1165			Self::HarkLeaf(HarkLeafVtxoPolicy { user_pubkey, unlock_hash }) => {
1166				w.emit_u8(VTXO_POLICY_HARK_LEAF)?;
1167				user_pubkey.encode(w)?;
1168				unlock_hash.encode(w)?;
1169			},
1170			Self::HarkLeaf_v0(HarkLeaf_v0_VtxoPolicy { user_pubkey, unlock_hash }) => {
1171				w.emit_u8(VTXO_POLICY_HARK_LEAF_V0)?;
1172				user_pubkey.encode(w)?;
1173				unlock_hash.encode(w)?;
1174			},
1175			Self::HarkForfeit(HarkForfeitVtxoPolicy { user_pubkey, unlock_hash }) => {
1176				w.emit_u8(VTXO_POLICY_HARK_FORFEIT)?;
1177				user_pubkey.encode(w)?;
1178				unlock_hash.encode(w)?;
1179			},
1180			Self::HarkForfeit_v0(HarkForfeit_v0_VtxoPolicy { user_pubkey, unlock_hash }) => {
1181				w.emit_u8(VTXO_POLICY_HARK_FORFEIT_V0)?;
1182				user_pubkey.encode(w)?;
1183				unlock_hash.encode(w)?;
1184			},
1185		}
1186		Ok(())
1187	}
1188
1189	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
1190		let type_byte = r.read_u8()?;
1191		match type_byte {
1192			VTXO_POLICY_PUBKEY | VTXO_POLICY_SERVER_HTLC_SEND | VTXO_POLICY_SERVER_HTLC_RECV
1193				| VTXO_POLICY_SERVER_HTLC_SEND_V0 | VTXO_POLICY_SERVER_HTLC_RECV_V0 =>
1194			{
1195				Ok(Self::User(decode_vtxo_policy(type_byte, r)?))
1196			},
1197			VTXO_POLICY_SERVER_OWNED => Ok(Self::ServerOwned),
1198			VTXO_POLICY_CHECKPOINT => {
1199				let user_pubkey = PublicKey::decode(r)?;
1200				Ok(Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }))
1201			},
1202			VTXO_POLICY_EXPIRY => {
1203				let internal_key = XOnlyPublicKey::decode(r)?;
1204				Ok(Self::Expiry(ExpiryVtxoPolicy { internal_key }))
1205			},
1206			VTXO_POLICY_HARK_LEAF => {
1207				let user_pubkey = PublicKey::decode(r)?;
1208				let unlock_hash = sha256::Hash::decode(r)?;
1209				Ok(Self::HarkLeaf(HarkLeafVtxoPolicy { user_pubkey, unlock_hash }))
1210			},
1211			VTXO_POLICY_HARK_LEAF_V0 => {
1212				let user_pubkey = PublicKey::decode(r)?;
1213				let unlock_hash = sha256::Hash::decode(r)?;
1214				Ok(Self::HarkLeaf_v0(HarkLeaf_v0_VtxoPolicy { user_pubkey, unlock_hash }))
1215			},
1216			VTXO_POLICY_HARK_FORFEIT => {
1217				let user_pubkey = PublicKey::decode(r)?;
1218				let unlock_hash = sha256::Hash::decode(r)?;
1219				Ok(Self::HarkForfeit(HarkForfeitVtxoPolicy { user_pubkey, unlock_hash }))
1220			},
1221			VTXO_POLICY_HARK_FORFEIT_V0 => {
1222				let user_pubkey = PublicKey::decode(r)?;
1223				let unlock_hash = sha256::Hash::decode(r)?;
1224				Ok(Self::HarkForfeit_v0(HarkForfeit_v0_VtxoPolicy { user_pubkey, unlock_hash }))
1225			},
1226			v => Err(ProtocolDecodingError::invalid(format_args!(
1227				"invalid ServerVtxoPolicy type byte: {v:#x}",
1228			))),
1229		}
1230	}
1231}
1232
1233/// The byte used to encode the [GenesisTransition::Cosigned] gen transition type.
1234const GENESIS_TRANSITION_TYPE_COSIGNED: u8 = 1;
1235
1236/// The byte used to encode the [GenesisTransition::Arkoor] gen transition type.
1237const GENESIS_TRANSITION_TYPE_ARKOOR: u8 = 2;
1238
1239/// The byte used to encode the [GenesisTransition::HashLockedCosigned_v0] gen transition type.
1240const GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED_V0: u8 = 3;
1241
1242/// The byte used to encode the [GenesisTransition::HashLockedCosigned] gen transition type.
1243const GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED: u8 = 4;
1244
1245impl ProtocolEncoding for GenesisTransition {
1246	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
1247		match self {
1248			Self::Cosigned(t) => {
1249				w.emit_u8(GENESIS_TRANSITION_TYPE_COSIGNED)?;
1250				LengthPrefixedVector::new(&t.pubkeys).encode(w)?;
1251				t.signature.encode(w)?;
1252			},
1253			Self::HashLockedCosigned(t) => {
1254				w.emit_u8(GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED)?;
1255				t.user_pubkey.encode(w)?;
1256				t.signature.encode(w)?;
1257				match t.unlock {
1258					MaybePreimage::Preimage(p) => {
1259						w.emit_u8(0)?;
1260						w.emit_slice(&p[..])?;
1261					},
1262					MaybePreimage::Hash(h) => {
1263						w.emit_u8(1)?;
1264						w.emit_slice(&h[..])?;
1265					},
1266				}
1267			},
1268			Self::HashLockedCosigned_v0(t) => {
1269				w.emit_u8(GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED_V0)?;
1270				t.user_pubkey.encode(w)?;
1271				t.signature.encode(w)?;
1272				match t.unlock {
1273					MaybePreimage::Preimage(p) => {
1274						w.emit_u8(0)?;
1275						w.emit_slice(&p[..])?;
1276					},
1277					MaybePreimage::Hash(h) => {
1278						w.emit_u8(1)?;
1279						w.emit_slice(&h[..])?;
1280					},
1281				}
1282			},
1283			Self::Arkoor(t) => {
1284				w.emit_u8(GENESIS_TRANSITION_TYPE_ARKOOR)?;
1285				LengthPrefixedVector::new(&t.client_cosigners).encode(w)?;
1286				t.tap_tweak.encode(w)?;
1287				t.signature.encode(w)?;
1288			},
1289		}
1290		Ok(())
1291	}
1292
1293	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
1294		match r.read_u8()? {
1295			GENESIS_TRANSITION_TYPE_COSIGNED => {
1296				let pubkeys: Vec<PublicKey> = LengthPrefixedVector::decode(r)?.into_inner();
1297				if pubkeys.is_empty() {
1298					return Err(ProtocolDecodingError::invalid(
1299						"cosigned genesis transition with empty pubkey list",
1300					));
1301				}
1302				let signature = Option::<schnorr::Signature>::decode(r)?;
1303				Ok(Self::new_cosigned(pubkeys, signature))
1304			},
1305			GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED => {
1306				let user_pubkey = PublicKey::decode(r)?;
1307				let signature = Option::<schnorr::Signature>::decode(r)?;
1308				let unlock = match r.read_u8()? {
1309					0 => MaybePreimage::Preimage(r.read_byte_array()?),
1310					1 => MaybePreimage::Hash(ProtocolEncoding::decode(r)?),
1311					v => return Err(ProtocolDecodingError::invalid(format_args!(
1312						"invalid MaybePreimage type byte: {v:#x}",
1313					))),
1314				};
1315				Ok(Self::HashLockedCosigned(genesis::HashLockedCosignedGenesis {
1316					user_pubkey, signature, unlock,
1317				}))
1318			},
1319			GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED_V0 => {
1320				let user_pubkey = PublicKey::decode(r)?;
1321				let signature = Option::<schnorr::Signature>::decode(r)?;
1322				let unlock = match r.read_u8()? {
1323					0 => MaybePreimage::Preimage(r.read_byte_array()?),
1324					1 => MaybePreimage::Hash(ProtocolEncoding::decode(r)?),
1325					v => return Err(ProtocolDecodingError::invalid(format_args!(
1326						"invalid MaybePreimage type byte: {v:#x}",
1327					))),
1328				};
1329				Ok(Self::HashLockedCosigned_v0(genesis::HashLockedCosignedGenesis_v0 {
1330					user_pubkey, signature, unlock,
1331				}))
1332			},
1333			GENESIS_TRANSITION_TYPE_ARKOOR => {
1334				let cosigners = LengthPrefixedVector::decode(r)?.into_inner();
1335				let taptweak = TapTweakHash::decode(r)?;
1336				if bitcoin::secp256k1::Scalar::from_be_bytes(taptweak.to_byte_array()).is_err() {
1337					return Err(ProtocolDecodingError::invalid(
1338						"arkoor genesis tap tweak is not a valid secp256k1 scalar",
1339					));
1340				}
1341				let signature = Option::<schnorr::Signature>::decode(r)?;
1342				Ok(Self::new_arkoor(cosigners, taptweak, signature))
1343			},
1344			v => Err(ProtocolDecodingError::invalid(format_args!(
1345				"invalid GenesisTransistion type byte: {v:#x}",
1346			))),
1347		}
1348	}
1349}
1350
1351/// A private trait for VTXO sub-objects that have different encodings dependent on
1352/// the VTXO encoding version
1353trait VtxoVersionedEncoding: Sized {
1354	fn encode<W: io::Write + ?Sized>(&self, w: &mut W, version: u16) -> Result<(), io::Error>;
1355
1356	fn decode<R: io::Read + ?Sized>(
1357		r: &mut R,
1358		version: u16,
1359	) -> Result<Self, ProtocolDecodingError>;
1360}
1361
1362impl VtxoVersionedEncoding for Bare {
1363	fn encode<W: io::Write + ?Sized>(&self, w: &mut W, _version: u16) -> Result<(), io::Error> {
1364		w.emit_compact_size(0u64)?;
1365		Ok(())
1366	}
1367
1368	fn decode<R: io::Read + ?Sized>(
1369		r: &mut R,
1370		version: u16,
1371	) -> Result<Self, ProtocolDecodingError> {
1372		// We want to be compatible with [Full] encoded VTXOs, so we just ignore
1373		// whatever genesis there might be.
1374		let _full = Full::decode(r, version)?;
1375
1376		Ok(Bare)
1377	}
1378}
1379
1380impl VtxoVersionedEncoding for Full {
1381	fn encode<W: io::Write + ?Sized>(&self, w: &mut W, _version: u16) -> Result<(), io::Error> {
1382		w.emit_compact_size(self.items.len() as u64)?;
1383		for item in &self.items {
1384			item.transition.encode(w)?;
1385			let nb_outputs = item.other_outputs.len().saturating_add(1);
1386			w.emit_u8(nb_outputs.try_into()
1387				.map_err(|_| io::Error::other("too many outputs on genesis transaction"))?)?;
1388			w.emit_u8(item.output_idx)?;
1389			for txout in &item.other_outputs {
1390				txout.encode(w)?;
1391			}
1392			w.emit_u64(item.fee_amount.to_sat())?;
1393		}
1394		Ok(())
1395	}
1396
1397	fn decode<R: io::Read + ?Sized>(
1398		r: &mut R,
1399		version: u16,
1400	) -> Result<Self, ProtocolDecodingError> {
1401		let nb_genesis_items = r.read_compact_size()? as usize;
1402		OversizedVectorError::check::<GenesisItem>(nb_genesis_items)?;
1403		let mut genesis = Vec::with_capacity(nb_genesis_items);
1404		for _ in 0..nb_genesis_items {
1405			let transition = GenesisTransition::decode(r)?;
1406			let nb_outputs = r.read_u8()? as usize;
1407			let output_idx = r.read_u8()?;
1408			let nb_other = nb_outputs.checked_sub(1)
1409				.ok_or_else(|| ProtocolDecodingError::invalid("genesis item with 0 outputs"))?;
1410			// `output_idx` MUST index a real output of the exit tx. Otherwise
1411			// `GenesisItem::tx` clamps the placement and the VTXO's `point` ends
1412			// up referencing a sibling output or the anyone-can-spend P2A fee
1413			// anchor rather than the transition's own output, breaking the
1414			// invariant that `point` is fully determined by the genesis data.
1415			if output_idx as usize >= nb_outputs {
1416				return Err(ProtocolDecodingError::invalid(
1417					"genesis item output_idx out of range (>= nb_outputs)",
1418				));
1419			}
1420			let mut other_outputs = Vec::with_capacity(nb_other);
1421			for _ in 0..nb_other {
1422				other_outputs.push(TxOut::decode(r)?);
1423			}
1424			let fee_amount = if version == VTXO_NO_FEE_AMOUNT_VERSION {
1425				// Maintain backwards compatibility by assuming a fee of zero.
1426				Amount::ZERO
1427			} else {
1428				Amount::from_sat(r.read_u64()?)
1429			};
1430			genesis.push(GenesisItem { transition, output_idx, other_outputs, fee_amount });
1431		}
1432		Ok(Full { items: genesis })
1433	}
1434}
1435
1436impl<P: Policy + ProtocolEncoding> ProtocolEncoding for Vtxo<Bare, P> {
1437	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
1438		vtxo_encode_inner(&self, w)
1439	}
1440
1441	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
1442		Ok(vtxo_decode_inner(r)?.0)
1443	}
1444}
1445
1446impl<P: Policy + ProtocolEncoding> ProtocolEncoding for Vtxo<Full, P> {
1447	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
1448		vtxo_encode_inner(&self, w)
1449	}
1450
1451	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
1452		// Only allow coding Vtxo<Full> with no genesis items if the VTXO is a virtual
1453		// representation of an onchain UTXO.
1454		let (vtxo, _) = vtxo_decode_inner::<Full, P, _>(r)?;
1455		if vtxo.point() != vtxo.chain_anchor() {
1456			if vtxo.genesis.items.is_empty() {
1457				return Err(ProtocolDecodingError::invalid_err(
1458					VtxoValidationError::MissingGenesisItems,
1459					format!("VTXO {} has no genesis item data", vtxo.id()),
1460				));
1461			}
1462		} else {
1463			if !vtxo.genesis.items.is_empty() {
1464				return Err(ProtocolDecodingError::invalid_err(
1465					VtxoValidationError::UnexpectedGenesisItems,
1466					format!("decoded genesis item data when there shouldn't be any for VTXO {}", vtxo.id()),
1467				));
1468			}
1469		}
1470		Ok(vtxo)
1471	}
1472}
1473
1474fn vtxo_encode_inner<G, P, W>(vtxo: &Vtxo<G, P>, w: &mut W) -> Result<(), io::Error>
1475where
1476	G: VtxoVersionedEncoding,
1477	P: Policy + ProtocolEncoding,
1478	W: io::Write + ?Sized,
1479{
1480	let version = VTXO_ENCODING_VERSION;
1481	w.emit_u16(version)?;
1482	w.emit_u64(vtxo.amount.to_sat())?;
1483	w.emit_u32(vtxo.expiry_height)?;
1484	vtxo.server_pubkey.encode(w)?;
1485	w.emit_u16(vtxo.exit_delta)?;
1486	vtxo.anchor_point.encode(w)?;
1487
1488	vtxo.genesis.encode(w, version)?;
1489
1490	vtxo.policy.encode(w)?;
1491	vtxo.point.encode(w)?;
1492	Ok(())
1493}
1494
1495fn vtxo_decode_inner<G, P, R>(r: &mut R) -> Result<(Vtxo<G, P>, u16), ProtocolDecodingError>
1496where
1497	G: VtxoVersionedEncoding,
1498	P: Policy + ProtocolEncoding,
1499	R: io::Read + ?Sized,
1500{
1501	let version = r.read_u16()?;
1502	if version != VTXO_ENCODING_VERSION && version != VTXO_NO_FEE_AMOUNT_VERSION {
1503		return Err(ProtocolDecodingError::invalid(format_args!(
1504			"invalid Vtxo encoding version byte: {version:#x}",
1505		)));
1506	}
1507
1508	let amount = Amount::from_sat(r.read_u64()?);
1509	let expiry_height = check_block_height(r.read_u32()?)
1510		.map_err(|e| ProtocolDecodingError::invalid_err(e, "expiry_height"))?;
1511	let server_pubkey = PublicKey::decode(r)?;
1512	let exit_delta = check_block_delta(r.read_u16()?)
1513		.map_err(|e| ProtocolDecodingError::invalid_err(e, "exit_delta"))?;
1514	let anchor_point = OutPoint::decode(r)?;
1515
1516	let genesis = VtxoVersionedEncoding::decode(r, version)?;
1517
1518	let policy = P::decode(r)?;
1519	let point = OutPoint::decode(r)?;
1520	let vtxo = Vtxo {
1521		amount, expiry_height, server_pubkey, exit_delta, anchor_point, genesis, policy, point,
1522	};
1523	Ok((vtxo, version))
1524}
1525
1526#[cfg(test)]
1527mod test {
1528	use bitcoin::consensus::encode::serialize_hex;
1529	use bitcoin::hex::DisplayHex;
1530
1531	use crate::test_util::encoding_roundtrip;
1532	use crate::test_util::dummy::{DUMMY_SERVER_KEY, DUMMY_USER_KEY};
1533	use crate::test_util::vectors::{
1534		generate_vtxo_vectors, VTXO_VECTORS, VTXO_NO_FEE_AMOUNT_VERSION_HEXES,
1535	};
1536
1537	use super::*;
1538
1539	#[test]
1540	fn test_generate_vtxo_vectors() {
1541		let g = generate_vtxo_vectors();
1542		// the generation code prints its inner values
1543
1544		println!("\n\ngenerated:");
1545		println!("  anchor_tx: {}", serialize_hex(&g.anchor_tx));
1546		println!("  board_vtxo: {}", g.board_vtxo.serialize().as_hex().to_string());
1547		println!("  arkoor_htlc_out_vtxo: {}", g.arkoor_htlc_out_vtxo.serialize().as_hex().to_string());
1548		println!("  arkoor2_vtxo: {}", g.arkoor2_vtxo.serialize().as_hex().to_string());
1549		println!("  round_tx: {}", serialize_hex(&g.round_tx));
1550		println!("  round1_vtxo: {}", g.round1_vtxo.serialize().as_hex().to_string());
1551		println!("  round2_vtxo: {}", g.round2_vtxo.serialize().as_hex().to_string());
1552		println!("  arkoor3_vtxo: {}", g.arkoor3_vtxo.serialize().as_hex().to_string());
1553
1554
1555		let v = &*VTXO_VECTORS;
1556		println!("\n\nstatic:");
1557		println!("  anchor_tx: {}", serialize_hex(&v.anchor_tx));
1558		println!("  board_vtxo: {}", v.board_vtxo.serialize().as_hex().to_string());
1559		println!("  arkoor_htlc_out_vtxo: {}", v.arkoor_htlc_out_vtxo.serialize().as_hex().to_string());
1560		println!("  arkoor2_vtxo: {}", v.arkoor2_vtxo.serialize().as_hex().to_string());
1561		println!("  round_tx: {}", serialize_hex(&v.round_tx));
1562		println!("  round1_vtxo: {}", v.round1_vtxo.serialize().as_hex().to_string());
1563		println!("  round2_vtxo: {}", v.round2_vtxo.serialize().as_hex().to_string());
1564		println!("  arkoor3_vtxo: {}", v.arkoor3_vtxo.serialize().as_hex().to_string());
1565
1566		assert_eq!(g.anchor_tx, v.anchor_tx, "anchor_tx does not match");
1567		assert_eq!(g.board_vtxo, v.board_vtxo, "board_vtxo does not match");
1568		assert_eq!(g.arkoor_htlc_out_vtxo, v.arkoor_htlc_out_vtxo, "arkoor_htlc_out_vtxo does not match");
1569		assert_eq!(g.arkoor2_vtxo, v.arkoor2_vtxo, "arkoor2_vtxo does not match");
1570		assert_eq!(g.round_tx, v.round_tx, "round_tx does not match");
1571		assert_eq!(g.round1_vtxo, v.round1_vtxo, "round1_vtxo does not match");
1572		assert_eq!(g.round2_vtxo, v.round2_vtxo, "round2_vtxo does not match");
1573		assert_eq!(g.arkoor3_vtxo, v.arkoor3_vtxo, "arkoor3_vtxo does not match");
1574
1575		// this passes because the Eq is based on id which doesn't compare signatures
1576		assert_eq!(g, *v);
1577	}
1578
1579	#[test]
1580	fn test_vtxo_no_fee_amount_version_upgrade() {
1581		let hexes = &*VTXO_NO_FEE_AMOUNT_VERSION_HEXES;
1582		let v = hexes.deserialize_test_vectors();
1583
1584		// Ensure all VTXOs validate correctly.
1585		v.validate_vtxos();
1586
1587		// Ensure each VTXO serializes and is different from the old hex.
1588		let board_hex = v.board_vtxo.serialize().as_hex().to_string();
1589		let arkoor_htlc_out_vtxo_hex = v.arkoor_htlc_out_vtxo.serialize().as_hex().to_string();
1590		let arkoor2_vtxo_hex = v.arkoor2_vtxo.serialize().as_hex().to_string();
1591		let round1_vtxo_hex = v.round1_vtxo.serialize().as_hex().to_string();
1592		let round2_vtxo_hex = v.round2_vtxo.serialize().as_hex().to_string();
1593		let arkoor3_vtxo_hex = v.arkoor3_vtxo.serialize().as_hex().to_string();
1594		assert_ne!(board_hex, hexes.board_vtxo);
1595		assert_ne!(arkoor_htlc_out_vtxo_hex, hexes.arkoor_htlc_out_vtxo);
1596		assert_ne!(arkoor2_vtxo_hex, hexes.arkoor2_vtxo);
1597		assert_ne!(round1_vtxo_hex, hexes.round1_vtxo);
1598		assert_ne!(round2_vtxo_hex, hexes.round2_vtxo);
1599		assert_ne!(arkoor3_vtxo_hex, hexes.arkoor3_vtxo);
1600
1601		// Now verify that deserializing them again results in exactly the same hex. This should be
1602		// the case because the initial hex strings should have been created with a different
1603		// version, then, when we serialize the VTXOs, we should use the newest version. If you
1604		// deserialize a VTXO with the latest version and serialize it, you should get the same
1605		// result.
1606		let board_vtxo = Vtxo::<Full>::deserialize_hex(&board_hex).unwrap();
1607		assert_eq!(board_vtxo.serialize().as_hex().to_string(), board_hex);
1608		let arkoor_htlc_out_vtxo = Vtxo::<Full>::deserialize_hex(&arkoor_htlc_out_vtxo_hex).unwrap();
1609		assert_eq!(arkoor_htlc_out_vtxo.serialize().as_hex().to_string(), arkoor_htlc_out_vtxo_hex);
1610		let arkoor2_vtxo = Vtxo::<Full>::deserialize_hex(&arkoor2_vtxo_hex).unwrap();
1611		assert_eq!(arkoor2_vtxo.serialize().as_hex().to_string(), arkoor2_vtxo_hex);
1612		let round1_vtxo = Vtxo::<Full>::deserialize_hex(&round1_vtxo_hex).unwrap();
1613		assert_eq!(round1_vtxo.serialize().as_hex().to_string(), round1_vtxo_hex);
1614		let round2_vtxo = Vtxo::<Full>::deserialize_hex(&round2_vtxo_hex).unwrap();
1615		assert_eq!(round2_vtxo.serialize().as_hex().to_string(), round2_vtxo_hex);
1616		let arkoor3_vtxo = Vtxo::<Full>::deserialize_hex(&arkoor3_vtxo_hex).unwrap();
1617		assert_eq!(arkoor3_vtxo.serialize().as_hex().to_string(), arkoor3_vtxo_hex);
1618	}
1619
1620	#[test]
1621	fn exit_depth() {
1622		let vtxos = &*VTXO_VECTORS;
1623		// board
1624		assert_eq!(vtxos.board_vtxo.exit_depth(), 1 /* cosign */);
1625
1626		// round
1627		assert_eq!(vtxos.round1_vtxo.exit_depth(), 3 /* cosign */);
1628
1629		// arkoor
1630		assert_eq!(
1631			vtxos.arkoor_htlc_out_vtxo.exit_depth(),
1632			1 /* cosign */ + 1 /* checkpoint*/ + 1 /* arkoor */,
1633		);
1634		assert_eq!(
1635			vtxos.arkoor2_vtxo.exit_depth(),
1636			1 /* cosign */ + 2 /* checkpoint */ + 2 /* arkoor */,
1637		);
1638		assert_eq!(
1639			vtxos.arkoor3_vtxo.exit_depth(),
1640			3 /* cosign */ + 1 /* checkpoint */ + 1 /* arkoor */,
1641		);
1642	}
1643
1644	#[test]
1645	fn ancestor_ids() {
1646		let v = &*VTXO_VECTORS;
1647
1648		// A board VTXO is its own chain anchor: a single genesis tx producing the
1649		// VTXO itself, hence no ancestors.
1650		assert_eq!(v.board_vtxo.exit_depth(), 1, "board is a single-tx chain anchor");
1651		assert!(v.board_vtxo.ancestor_ids().is_empty(),
1652			"a chain-anchor VTXO has no ancestors");
1653
1654		// For every fixture: ancestor_ids is the whole genesis chain minus the
1655		// VTXO itself — length one less than the chain, never the VTXO's own id,
1656		// and the chain's final tx produces the VTXO itself (the invariant
1657		// ancestor_ids relies on).
1658		for vtxo in [
1659			&v.board_vtxo, &v.arkoor_htlc_out_vtxo, &v.arkoor2_vtxo,
1660			&v.round1_vtxo, &v.round2_vtxo, &v.arkoor3_vtxo,
1661		] {
1662			let ancestors = vtxo.ancestor_ids();
1663
1664			assert_eq!(ancestors.len(), vtxo.exit_depth() as usize - 1,
1665				"ancestor_ids is the whole genesis chain except the VTXO itself");
1666			assert!(!ancestors.contains(&vtxo.id()),
1667				"ancestor_ids must never contain the VTXO's own id");
1668
1669			let last = vtxo.transactions().last().expect("a VTXO has >=1 transaction");
1670			let last_id: VtxoId = OutPoint::new(last.tx.compute_txid(), last.output_idx as u32).into();
1671			assert_eq!(last_id, vtxo.id(),
1672				"the final genesis tx must produce the VTXO itself");
1673		}
1674
1675		// The recovery-critical property: a VTXO's ancestor set contains the id
1676		// of every owned VTXO it (transitively) spent, ordered chain-anchor-first,
1677		// so recovery can skip a parent spent into a newer recovered child.
1678
1679		// board -> arkoor1: arkoor1 spent the board.
1680		assert!(v.arkoor_htlc_out_vtxo.ancestor_ids().contains(&v.board_vtxo.id()),
1681			"a single-hop arkoor lists the board it spent as an ancestor");
1682
1683		// board -> arkoor1 -> arkoor2: arkoor2 lists both, ordered anchor-first.
1684		let anc2 = v.arkoor2_vtxo.ancestor_ids();
1685		let board_pos = anc2.iter().position(|id| *id == v.board_vtxo.id())
1686			.expect("arkoor2 must list the board ancestor");
1687		let arkoor1_pos = anc2.iter().position(|id| *id == v.arkoor_htlc_out_vtxo.id())
1688			.expect("arkoor2 must list the arkoor1 ancestor");
1689		assert!(board_pos < arkoor1_pos,
1690			"ancestors are ordered from chain anchor down to the immediate parent");
1691
1692		// A child's ancestor chain begins with its parent's whole chain (the
1693		// parent's own ancestors followed by the parent itself).
1694		let mut parent_chain = v.arkoor_htlc_out_vtxo.ancestor_ids();
1695		parent_chain.push(v.arkoor_htlc_out_vtxo.id());
1696		assert!(v.arkoor2_vtxo.ancestor_ids().starts_with(&parent_chain),
1697			"a child's ancestors extend its parent's full genesis chain");
1698
1699		// round2 -> arkoor3: an arkoor built on a round output lists that output.
1700		assert!(v.arkoor3_vtxo.ancestor_ids().contains(&v.round2_vtxo.id()),
1701			"an arkoor spending a round output lists it as an ancestor");
1702	}
1703
1704	#[test]
1705	fn test_split_genesis_roundtrip() {
1706		// For each fixture, splitting the encoding into bare bytes + genesis
1707		// bytes and reassembling must produce a byte-identical full VTXO. This
1708		// is the load-bearing invariant for the m0029 storage migration.
1709		fn check<P: Policy + ProtocolEncoding + Clone + std::fmt::Debug>(
1710			vtxo: &Vtxo<Full, P>,
1711		) where
1712			Vtxo<Full, P>: PartialEq,
1713		{
1714			let original = vtxo.serialize();
1715
1716			let bare_bytes = vtxo.to_bare().serialize();
1717			let genesis_bytes = vtxo.serialize_genesis();
1718
1719			let bare = Vtxo::<Bare, P>::deserialize(&bare_bytes)
1720				.expect("bare deserialize");
1721			let genesis = Full::decode(&mut &genesis_bytes[..], VTXO_ENCODING_VERSION)
1722				.expect("decode_genesis");
1723			let reassembled = bare.with_genesis(genesis)
1724				.expect("reassemble");
1725
1726			assert_eq!(*vtxo, reassembled, "reassembled vtxo differs from original");
1727			assert_eq!(reassembled.serialize(), original,
1728				"reassembled bytes differ from original");
1729		}
1730
1731		let v = &*VTXO_VECTORS;
1732		check(&v.board_vtxo);
1733		check(&v.arkoor_htlc_out_vtxo);
1734		check(&v.arkoor2_vtxo);
1735		check(&v.round1_vtxo);
1736		check(&v.round2_vtxo);
1737		check(&v.arkoor3_vtxo);
1738
1739		// Also exercise a depth-257 genesis to cover compact_size > 252.
1740		let big: Vtxo<Full> = Vtxo {
1741			policy: VtxoPolicy::new_pubkey(DUMMY_USER_KEY.public_key()),
1742			amount: Amount::from_sat(10_000),
1743			expiry_height: 101_010,
1744			server_pubkey: DUMMY_SERVER_KEY.public_key(),
1745			exit_delta: 2016,
1746			anchor_point: OutPoint::new(Txid::from_slice(&[1u8; 32]).unwrap(), 1),
1747			genesis: Full {
1748				items: vec![GenesisItem {
1749					transition: GenesisTransition::new_cosigned(
1750						vec![DUMMY_USER_KEY.public_key()],
1751						Some(schnorr::Signature::from_slice(&[2u8; 64]).unwrap()),
1752					),
1753					output_idx: 0,
1754					other_outputs: vec![],
1755					fee_amount: Amount::ZERO,
1756				}; 257],
1757			},
1758			point: OutPoint::new(Txid::from_slice(&[3u8; 32]).unwrap(), 3),
1759		};
1760		check(&big);
1761	}
1762
1763	#[test]
1764	fn test_genesis_length_257() {
1765		let vtxo: Vtxo<Full> = Vtxo {
1766			policy: VtxoPolicy::new_pubkey(DUMMY_USER_KEY.public_key()),
1767			amount: Amount::from_sat(10_000),
1768			expiry_height: 101_010,
1769			server_pubkey: DUMMY_SERVER_KEY.public_key(),
1770			exit_delta: 2016,
1771			anchor_point: OutPoint::new(Txid::from_slice(&[1u8; 32]).unwrap(), 1),
1772			genesis: Full {
1773				items: vec![GenesisItem {
1774					transition: GenesisTransition::new_cosigned(
1775						vec![DUMMY_USER_KEY.public_key()],
1776						Some(schnorr::Signature::from_slice(&[2u8; 64]).unwrap()),
1777					),
1778					output_idx: 0,
1779					other_outputs: vec![],
1780					fee_amount: Amount::ZERO,
1781				}; 257],
1782			},
1783			point: OutPoint::new(Txid::from_slice(&[3u8; 32]).unwrap(), 3),
1784		};
1785		assert_eq!(vtxo.genesis.items.len(), 257);
1786		encoding_roundtrip(&vtxo);
1787	}
1788
1789	#[test]
1790	fn test_genesis_decoding() {
1791		// We should disallow decoding a Vtxo<Bare> as a Vtxo<Full> since it's nonsensical and will
1792		// only lead to confusing errors, such as when validating a VTXO.
1793		fn check<P: Policy + ProtocolEncoding + Clone + std::fmt::Debug>(
1794			vtxo: &Vtxo<Full, P>,
1795		) where
1796			Vtxo<Full, P>: PartialEq,
1797		{
1798			let full_bytes = vtxo.serialize();
1799			let bare_bytes = vtxo.as_bare_vtxo().unwrap().serialize();
1800
1801			// We should support the following:
1802			// - Full -> Full
1803			// - Full -> Bare
1804			// - Bare -> Bare
1805			// We should disallow Bare -> Full.
1806			let full_to_full = Vtxo::<Full>::deserialize(&full_bytes).expect("works");
1807			let full_to_bare = Vtxo::<Bare>::deserialize(&full_bytes).expect("works");
1808			let bare_to_bare = Vtxo::<Bare>::deserialize(&bare_bytes).expect("works");
1809			Vtxo::<Full>::deserialize(&bare_bytes).expect_err("bare to full fails");
1810
1811			assert_eq!(full_to_full.serialize(), full_bytes);
1812			assert_eq!(full_to_bare.serialize(), bare_bytes);
1813			assert_eq!(bare_to_bare.serialize(), bare_bytes);
1814		}
1815
1816		let v = &*VTXO_VECTORS;
1817		check(&v.board_vtxo);
1818		check(&v.arkoor_htlc_out_vtxo);
1819		check(&v.arkoor2_vtxo);
1820		check(&v.round1_vtxo);
1821		check(&v.round2_vtxo);
1822		check(&v.arkoor3_vtxo);
1823	}
1824
1825	/// Build a minimal single-item [Vtxo<Full>] for standardness tests.
1826	///
1827	/// The genesis chain is one cosigned transition wide; callers control
1828	/// the VTXO's own amount and the sibling outputs in that transition.
1829	fn dummy_vtxo_with(amount: Amount, other_outputs: Vec<TxOut>) -> Vtxo<Full> {
1830		Vtxo {
1831			policy: VtxoPolicy::new_pubkey(DUMMY_USER_KEY.public_key()),
1832			amount,
1833			expiry_height: 101_010,
1834			server_pubkey: DUMMY_SERVER_KEY.public_key(),
1835			exit_delta: 2016,
1836			anchor_point: OutPoint::new(Txid::from_slice(&[1u8; 32]).unwrap(), 1),
1837			genesis: Full {
1838				items: vec![GenesisItem {
1839					transition: GenesisTransition::new_cosigned(
1840						vec![DUMMY_USER_KEY.public_key()],
1841						Some(schnorr::Signature::from_slice(&[2u8; 64]).unwrap()),
1842					),
1843					output_idx: 0,
1844					other_outputs,
1845					fee_amount: Amount::ZERO,
1846				}],
1847			},
1848			point: OutPoint::new(Txid::from_slice(&[3u8; 32]).unwrap(), 3),
1849		}
1850	}
1851
1852	/// A valid P2TR script_pubkey usable as a sibling output.
1853	fn dummy_p2tr_script() -> ScriptBuf {
1854		VtxoPolicy::new_pubkey(DUMMY_USER_KEY.public_key())
1855			.script_pubkey(DUMMY_SERVER_KEY.public_key(), 2016, 101_010)
1856	}
1857
1858	#[test]
1859	fn check_standard_accepts_real_vtxos() {
1860		// The hand-rolled test vectors must all be standard so that
1861		// regular VTXO use never falsely trips check_standard.
1862		let v = &*VTXO_VECTORS;
1863		assert_eq!(v.board_vtxo.check_standard(), Ok(()));
1864		assert_eq!(v.arkoor_htlc_out_vtxo.check_standard(), Ok(()));
1865		assert_eq!(v.arkoor2_vtxo.check_standard(), Ok(()));
1866		assert_eq!(v.round1_vtxo.check_standard(), Ok(()));
1867		assert_eq!(v.round2_vtxo.check_standard(), Ok(()));
1868		assert_eq!(v.arkoor3_vtxo.check_standard(), Ok(()));
1869		assert!(v.board_vtxo.is_standard());
1870	}
1871
1872	#[test]
1873	fn check_standard_dusty_own_output() {
1874		// A VTXO whose own output is below P2TR_DUST is Dusty. The
1875		// VtxoPolicy script is always P2TR, so the dust limit is 330 sat.
1876		let vtxo = dummy_vtxo_with(Amount::from_sat(100), vec![]);
1877		assert_eq!(vtxo.check_standard(), Err(VtxoStandardnessError::Dusty));
1878		assert!(!vtxo.is_standard());
1879	}
1880
1881	#[test]
1882	fn check_standard_dust_sibling() {
1883		// Own amount is fine, but a sub-dust P2TR sibling output along
1884		// the exit chain should surface as DustSibling and point at the
1885		// offending position.
1886		let dust = TxOut {
1887			value: Amount::from_sat(100),
1888			script_pubkey: dummy_p2tr_script(),
1889		};
1890		let vtxo = dummy_vtxo_with(Amount::from_sat(10_000), vec![dust]);
1891		assert_eq!(
1892			vtxo.check_standard(),
1893			Err(VtxoStandardnessError::DustSibling {
1894				item_idx: 0,
1895				item_count: 1,
1896				output_idx: 0,
1897			}),
1898		);
1899	}
1900
1901	#[test]
1902	fn check_standard_script_sibling() {
1903		// A sibling using an unrecognised script template (here just a
1904		// pair of arbitrary bytes that match none of P2PKH/P2SH/P2WPKH/
1905		// P2WSH/P2TR/OP_RETURN) trips ScriptSibling regardless of value.
1906		let bad = TxOut {
1907			value: Amount::from_sat(10_000),
1908			script_pubkey: ScriptBuf::from_bytes(vec![0xab, 0xcd]),
1909		};
1910		let vtxo = dummy_vtxo_with(Amount::from_sat(10_000), vec![bad]);
1911		assert_eq!(
1912			vtxo.check_standard(),
1913			Err(VtxoStandardnessError::ScriptSibling {
1914				item_idx: 0,
1915				item_count: 1,
1916				output_idx: 0,
1917			}),
1918		);
1919	}
1920
1921	#[test]
1922	fn check_standard_dust_takes_priority_over_later_script_sibling() {
1923		// The check short-circuits on the first violation: a sub-dust
1924		// sibling earlier in the list wins over a bad-script one later.
1925		let dust = TxOut {
1926			value: Amount::from_sat(100),
1927			script_pubkey: dummy_p2tr_script(),
1928		};
1929		let bad = TxOut {
1930			value: Amount::from_sat(10_000),
1931			script_pubkey: ScriptBuf::from_bytes(vec![0xab, 0xcd]),
1932		};
1933		let vtxo = dummy_vtxo_with(Amount::from_sat(10_000), vec![dust, bad]);
1934		assert_eq!(
1935			vtxo.check_standard(),
1936			Err(VtxoStandardnessError::DustSibling {
1937				item_idx: 0,
1938				item_count: 1,
1939				output_idx: 0,
1940			}),
1941		);
1942	}
1943
1944	mod genesis_transition_encoding {
1945		use bitcoin::hashes::{sha256, Hash};
1946		use bitcoin::secp256k1::{Keypair, PublicKey};
1947		use bitcoin::taproot::TapTweakHash;
1948		use std::str::FromStr;
1949
1950		use crate::encode::ProtocolEncoding;
1951		use crate::test_util::encoding_roundtrip;
1952		use super::genesis::{
1953			GenesisTransition, CosignedGenesis, HashLockedCosignedGenesis_v0, ArkoorGenesis,
1954		};
1955		use super::MaybePreimage;
1956
1957		fn test_pubkey() -> PublicKey {
1958			Keypair::from_str(
1959				"916da686cedaee9a9bfb731b77439f2a3f1df8664e16488fba46b8d2bfe15e92"
1960			).unwrap().public_key()
1961		}
1962
1963		fn test_signature() -> bitcoin::secp256k1::schnorr::Signature {
1964			"cc8b93e9f6fbc2506bb85ae8bbb530b178daac49704f5ce2e3ab69c266fd5932\
1965			 0b28d028eef212e3b9fdc42cfd2e0760a0359d3ea7d2e9e8cfe2040e3f1b71ea"
1966				.parse().unwrap()
1967		}
1968
1969		#[test]
1970		fn cosigned_with_signature() {
1971			let transition = GenesisTransition::Cosigned(CosignedGenesis {
1972				pubkeys: vec![test_pubkey()],
1973				signature: Some(test_signature()),
1974			});
1975			encoding_roundtrip(&transition);
1976		}
1977
1978		#[test]
1979		fn cosigned_without_signature() {
1980			let transition = GenesisTransition::Cosigned(CosignedGenesis {
1981				pubkeys: vec![test_pubkey()],
1982				signature: None,
1983			});
1984			encoding_roundtrip(&transition);
1985		}
1986
1987		#[test]
1988		fn cosigned_empty_pubkeys_rejected() {
1989			let mut buf = Vec::new();
1990			buf.push(super::GENESIS_TRANSITION_TYPE_COSIGNED);
1991			buf.push(0x00); // LengthPrefixedVector length = 0
1992			buf.push(0x00); // Option::<Signature> = None
1993			let err = GenesisTransition::deserialize(&mut buf.as_slice())
1994				.expect_err("empty pubkeys must be rejected");
1995			assert!(format!("{err}").contains("empty pubkey list"), "got: {err}");
1996		}
1997
1998		#[test]
1999		fn cosigned_multiple_pubkeys() {
2000			let pk1 = test_pubkey();
2001			let pk2 = Keypair::from_str(
2002				"fab9e598081a3e74b2233d470c4ad87bcc285b6912ed929568e62ac0e9409879"
2003			).unwrap().public_key();
2004
2005			let transition = GenesisTransition::Cosigned(CosignedGenesis {
2006				pubkeys: vec![pk1, pk2],
2007				signature: Some(test_signature()),
2008			});
2009			encoding_roundtrip(&transition);
2010		}
2011
2012		#[test]
2013		fn hash_locked_cosigned_with_preimage() {
2014			let preimage = [0x42u8; 32];
2015			let transition = GenesisTransition::HashLockedCosigned_v0(HashLockedCosignedGenesis_v0 {
2016				user_pubkey: test_pubkey(),
2017				signature: Some(test_signature()),
2018				unlock: MaybePreimage::Preimage(preimage),
2019			});
2020			encoding_roundtrip(&transition);
2021		}
2022
2023		#[test]
2024		fn hash_locked_cosigned_with_hash() {
2025			let hash = sha256::Hash::hash(b"test preimage");
2026			let transition = GenesisTransition::HashLockedCosigned_v0(HashLockedCosignedGenesis_v0 {
2027				user_pubkey: test_pubkey(),
2028				signature: Some(test_signature()),
2029				unlock: MaybePreimage::Hash(hash),
2030			});
2031			encoding_roundtrip(&transition);
2032		}
2033
2034		#[test]
2035		fn hash_locked_cosigned_without_signature() {
2036			let preimage = [0x42u8; 32];
2037			let transition = GenesisTransition::HashLockedCosigned_v0(HashLockedCosignedGenesis_v0 {
2038				user_pubkey: test_pubkey(),
2039				signature: None,
2040				unlock: MaybePreimage::Preimage(preimage),
2041			});
2042			encoding_roundtrip(&transition);
2043		}
2044
2045		#[test]
2046		fn arkoor_with_signature() {
2047			let tap_tweak = TapTweakHash::from_slice(&[0xabu8; 32]).unwrap();
2048			let transition = GenesisTransition::Arkoor(ArkoorGenesis {
2049				client_cosigners: vec![test_pubkey()],
2050				tap_tweak,
2051				signature: Some(test_signature()),
2052			});
2053			encoding_roundtrip(&transition);
2054		}
2055
2056		#[test]
2057		fn arkoor_without_signature() {
2058			let tap_tweak = TapTweakHash::from_slice(&[0xabu8; 32]).unwrap();
2059			let transition = GenesisTransition::Arkoor(ArkoorGenesis {
2060				client_cosigners: vec![test_pubkey()],
2061				tap_tweak,
2062				signature: None,
2063			});
2064			encoding_roundtrip(&transition);
2065		}
2066
2067		#[test]
2068		fn arkoor_out_of_range_tweak_rejected() {
2069			// A tap tweak at or above the secp256k1 curve order is not a valid
2070			// musig scalar and would panic in `musig::tweaked_key_agg` during
2071			// validation; decoding must reject it at the untrusted-input boundary.
2072			let valid = GenesisTransition::Arkoor(ArkoorGenesis {
2073				client_cosigners: vec![test_pubkey()],
2074				tap_tweak: TapTweakHash::from_slice(&[0xabu8; 32]).unwrap(),
2075				signature: None,
2076			});
2077			let mut bytes = valid.serialize();
2078			// Trailing layout is [tap_tweak: 32 bytes][signature: 64 bytes];
2079			// overwrite the tweak with all-ones, which exceeds the curve order.
2080			let n = bytes.len();
2081			for b in &mut bytes[n - 96 .. n - 64] {
2082				*b = 0xff;
2083			}
2084			let err = GenesisTransition::deserialize(&mut bytes.as_slice())
2085				.expect_err("out-of-range tap tweak must be rejected");
2086			assert!(
2087				format!("{err}").contains("not a valid secp256k1 scalar"),
2088				"got: {err}",
2089			);
2090		}
2091	}
2092}