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			.expect("not a hArk leaf VTXO");
882		let cosign = LeafVtxoCosignResponse::new_cosign(&req, self, chain_anchor, server_key)
883			.expect("not a hArk leaf VTXO");
884		assert!(ctx.finalize(self, cosign));
885		// then provide preimage
886		assert!(self.provide_unlock_preimage(unlock_preimage));
887	}
888}
889
890impl<G> Vtxo<G, ServerVtxoPolicy> {
891	/// Try to convert into a user [Vtxo]
892	///
893	/// Returns the original value on failure.
894	pub fn try_into_user_vtxo(self) -> Result<Vtxo<G, VtxoPolicy>, ServerVtxo<G>> {
895		if let Some(p) = self.policy.clone().into_user_policy() {
896			Ok(Vtxo {
897				policy: p,
898				amount: self.amount,
899				expiry_height: self.expiry_height,
900				server_pubkey: self.server_pubkey,
901				exit_delta: self.exit_delta,
902				anchor_point: self.anchor_point,
903				genesis: self.genesis,
904				point: self.point,
905			})
906		} else {
907			Err(self)
908		}
909	}
910}
911
912impl<G, P: Policy> PartialEq for Vtxo<G, P> {
913	fn eq(&self, other: &Self) -> bool {
914		PartialEq::eq(&self.id(), &other.id())
915	}
916}
917
918impl<G, P: Policy> Eq for Vtxo<G, P> {}
919
920impl<G, P: Policy> PartialOrd for Vtxo<G, P> {
921	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
922		PartialOrd::partial_cmp(&self.id(), &other.id())
923	}
924}
925
926impl<G, P: Policy> Ord for Vtxo<G, P> {
927	fn cmp(&self, other: &Self) -> std::cmp::Ordering {
928		Ord::cmp(&self.id(), &other.id())
929	}
930}
931
932impl<G, P: Policy> std::hash::Hash for Vtxo<G, P> {
933	fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
934		std::hash::Hash::hash(&self.id(), state)
935	}
936}
937
938impl<G, P: Policy> AsRef<Vtxo<G, P>> for Vtxo<G, P> {
939	fn as_ref(&self) -> &Vtxo<G, P> {
940	    self
941	}
942}
943
944impl<G> From<Vtxo<G>> for ServerVtxo<G> {
945	fn from(vtxo: Vtxo<G>) -> ServerVtxo<G> {
946		ServerVtxo {
947			policy: vtxo.policy.into(),
948			amount: vtxo.amount,
949			expiry_height: vtxo.expiry_height,
950			server_pubkey: vtxo.server_pubkey,
951			exit_delta: vtxo.exit_delta,
952			anchor_point: vtxo.anchor_point,
953			genesis: vtxo.genesis,
954			point: vtxo.point,
955		}
956	}
957}
958
959/// Implemented on anything that is kinda a [Vtxo]
960pub trait VtxoRef<P: Policy = VtxoPolicy> {
961	/// The [VtxoId] of the VTXO
962	fn vtxo_id(&self) -> VtxoId;
963
964	/// If the bare [Vtxo] can be provided, provides it by reference
965	fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { None }
966
967	/// If the full [Vtxo] can be provided, provides it by reference
968	fn as_full_vtxo(&self) -> Option<&Vtxo<Full, P>> { None }
969
970	/// If the full [Vtxo] can be provided, provides it by value, either directly or via cloning
971	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> where Self: Sized;
972}
973
974impl<P: Policy> VtxoRef<P> for VtxoId {
975	fn vtxo_id(&self) -> VtxoId { *self }
976	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
977}
978
979impl<'a, P: Policy> VtxoRef<P> for &'a VtxoId {
980	fn vtxo_id(&self) -> VtxoId { **self }
981	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
982}
983
984impl<P: Policy> VtxoRef<P> for Vtxo<Bare, P> {
985	fn vtxo_id(&self) -> VtxoId { self.id() }
986	fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Borrowed(self)) }
987	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
988}
989
990impl<'a, P: Policy> VtxoRef<P> for &'a Vtxo<Bare, P> {
991	fn vtxo_id(&self) -> VtxoId { self.id() }
992	fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Borrowed(*self)) }
993	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { None }
994}
995
996impl<P: Policy> VtxoRef<P> for Vtxo<Full, P> {
997	fn vtxo_id(&self) -> VtxoId { self.id() }
998	fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Owned(self.to_bare())) }
999	fn as_full_vtxo(&self) -> Option<&Vtxo<Full, P>> { Some(self) }
1000	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { Some(self) }
1001}
1002
1003impl<'a, P: Policy> VtxoRef<P> for &'a Vtxo<Full, P> {
1004	fn vtxo_id(&self) -> VtxoId { self.id() }
1005	fn as_bare_vtxo(&self) -> Option<Cow<'_, Vtxo<Bare, P>>> { Some(Cow::Owned(self.to_bare())) }
1006	fn as_full_vtxo(&self) -> Option<&Vtxo<Full, P>> { Some(*self) }
1007	fn into_full_vtxo(self) -> Option<Vtxo<Full, P>> { Some(self.clone()) }
1008}
1009
1010/// The byte used to encode the [VtxoPolicy::Pubkey] output type.
1011const VTXO_POLICY_PUBKEY: u8 = 0x00;
1012
1013/// The byte used to encode the [VtxoPolicy::ServerHtlcSend_v0] output type.
1014const VTXO_POLICY_SERVER_HTLC_SEND_V0: u8 = 0x01;
1015
1016/// The byte used to encode the [VtxoPolicy::ServerHtlcRecv_v0] output type.
1017const VTXO_POLICY_SERVER_HTLC_RECV_V0: u8 = 0x02;
1018
1019/// The byte used to encode the [ServerVtxoPolicy::Checkpoint] output type.
1020const VTXO_POLICY_CHECKPOINT: u8 = 0x03;
1021
1022/// The byte used to encode the [ServerVtxoPolicy::Expiry] output type.
1023const VTXO_POLICY_EXPIRY: u8 = 0x04;
1024
1025/// The byte used to encode the [ServerVtxoPolicy::HarkLeaf_v0] output type.
1026const VTXO_POLICY_HARK_LEAF_V0: u8 = 0x05;
1027
1028/// The byte used to encode the [ServerVtxoPolicy::HarkForfeit_v0] output type.
1029const VTXO_POLICY_HARK_FORFEIT_V0: u8 = 0x06;
1030
1031/// The byte used to encode the [ServerVtxoPolicy::ServerOwned] output type.
1032const VTXO_POLICY_SERVER_OWNED: u8 = 0x07;
1033
1034/// The byte used to encode the [VtxoPolicy::ServerHtlcRecv] output type.
1035const VTXO_POLICY_SERVER_HTLC_RECV: u8 = 0x08;
1036
1037/// The byte used to encode the [VtxoPolicy::ServerHtlcSend] output type.
1038const VTXO_POLICY_SERVER_HTLC_SEND: u8 = 0x09;
1039
1040/// The byte used to encode the [ServerVtxoPolicy::HarkLeaf] output type.
1041const VTXO_POLICY_HARK_LEAF: u8 = 0x0a;
1042
1043/// The byte used to encode the [ServerVtxoPolicy::HarkForfeit] output type.
1044const VTXO_POLICY_HARK_FORFEIT: u8 = 0x0b;
1045
1046impl ProtocolEncoding for VtxoPolicy {
1047	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
1048		match self {
1049			Self::Pubkey(PubkeyVtxoPolicy { user_pubkey }) => {
1050				w.emit_u8(VTXO_POLICY_PUBKEY)?;
1051				user_pubkey.encode(w)?;
1052			},
1053			Self::ServerHtlcSend(ServerHtlcSendVtxoPolicy { user_pubkey, payment_hash, htlc_expiry }) => {
1054				w.emit_u8(VTXO_POLICY_SERVER_HTLC_SEND)?;
1055				user_pubkey.encode(w)?;
1056				payment_hash.to_sha256_hash().encode(w)?;
1057				w.emit_u32(*htlc_expiry)?;
1058			},
1059			Self::ServerHtlcSend_v0(ServerHtlcSend_v0_VtxoPolicy { user_pubkey, payment_hash, htlc_expiry }) => {
1060				w.emit_u8(VTXO_POLICY_SERVER_HTLC_SEND_V0)?;
1061				user_pubkey.encode(w)?;
1062				payment_hash.to_sha256_hash().encode(w)?;
1063				w.emit_u32(*htlc_expiry)?;
1064			},
1065			Self::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy {
1066				user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta,
1067			}) => {
1068				w.emit_u8(VTXO_POLICY_SERVER_HTLC_RECV)?;
1069				user_pubkey.encode(w)?;
1070				payment_hash.to_sha256_hash().encode(w)?;
1071				w.emit_u32(*htlc_expiry)?;
1072				w.emit_u16(*htlc_expiry_delta)?;
1073			},
1074			Self::ServerHtlcRecv_v0(ServerHtlcRecv_v0_VtxoPolicy {
1075				user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta,
1076			}) => {
1077				w.emit_u8(VTXO_POLICY_SERVER_HTLC_RECV_V0)?;
1078				user_pubkey.encode(w)?;
1079				payment_hash.to_sha256_hash().encode(w)?;
1080				w.emit_u32(*htlc_expiry)?;
1081				w.emit_u16(*htlc_expiry_delta)?;
1082			},
1083		}
1084		Ok(())
1085	}
1086
1087	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
1088		let type_byte = r.read_u8()?;
1089		decode_vtxo_policy(type_byte, r)
1090	}
1091}
1092
1093/// Decode a [VtxoPolicy] with the given type byte
1094///
1095/// We have this function so it can be reused in [VtxoPolicy] and [ServerVtxoPolicy].
1096fn decode_vtxo_policy<R: io::Read + ?Sized>(
1097	type_byte: u8,
1098	r: &mut R,
1099) -> Result<VtxoPolicy, ProtocolDecodingError> {
1100	match type_byte {
1101		VTXO_POLICY_PUBKEY => {
1102			let user_pubkey = PublicKey::decode(r)?;
1103			Ok(VtxoPolicy::Pubkey(PubkeyVtxoPolicy { user_pubkey }))
1104		},
1105		VTXO_POLICY_SERVER_HTLC_SEND => {
1106			let user_pubkey = PublicKey::decode(r)?;
1107			let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
1108			let htlc_expiry = check_block_height(r.read_u32()?)
1109				.map_err(|e| ProtocolDecodingError::invalid_err(e, "htlc_expiry"))?;
1110			Ok(VtxoPolicy::ServerHtlcSend(ServerHtlcSendVtxoPolicy {
1111				user_pubkey, payment_hash, htlc_expiry,
1112			}))
1113		},
1114		VTXO_POLICY_SERVER_HTLC_SEND_V0 => {
1115			let user_pubkey = PublicKey::decode(r)?;
1116			let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
1117			let htlc_expiry = check_block_height(r.read_u32()?)
1118				.map_err(|e| ProtocolDecodingError::invalid_err(e, "htlc_expiry"))?;
1119			Ok(VtxoPolicy::ServerHtlcSend_v0(ServerHtlcSend_v0_VtxoPolicy { user_pubkey, payment_hash, htlc_expiry }))
1120		},
1121		VTXO_POLICY_SERVER_HTLC_RECV => {
1122			let user_pubkey = PublicKey::decode(r)?;
1123			let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
1124			let htlc_expiry = check_block_height(r.read_u32()?)
1125				.map_err(|e| ProtocolDecodingError::invalid_err(e, "htlc_expiry"))?;
1126			let htlc_expiry_delta = check_block_delta(r.read_u16()?)
1127				.map_err(|e| ProtocolDecodingError::invalid_err(e, "htlc_expiry_delta"))?;
1128			Ok(VtxoPolicy::ServerHtlcRecv(ServerHtlcRecvVtxoPolicy {
1129				user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta,
1130			}))
1131		},
1132		VTXO_POLICY_SERVER_HTLC_RECV_V0 => {
1133			let user_pubkey = PublicKey::decode(r)?;
1134			let payment_hash = PaymentHash::from(sha256::Hash::decode(r)?.to_byte_array());
1135			let htlc_expiry = check_block_height(r.read_u32()?)
1136				.map_err(|e| ProtocolDecodingError::invalid_err(e, "htlc_expiry"))?;
1137			let htlc_expiry_delta = check_block_delta(r.read_u16()?)
1138				.map_err(|e| ProtocolDecodingError::invalid_err(e, "htlc_expiry_delta"))?;
1139			Ok(VtxoPolicy::ServerHtlcRecv_v0(ServerHtlcRecv_v0_VtxoPolicy { user_pubkey, payment_hash, htlc_expiry, htlc_expiry_delta }))
1140		},
1141
1142		// IMPORTANT:
1143		// When adding a new user vtxo policy variant, don't forget
1144		// to also add it to the ServerVtxoPolicy decode match arm.
1145
1146		v => Err(ProtocolDecodingError::invalid(format_args!(
1147			"invalid VtxoPolicy type byte: {v:#x}",
1148		))),
1149	}
1150}
1151
1152impl ProtocolEncoding for ServerVtxoPolicy {
1153	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
1154		match self {
1155			Self::User(p) => p.encode(w)?,
1156			Self::ServerOwned => {
1157				w.emit_u8(VTXO_POLICY_SERVER_OWNED)?;
1158			},
1159			Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }) => {
1160				w.emit_u8(VTXO_POLICY_CHECKPOINT)?;
1161				user_pubkey.encode(w)?;
1162			},
1163			Self::Expiry(ExpiryVtxoPolicy { internal_key }) => {
1164				w.emit_u8(VTXO_POLICY_EXPIRY)?;
1165				internal_key.encode(w)?;
1166			},
1167			Self::HarkLeaf(HarkLeafVtxoPolicy { user_pubkey, unlock_hash }) => {
1168				w.emit_u8(VTXO_POLICY_HARK_LEAF)?;
1169				user_pubkey.encode(w)?;
1170				unlock_hash.encode(w)?;
1171			},
1172			Self::HarkLeaf_v0(HarkLeaf_v0_VtxoPolicy { user_pubkey, unlock_hash }) => {
1173				w.emit_u8(VTXO_POLICY_HARK_LEAF_V0)?;
1174				user_pubkey.encode(w)?;
1175				unlock_hash.encode(w)?;
1176			},
1177			Self::HarkForfeit(HarkForfeitVtxoPolicy { user_pubkey, unlock_hash }) => {
1178				w.emit_u8(VTXO_POLICY_HARK_FORFEIT)?;
1179				user_pubkey.encode(w)?;
1180				unlock_hash.encode(w)?;
1181			},
1182			Self::HarkForfeit_v0(HarkForfeit_v0_VtxoPolicy { user_pubkey, unlock_hash }) => {
1183				w.emit_u8(VTXO_POLICY_HARK_FORFEIT_V0)?;
1184				user_pubkey.encode(w)?;
1185				unlock_hash.encode(w)?;
1186			},
1187		}
1188		Ok(())
1189	}
1190
1191	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
1192		let type_byte = r.read_u8()?;
1193		match type_byte {
1194			VTXO_POLICY_PUBKEY | VTXO_POLICY_SERVER_HTLC_SEND | VTXO_POLICY_SERVER_HTLC_RECV
1195				| VTXO_POLICY_SERVER_HTLC_SEND_V0 | VTXO_POLICY_SERVER_HTLC_RECV_V0 =>
1196			{
1197				Ok(Self::User(decode_vtxo_policy(type_byte, r)?))
1198			},
1199			VTXO_POLICY_SERVER_OWNED => Ok(Self::ServerOwned),
1200			VTXO_POLICY_CHECKPOINT => {
1201				let user_pubkey = PublicKey::decode(r)?;
1202				Ok(Self::Checkpoint(CheckpointVtxoPolicy { user_pubkey }))
1203			},
1204			VTXO_POLICY_EXPIRY => {
1205				let internal_key = XOnlyPublicKey::decode(r)?;
1206				Ok(Self::Expiry(ExpiryVtxoPolicy { internal_key }))
1207			},
1208			VTXO_POLICY_HARK_LEAF => {
1209				let user_pubkey = PublicKey::decode(r)?;
1210				let unlock_hash = sha256::Hash::decode(r)?;
1211				Ok(Self::HarkLeaf(HarkLeafVtxoPolicy { user_pubkey, unlock_hash }))
1212			},
1213			VTXO_POLICY_HARK_LEAF_V0 => {
1214				let user_pubkey = PublicKey::decode(r)?;
1215				let unlock_hash = sha256::Hash::decode(r)?;
1216				Ok(Self::HarkLeaf_v0(HarkLeaf_v0_VtxoPolicy { user_pubkey, unlock_hash }))
1217			},
1218			VTXO_POLICY_HARK_FORFEIT => {
1219				let user_pubkey = PublicKey::decode(r)?;
1220				let unlock_hash = sha256::Hash::decode(r)?;
1221				Ok(Self::HarkForfeit(HarkForfeitVtxoPolicy { user_pubkey, unlock_hash }))
1222			},
1223			VTXO_POLICY_HARK_FORFEIT_V0 => {
1224				let user_pubkey = PublicKey::decode(r)?;
1225				let unlock_hash = sha256::Hash::decode(r)?;
1226				Ok(Self::HarkForfeit_v0(HarkForfeit_v0_VtxoPolicy { user_pubkey, unlock_hash }))
1227			},
1228			v => Err(ProtocolDecodingError::invalid(format_args!(
1229				"invalid ServerVtxoPolicy type byte: {v:#x}",
1230			))),
1231		}
1232	}
1233}
1234
1235/// The byte used to encode the [GenesisTransition::Cosigned] gen transition type.
1236const GENESIS_TRANSITION_TYPE_COSIGNED: u8 = 1;
1237
1238/// The byte used to encode the [GenesisTransition::Arkoor] gen transition type.
1239const GENESIS_TRANSITION_TYPE_ARKOOR: u8 = 2;
1240
1241/// The byte used to encode the [GenesisTransition::HashLockedCosigned_v0] gen transition type.
1242const GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED_V0: u8 = 3;
1243
1244/// The byte used to encode the [GenesisTransition::HashLockedCosigned] gen transition type.
1245const GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED: u8 = 4;
1246
1247impl ProtocolEncoding for GenesisTransition {
1248	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
1249		match self {
1250			Self::Cosigned(t) => {
1251				w.emit_u8(GENESIS_TRANSITION_TYPE_COSIGNED)?;
1252				LengthPrefixedVector::new(&t.pubkeys).encode(w)?;
1253				t.signature.encode(w)?;
1254			},
1255			Self::HashLockedCosigned(t) => {
1256				w.emit_u8(GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED)?;
1257				t.user_pubkey.encode(w)?;
1258				t.signature.encode(w)?;
1259				match t.unlock {
1260					MaybePreimage::Preimage(p) => {
1261						w.emit_u8(0)?;
1262						w.emit_slice(&p[..])?;
1263					},
1264					MaybePreimage::Hash(h) => {
1265						w.emit_u8(1)?;
1266						w.emit_slice(&h[..])?;
1267					},
1268				}
1269			},
1270			Self::HashLockedCosigned_v0(t) => {
1271				w.emit_u8(GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED_V0)?;
1272				t.user_pubkey.encode(w)?;
1273				t.signature.encode(w)?;
1274				match t.unlock {
1275					MaybePreimage::Preimage(p) => {
1276						w.emit_u8(0)?;
1277						w.emit_slice(&p[..])?;
1278					},
1279					MaybePreimage::Hash(h) => {
1280						w.emit_u8(1)?;
1281						w.emit_slice(&h[..])?;
1282					},
1283				}
1284			},
1285			Self::Arkoor(t) => {
1286				w.emit_u8(GENESIS_TRANSITION_TYPE_ARKOOR)?;
1287				LengthPrefixedVector::new(&t.client_cosigners).encode(w)?;
1288				t.tap_tweak.encode(w)?;
1289				t.signature.encode(w)?;
1290			},
1291		}
1292		Ok(())
1293	}
1294
1295	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
1296		match r.read_u8()? {
1297			GENESIS_TRANSITION_TYPE_COSIGNED => {
1298				let pubkeys: Vec<PublicKey> = LengthPrefixedVector::decode(r)?.into_inner();
1299				if pubkeys.is_empty() {
1300					return Err(ProtocolDecodingError::invalid(
1301						"cosigned genesis transition with empty pubkey list",
1302					));
1303				}
1304				let signature = Option::<schnorr::Signature>::decode(r)?;
1305				Ok(Self::new_cosigned(pubkeys, signature))
1306			},
1307			GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED => {
1308				let user_pubkey = PublicKey::decode(r)?;
1309				let signature = Option::<schnorr::Signature>::decode(r)?;
1310				let unlock = match r.read_u8()? {
1311					0 => MaybePreimage::Preimage(r.read_byte_array()?),
1312					1 => MaybePreimage::Hash(ProtocolEncoding::decode(r)?),
1313					v => return Err(ProtocolDecodingError::invalid(format_args!(
1314						"invalid MaybePreimage type byte: {v:#x}",
1315					))),
1316				};
1317				Ok(Self::HashLockedCosigned(genesis::HashLockedCosignedGenesis {
1318					user_pubkey, signature, unlock,
1319				}))
1320			},
1321			GENESIS_TRANSITION_TYPE_HASH_LOCKED_COSIGNED_V0 => {
1322				let user_pubkey = PublicKey::decode(r)?;
1323				let signature = Option::<schnorr::Signature>::decode(r)?;
1324				let unlock = match r.read_u8()? {
1325					0 => MaybePreimage::Preimage(r.read_byte_array()?),
1326					1 => MaybePreimage::Hash(ProtocolEncoding::decode(r)?),
1327					v => return Err(ProtocolDecodingError::invalid(format_args!(
1328						"invalid MaybePreimage type byte: {v:#x}",
1329					))),
1330				};
1331				Ok(Self::HashLockedCosigned_v0(genesis::HashLockedCosignedGenesis_v0 {
1332					user_pubkey, signature, unlock,
1333				}))
1334			},
1335			GENESIS_TRANSITION_TYPE_ARKOOR => {
1336				let cosigners = LengthPrefixedVector::decode(r)?.into_inner();
1337				let taptweak = TapTweakHash::decode(r)?;
1338				if bitcoin::secp256k1::Scalar::from_be_bytes(taptweak.to_byte_array()).is_err() {
1339					return Err(ProtocolDecodingError::invalid(
1340						"arkoor genesis tap tweak is not a valid secp256k1 scalar",
1341					));
1342				}
1343				let signature = Option::<schnorr::Signature>::decode(r)?;
1344				Ok(Self::new_arkoor(cosigners, taptweak, signature))
1345			},
1346			v => Err(ProtocolDecodingError::invalid(format_args!(
1347				"invalid GenesisTransistion type byte: {v:#x}",
1348			))),
1349		}
1350	}
1351}
1352
1353/// A private trait for VTXO sub-objects that have different encodings dependent on
1354/// the VTXO encoding version
1355trait VtxoVersionedEncoding: Sized {
1356	fn encode<W: io::Write + ?Sized>(&self, w: &mut W, version: u16) -> Result<(), io::Error>;
1357
1358	fn decode<R: io::Read + ?Sized>(
1359		r: &mut R,
1360		version: u16,
1361	) -> Result<Self, ProtocolDecodingError>;
1362}
1363
1364impl VtxoVersionedEncoding for Bare {
1365	fn encode<W: io::Write + ?Sized>(&self, w: &mut W, _version: u16) -> Result<(), io::Error> {
1366		w.emit_compact_size(0u64)?;
1367		Ok(())
1368	}
1369
1370	fn decode<R: io::Read + ?Sized>(
1371		r: &mut R,
1372		version: u16,
1373	) -> Result<Self, ProtocolDecodingError> {
1374		// We want to be compatible with [Full] encoded VTXOs, so we just ignore
1375		// whatever genesis there might be.
1376		let _full = Full::decode(r, version)?;
1377
1378		Ok(Bare)
1379	}
1380}
1381
1382impl VtxoVersionedEncoding for Full {
1383	fn encode<W: io::Write + ?Sized>(&self, w: &mut W, _version: u16) -> Result<(), io::Error> {
1384		w.emit_compact_size(self.items.len() as u64)?;
1385		for item in &self.items {
1386			item.transition.encode(w)?;
1387			let nb_outputs = item.other_outputs.len().saturating_add(1);
1388			w.emit_u8(nb_outputs.try_into()
1389				.map_err(|_| io::Error::other("too many outputs on genesis transaction"))?)?;
1390			w.emit_u8(item.output_idx)?;
1391			for txout in &item.other_outputs {
1392				txout.encode(w)?;
1393			}
1394			w.emit_u64(item.fee_amount.to_sat())?;
1395		}
1396		Ok(())
1397	}
1398
1399	fn decode<R: io::Read + ?Sized>(
1400		r: &mut R,
1401		version: u16,
1402	) -> Result<Self, ProtocolDecodingError> {
1403		let nb_genesis_items = r.read_compact_size()? as usize;
1404		OversizedVectorError::check::<GenesisItem>(nb_genesis_items)?;
1405		let mut genesis = Vec::with_capacity(nb_genesis_items);
1406		for _ in 0..nb_genesis_items {
1407			let transition = GenesisTransition::decode(r)?;
1408			let nb_outputs = r.read_u8()? as usize;
1409			let output_idx = r.read_u8()?;
1410			let nb_other = nb_outputs.checked_sub(1)
1411				.ok_or_else(|| ProtocolDecodingError::invalid("genesis item with 0 outputs"))?;
1412			// `output_idx` MUST index a real output of the exit tx. Otherwise
1413			// `GenesisItem::tx` clamps the placement and the VTXO's `point` ends
1414			// up referencing a sibling output or the anyone-can-spend P2A fee
1415			// anchor rather than the transition's own output, breaking the
1416			// invariant that `point` is fully determined by the genesis data.
1417			if output_idx as usize >= nb_outputs {
1418				return Err(ProtocolDecodingError::invalid(
1419					"genesis item output_idx out of range (>= nb_outputs)",
1420				));
1421			}
1422			let mut other_outputs = Vec::with_capacity(nb_other);
1423			for _ in 0..nb_other {
1424				other_outputs.push(TxOut::decode(r)?);
1425			}
1426			let fee_amount = if version == VTXO_NO_FEE_AMOUNT_VERSION {
1427				// Maintain backwards compatibility by assuming a fee of zero.
1428				Amount::ZERO
1429			} else {
1430				Amount::from_sat(r.read_u64()?)
1431			};
1432			genesis.push(GenesisItem { transition, output_idx, other_outputs, fee_amount });
1433		}
1434		Ok(Full { items: genesis })
1435	}
1436}
1437
1438impl<P: Policy + ProtocolEncoding> ProtocolEncoding for Vtxo<Bare, P> {
1439	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
1440		vtxo_encode_inner(&self, w)
1441	}
1442
1443	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
1444		Ok(vtxo_decode_inner(r)?.0)
1445	}
1446}
1447
1448impl<P: Policy + ProtocolEncoding> ProtocolEncoding for Vtxo<Full, P> {
1449	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
1450		vtxo_encode_inner(&self, w)
1451	}
1452
1453	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
1454		// Only allow coding Vtxo<Full> with no genesis items if the VTXO is a virtual
1455		// representation of an onchain UTXO.
1456		let (vtxo, _) = vtxo_decode_inner::<Full, P, _>(r)?;
1457		if vtxo.point() != vtxo.chain_anchor() {
1458			if vtxo.genesis.items.is_empty() {
1459				return Err(ProtocolDecodingError::invalid_err(
1460					VtxoValidationError::MissingGenesisItems,
1461					format!("VTXO {} has no genesis item data", vtxo.id()),
1462				));
1463			}
1464		} else {
1465			if !vtxo.genesis.items.is_empty() {
1466				return Err(ProtocolDecodingError::invalid_err(
1467					VtxoValidationError::UnexpectedGenesisItems,
1468					format!("decoded genesis item data when there shouldn't be any for VTXO {}", vtxo.id()),
1469				));
1470			}
1471		}
1472		Ok(vtxo)
1473	}
1474}
1475
1476fn vtxo_encode_inner<G, P, W>(vtxo: &Vtxo<G, P>, w: &mut W) -> Result<(), io::Error>
1477where
1478	G: VtxoVersionedEncoding,
1479	P: Policy + ProtocolEncoding,
1480	W: io::Write + ?Sized,
1481{
1482	let version = VTXO_ENCODING_VERSION;
1483	w.emit_u16(version)?;
1484	w.emit_u64(vtxo.amount.to_sat())?;
1485	w.emit_u32(vtxo.expiry_height)?;
1486	vtxo.server_pubkey.encode(w)?;
1487	w.emit_u16(vtxo.exit_delta)?;
1488	vtxo.anchor_point.encode(w)?;
1489
1490	vtxo.genesis.encode(w, version)?;
1491
1492	vtxo.policy.encode(w)?;
1493	vtxo.point.encode(w)?;
1494	Ok(())
1495}
1496
1497fn vtxo_decode_inner<G, P, R>(r: &mut R) -> Result<(Vtxo<G, P>, u16), ProtocolDecodingError>
1498where
1499	G: VtxoVersionedEncoding,
1500	P: Policy + ProtocolEncoding,
1501	R: io::Read + ?Sized,
1502{
1503	let version = r.read_u16()?;
1504	if version != VTXO_ENCODING_VERSION && version != VTXO_NO_FEE_AMOUNT_VERSION {
1505		return Err(ProtocolDecodingError::invalid(format_args!(
1506			"invalid Vtxo encoding version byte: {version:#x}",
1507		)));
1508	}
1509
1510	let amount = Amount::from_sat(r.read_u64()?);
1511	let expiry_height = check_block_height(r.read_u32()?)
1512		.map_err(|e| ProtocolDecodingError::invalid_err(e, "expiry_height"))?;
1513	let server_pubkey = PublicKey::decode(r)?;
1514	let exit_delta = check_block_delta(r.read_u16()?)
1515		.map_err(|e| ProtocolDecodingError::invalid_err(e, "exit_delta"))?;
1516	let anchor_point = OutPoint::decode(r)?;
1517
1518	let genesis = VtxoVersionedEncoding::decode(r, version)?;
1519
1520	let policy = P::decode(r)?;
1521	let point = OutPoint::decode(r)?;
1522	let vtxo = Vtxo {
1523		amount, expiry_height, server_pubkey, exit_delta, anchor_point, genesis, policy, point,
1524	};
1525	Ok((vtxo, version))
1526}
1527
1528#[cfg(test)]
1529mod test {
1530	use bitcoin::consensus::encode::serialize_hex;
1531	use bitcoin::hex::DisplayHex;
1532
1533	use crate::test_util::encoding_roundtrip;
1534	use crate::test_util::dummy::{DUMMY_SERVER_KEY, DUMMY_USER_KEY};
1535	use crate::test_util::vectors::{
1536		generate_vtxo_vectors, VTXO_VECTORS, VTXO_NO_FEE_AMOUNT_VERSION_HEXES,
1537	};
1538
1539	use super::*;
1540
1541	#[test]
1542	fn test_generate_vtxo_vectors() {
1543		let g = generate_vtxo_vectors();
1544		// the generation code prints its inner values
1545
1546		println!("\n\ngenerated:");
1547		println!("  anchor_tx: {}", serialize_hex(&g.anchor_tx));
1548		println!("  board_vtxo: {}", g.board_vtxo.serialize().as_hex().to_string());
1549		println!("  arkoor_htlc_out_vtxo: {}", g.arkoor_htlc_out_vtxo.serialize().as_hex().to_string());
1550		println!("  arkoor2_vtxo: {}", g.arkoor2_vtxo.serialize().as_hex().to_string());
1551		println!("  round_tx: {}", serialize_hex(&g.round_tx));
1552		println!("  round1_vtxo: {}", g.round1_vtxo.serialize().as_hex().to_string());
1553		println!("  round2_vtxo: {}", g.round2_vtxo.serialize().as_hex().to_string());
1554		println!("  arkoor3_vtxo: {}", g.arkoor3_vtxo.serialize().as_hex().to_string());
1555
1556
1557		let v = &*VTXO_VECTORS;
1558		println!("\n\nstatic:");
1559		println!("  anchor_tx: {}", serialize_hex(&v.anchor_tx));
1560		println!("  board_vtxo: {}", v.board_vtxo.serialize().as_hex().to_string());
1561		println!("  arkoor_htlc_out_vtxo: {}", v.arkoor_htlc_out_vtxo.serialize().as_hex().to_string());
1562		println!("  arkoor2_vtxo: {}", v.arkoor2_vtxo.serialize().as_hex().to_string());
1563		println!("  round_tx: {}", serialize_hex(&v.round_tx));
1564		println!("  round1_vtxo: {}", v.round1_vtxo.serialize().as_hex().to_string());
1565		println!("  round2_vtxo: {}", v.round2_vtxo.serialize().as_hex().to_string());
1566		println!("  arkoor3_vtxo: {}", v.arkoor3_vtxo.serialize().as_hex().to_string());
1567
1568		assert_eq!(g.anchor_tx, v.anchor_tx, "anchor_tx does not match");
1569		assert_eq!(g.board_vtxo, v.board_vtxo, "board_vtxo does not match");
1570		assert_eq!(g.arkoor_htlc_out_vtxo, v.arkoor_htlc_out_vtxo, "arkoor_htlc_out_vtxo does not match");
1571		assert_eq!(g.arkoor2_vtxo, v.arkoor2_vtxo, "arkoor2_vtxo does not match");
1572		assert_eq!(g.round_tx, v.round_tx, "round_tx does not match");
1573		assert_eq!(g.round1_vtxo, v.round1_vtxo, "round1_vtxo does not match");
1574		assert_eq!(g.round2_vtxo, v.round2_vtxo, "round2_vtxo does not match");
1575		assert_eq!(g.arkoor3_vtxo, v.arkoor3_vtxo, "arkoor3_vtxo does not match");
1576
1577		// this passes because the Eq is based on id which doesn't compare signatures
1578		assert_eq!(g, *v);
1579	}
1580
1581	#[test]
1582	fn test_vtxo_no_fee_amount_version_upgrade() {
1583		let hexes = &*VTXO_NO_FEE_AMOUNT_VERSION_HEXES;
1584		let v = hexes.deserialize_test_vectors();
1585
1586		// Ensure all VTXOs validate correctly.
1587		v.validate_vtxos();
1588
1589		// Ensure each VTXO serializes and is different from the old hex.
1590		let board_hex = v.board_vtxo.serialize().as_hex().to_string();
1591		let arkoor_htlc_out_vtxo_hex = v.arkoor_htlc_out_vtxo.serialize().as_hex().to_string();
1592		let arkoor2_vtxo_hex = v.arkoor2_vtxo.serialize().as_hex().to_string();
1593		let round1_vtxo_hex = v.round1_vtxo.serialize().as_hex().to_string();
1594		let round2_vtxo_hex = v.round2_vtxo.serialize().as_hex().to_string();
1595		let arkoor3_vtxo_hex = v.arkoor3_vtxo.serialize().as_hex().to_string();
1596		assert_ne!(board_hex, hexes.board_vtxo);
1597		assert_ne!(arkoor_htlc_out_vtxo_hex, hexes.arkoor_htlc_out_vtxo);
1598		assert_ne!(arkoor2_vtxo_hex, hexes.arkoor2_vtxo);
1599		assert_ne!(round1_vtxo_hex, hexes.round1_vtxo);
1600		assert_ne!(round2_vtxo_hex, hexes.round2_vtxo);
1601		assert_ne!(arkoor3_vtxo_hex, hexes.arkoor3_vtxo);
1602
1603		// Now verify that deserializing them again results in exactly the same hex. This should be
1604		// the case because the initial hex strings should have been created with a different
1605		// version, then, when we serialize the VTXOs, we should use the newest version. If you
1606		// deserialize a VTXO with the latest version and serialize it, you should get the same
1607		// result.
1608		let board_vtxo = Vtxo::<Full>::deserialize_hex(&board_hex).unwrap();
1609		assert_eq!(board_vtxo.serialize().as_hex().to_string(), board_hex);
1610		let arkoor_htlc_out_vtxo = Vtxo::<Full>::deserialize_hex(&arkoor_htlc_out_vtxo_hex).unwrap();
1611		assert_eq!(arkoor_htlc_out_vtxo.serialize().as_hex().to_string(), arkoor_htlc_out_vtxo_hex);
1612		let arkoor2_vtxo = Vtxo::<Full>::deserialize_hex(&arkoor2_vtxo_hex).unwrap();
1613		assert_eq!(arkoor2_vtxo.serialize().as_hex().to_string(), arkoor2_vtxo_hex);
1614		let round1_vtxo = Vtxo::<Full>::deserialize_hex(&round1_vtxo_hex).unwrap();
1615		assert_eq!(round1_vtxo.serialize().as_hex().to_string(), round1_vtxo_hex);
1616		let round2_vtxo = Vtxo::<Full>::deserialize_hex(&round2_vtxo_hex).unwrap();
1617		assert_eq!(round2_vtxo.serialize().as_hex().to_string(), round2_vtxo_hex);
1618		let arkoor3_vtxo = Vtxo::<Full>::deserialize_hex(&arkoor3_vtxo_hex).unwrap();
1619		assert_eq!(arkoor3_vtxo.serialize().as_hex().to_string(), arkoor3_vtxo_hex);
1620	}
1621
1622	#[test]
1623	fn exit_depth() {
1624		let vtxos = &*VTXO_VECTORS;
1625		// board
1626		assert_eq!(vtxos.board_vtxo.exit_depth(), 1 /* cosign */);
1627
1628		// round
1629		assert_eq!(vtxos.round1_vtxo.exit_depth(), 3 /* cosign */);
1630
1631		// arkoor
1632		assert_eq!(
1633			vtxos.arkoor_htlc_out_vtxo.exit_depth(),
1634			1 /* cosign */ + 1 /* checkpoint*/ + 1 /* arkoor */,
1635		);
1636		assert_eq!(
1637			vtxos.arkoor2_vtxo.exit_depth(),
1638			1 /* cosign */ + 2 /* checkpoint */ + 2 /* arkoor */,
1639		);
1640		assert_eq!(
1641			vtxos.arkoor3_vtxo.exit_depth(),
1642			3 /* cosign */ + 1 /* checkpoint */ + 1 /* arkoor */,
1643		);
1644	}
1645
1646	#[test]
1647	fn ancestor_ids() {
1648		let v = &*VTXO_VECTORS;
1649
1650		// A board VTXO is its own chain anchor: a single genesis tx producing the
1651		// VTXO itself, hence no ancestors.
1652		assert_eq!(v.board_vtxo.exit_depth(), 1, "board is a single-tx chain anchor");
1653		assert!(v.board_vtxo.ancestor_ids().is_empty(),
1654			"a chain-anchor VTXO has no ancestors");
1655
1656		// For every fixture: ancestor_ids is the whole genesis chain minus the
1657		// VTXO itself — length one less than the chain, never the VTXO's own id,
1658		// and the chain's final tx produces the VTXO itself (the invariant
1659		// ancestor_ids relies on).
1660		for vtxo in [
1661			&v.board_vtxo, &v.arkoor_htlc_out_vtxo, &v.arkoor2_vtxo,
1662			&v.round1_vtxo, &v.round2_vtxo, &v.arkoor3_vtxo,
1663		] {
1664			let ancestors = vtxo.ancestor_ids();
1665
1666			assert_eq!(ancestors.len(), vtxo.exit_depth() as usize - 1,
1667				"ancestor_ids is the whole genesis chain except the VTXO itself");
1668			assert!(!ancestors.contains(&vtxo.id()),
1669				"ancestor_ids must never contain the VTXO's own id");
1670
1671			let last = vtxo.transactions().last().expect("a VTXO has >=1 transaction");
1672			let last_id: VtxoId = OutPoint::new(last.tx.compute_txid(), last.output_idx as u32).into();
1673			assert_eq!(last_id, vtxo.id(),
1674				"the final genesis tx must produce the VTXO itself");
1675		}
1676
1677		// The recovery-critical property: a VTXO's ancestor set contains the id
1678		// of every owned VTXO it (transitively) spent, ordered chain-anchor-first,
1679		// so recovery can skip a parent spent into a newer recovered child.
1680
1681		// board -> arkoor1: arkoor1 spent the board.
1682		assert!(v.arkoor_htlc_out_vtxo.ancestor_ids().contains(&v.board_vtxo.id()),
1683			"a single-hop arkoor lists the board it spent as an ancestor");
1684
1685		// board -> arkoor1 -> arkoor2: arkoor2 lists both, ordered anchor-first.
1686		let anc2 = v.arkoor2_vtxo.ancestor_ids();
1687		let board_pos = anc2.iter().position(|id| *id == v.board_vtxo.id())
1688			.expect("arkoor2 must list the board ancestor");
1689		let arkoor1_pos = anc2.iter().position(|id| *id == v.arkoor_htlc_out_vtxo.id())
1690			.expect("arkoor2 must list the arkoor1 ancestor");
1691		assert!(board_pos < arkoor1_pos,
1692			"ancestors are ordered from chain anchor down to the immediate parent");
1693
1694		// A child's ancestor chain begins with its parent's whole chain (the
1695		// parent's own ancestors followed by the parent itself).
1696		let mut parent_chain = v.arkoor_htlc_out_vtxo.ancestor_ids();
1697		parent_chain.push(v.arkoor_htlc_out_vtxo.id());
1698		assert!(v.arkoor2_vtxo.ancestor_ids().starts_with(&parent_chain),
1699			"a child's ancestors extend its parent's full genesis chain");
1700
1701		// round2 -> arkoor3: an arkoor built on a round output lists that output.
1702		assert!(v.arkoor3_vtxo.ancestor_ids().contains(&v.round2_vtxo.id()),
1703			"an arkoor spending a round output lists it as an ancestor");
1704	}
1705
1706	#[test]
1707	fn test_split_genesis_roundtrip() {
1708		// For each fixture, splitting the encoding into bare bytes + genesis
1709		// bytes and reassembling must produce a byte-identical full VTXO. This
1710		// is the load-bearing invariant for the m0029 storage migration.
1711		fn check<P: Policy + ProtocolEncoding + Clone + std::fmt::Debug>(
1712			vtxo: &Vtxo<Full, P>,
1713		) where
1714			Vtxo<Full, P>: PartialEq,
1715		{
1716			let original = vtxo.serialize();
1717
1718			let bare_bytes = vtxo.to_bare().serialize();
1719			let genesis_bytes = vtxo.serialize_genesis();
1720
1721			let bare = Vtxo::<Bare, P>::deserialize(&bare_bytes)
1722				.expect("bare deserialize");
1723			let genesis = Full::decode(&mut &genesis_bytes[..], VTXO_ENCODING_VERSION)
1724				.expect("decode_genesis");
1725			let reassembled = bare.with_genesis(genesis)
1726				.expect("reassemble");
1727
1728			assert_eq!(*vtxo, reassembled, "reassembled vtxo differs from original");
1729			assert_eq!(reassembled.serialize(), original,
1730				"reassembled bytes differ from original");
1731		}
1732
1733		let v = &*VTXO_VECTORS;
1734		check(&v.board_vtxo);
1735		check(&v.arkoor_htlc_out_vtxo);
1736		check(&v.arkoor2_vtxo);
1737		check(&v.round1_vtxo);
1738		check(&v.round2_vtxo);
1739		check(&v.arkoor3_vtxo);
1740
1741		// Also exercise a depth-257 genesis to cover compact_size > 252.
1742		let big: Vtxo<Full> = Vtxo {
1743			policy: VtxoPolicy::new_pubkey(DUMMY_USER_KEY.public_key()),
1744			amount: Amount::from_sat(10_000),
1745			expiry_height: 101_010,
1746			server_pubkey: DUMMY_SERVER_KEY.public_key(),
1747			exit_delta: 2016,
1748			anchor_point: OutPoint::new(Txid::from_slice(&[1u8; 32]).unwrap(), 1),
1749			genesis: Full {
1750				items: vec![GenesisItem {
1751					transition: GenesisTransition::new_cosigned(
1752						vec![DUMMY_USER_KEY.public_key()],
1753						Some(schnorr::Signature::from_slice(&[2u8; 64]).unwrap()),
1754					),
1755					output_idx: 0,
1756					other_outputs: vec![],
1757					fee_amount: Amount::ZERO,
1758				}; 257],
1759			},
1760			point: OutPoint::new(Txid::from_slice(&[3u8; 32]).unwrap(), 3),
1761		};
1762		check(&big);
1763	}
1764
1765	#[test]
1766	fn test_genesis_length_257() {
1767		let vtxo: Vtxo<Full> = Vtxo {
1768			policy: VtxoPolicy::new_pubkey(DUMMY_USER_KEY.public_key()),
1769			amount: Amount::from_sat(10_000),
1770			expiry_height: 101_010,
1771			server_pubkey: DUMMY_SERVER_KEY.public_key(),
1772			exit_delta: 2016,
1773			anchor_point: OutPoint::new(Txid::from_slice(&[1u8; 32]).unwrap(), 1),
1774			genesis: Full {
1775				items: vec![GenesisItem {
1776					transition: GenesisTransition::new_cosigned(
1777						vec![DUMMY_USER_KEY.public_key()],
1778						Some(schnorr::Signature::from_slice(&[2u8; 64]).unwrap()),
1779					),
1780					output_idx: 0,
1781					other_outputs: vec![],
1782					fee_amount: Amount::ZERO,
1783				}; 257],
1784			},
1785			point: OutPoint::new(Txid::from_slice(&[3u8; 32]).unwrap(), 3),
1786		};
1787		assert_eq!(vtxo.genesis.items.len(), 257);
1788		encoding_roundtrip(&vtxo);
1789	}
1790
1791	#[test]
1792	fn test_genesis_decoding() {
1793		// We should disallow decoding a Vtxo<Bare> as a Vtxo<Full> since it's nonsensical and will
1794		// only lead to confusing errors, such as when validating a VTXO.
1795		fn check<P: Policy + ProtocolEncoding + Clone + std::fmt::Debug>(
1796			vtxo: &Vtxo<Full, P>,
1797		) where
1798			Vtxo<Full, P>: PartialEq,
1799		{
1800			let full_bytes = vtxo.serialize();
1801			let bare_bytes = vtxo.as_bare_vtxo().unwrap().serialize();
1802
1803			// We should support the following:
1804			// - Full -> Full
1805			// - Full -> Bare
1806			// - Bare -> Bare
1807			// We should disallow Bare -> Full.
1808			let full_to_full = Vtxo::<Full>::deserialize(&full_bytes).expect("works");
1809			let full_to_bare = Vtxo::<Bare>::deserialize(&full_bytes).expect("works");
1810			let bare_to_bare = Vtxo::<Bare>::deserialize(&bare_bytes).expect("works");
1811			Vtxo::<Full>::deserialize(&bare_bytes).expect_err("bare to full fails");
1812
1813			assert_eq!(full_to_full.serialize(), full_bytes);
1814			assert_eq!(full_to_bare.serialize(), bare_bytes);
1815			assert_eq!(bare_to_bare.serialize(), bare_bytes);
1816		}
1817
1818		let v = &*VTXO_VECTORS;
1819		check(&v.board_vtxo);
1820		check(&v.arkoor_htlc_out_vtxo);
1821		check(&v.arkoor2_vtxo);
1822		check(&v.round1_vtxo);
1823		check(&v.round2_vtxo);
1824		check(&v.arkoor3_vtxo);
1825	}
1826
1827	/// Build a minimal single-item [Vtxo<Full>] for standardness tests.
1828	///
1829	/// The genesis chain is one cosigned transition wide; callers control
1830	/// the VTXO's own amount and the sibling outputs in that transition.
1831	fn dummy_vtxo_with(amount: Amount, other_outputs: Vec<TxOut>) -> Vtxo<Full> {
1832		Vtxo {
1833			policy: VtxoPolicy::new_pubkey(DUMMY_USER_KEY.public_key()),
1834			amount,
1835			expiry_height: 101_010,
1836			server_pubkey: DUMMY_SERVER_KEY.public_key(),
1837			exit_delta: 2016,
1838			anchor_point: OutPoint::new(Txid::from_slice(&[1u8; 32]).unwrap(), 1),
1839			genesis: Full {
1840				items: vec![GenesisItem {
1841					transition: GenesisTransition::new_cosigned(
1842						vec![DUMMY_USER_KEY.public_key()],
1843						Some(schnorr::Signature::from_slice(&[2u8; 64]).unwrap()),
1844					),
1845					output_idx: 0,
1846					other_outputs,
1847					fee_amount: Amount::ZERO,
1848				}],
1849			},
1850			point: OutPoint::new(Txid::from_slice(&[3u8; 32]).unwrap(), 3),
1851		}
1852	}
1853
1854	/// A valid P2TR script_pubkey usable as a sibling output.
1855	fn dummy_p2tr_script() -> ScriptBuf {
1856		VtxoPolicy::new_pubkey(DUMMY_USER_KEY.public_key())
1857			.script_pubkey(DUMMY_SERVER_KEY.public_key(), 2016, 101_010)
1858	}
1859
1860	#[test]
1861	fn check_standard_accepts_real_vtxos() {
1862		// The hand-rolled test vectors must all be standard so that
1863		// regular VTXO use never falsely trips check_standard.
1864		let v = &*VTXO_VECTORS;
1865		assert_eq!(v.board_vtxo.check_standard(), Ok(()));
1866		assert_eq!(v.arkoor_htlc_out_vtxo.check_standard(), Ok(()));
1867		assert_eq!(v.arkoor2_vtxo.check_standard(), Ok(()));
1868		assert_eq!(v.round1_vtxo.check_standard(), Ok(()));
1869		assert_eq!(v.round2_vtxo.check_standard(), Ok(()));
1870		assert_eq!(v.arkoor3_vtxo.check_standard(), Ok(()));
1871		assert!(v.board_vtxo.is_standard());
1872	}
1873
1874	#[test]
1875	fn check_standard_dusty_own_output() {
1876		// A VTXO whose own output is below P2TR_DUST is Dusty. The
1877		// VtxoPolicy script is always P2TR, so the dust limit is 330 sat.
1878		let vtxo = dummy_vtxo_with(Amount::from_sat(100), vec![]);
1879		assert_eq!(vtxo.check_standard(), Err(VtxoStandardnessError::Dusty));
1880		assert!(!vtxo.is_standard());
1881	}
1882
1883	#[test]
1884	fn check_standard_dust_sibling() {
1885		// Own amount is fine, but a sub-dust P2TR sibling output along
1886		// the exit chain should surface as DustSibling and point at the
1887		// offending position.
1888		let dust = TxOut {
1889			value: Amount::from_sat(100),
1890			script_pubkey: dummy_p2tr_script(),
1891		};
1892		let vtxo = dummy_vtxo_with(Amount::from_sat(10_000), vec![dust]);
1893		assert_eq!(
1894			vtxo.check_standard(),
1895			Err(VtxoStandardnessError::DustSibling {
1896				item_idx: 0,
1897				item_count: 1,
1898				output_idx: 0,
1899			}),
1900		);
1901	}
1902
1903	#[test]
1904	fn check_standard_script_sibling() {
1905		// A sibling using an unrecognised script template (here just a
1906		// pair of arbitrary bytes that match none of P2PKH/P2SH/P2WPKH/
1907		// P2WSH/P2TR/OP_RETURN) trips ScriptSibling regardless of value.
1908		let bad = TxOut {
1909			value: Amount::from_sat(10_000),
1910			script_pubkey: ScriptBuf::from_bytes(vec![0xab, 0xcd]),
1911		};
1912		let vtxo = dummy_vtxo_with(Amount::from_sat(10_000), vec![bad]);
1913		assert_eq!(
1914			vtxo.check_standard(),
1915			Err(VtxoStandardnessError::ScriptSibling {
1916				item_idx: 0,
1917				item_count: 1,
1918				output_idx: 0,
1919			}),
1920		);
1921	}
1922
1923	#[test]
1924	fn check_standard_dust_takes_priority_over_later_script_sibling() {
1925		// The check short-circuits on the first violation: a sub-dust
1926		// sibling earlier in the list wins over a bad-script one later.
1927		let dust = TxOut {
1928			value: Amount::from_sat(100),
1929			script_pubkey: dummy_p2tr_script(),
1930		};
1931		let bad = TxOut {
1932			value: Amount::from_sat(10_000),
1933			script_pubkey: ScriptBuf::from_bytes(vec![0xab, 0xcd]),
1934		};
1935		let vtxo = dummy_vtxo_with(Amount::from_sat(10_000), vec![dust, bad]);
1936		assert_eq!(
1937			vtxo.check_standard(),
1938			Err(VtxoStandardnessError::DustSibling {
1939				item_idx: 0,
1940				item_count: 1,
1941				output_idx: 0,
1942			}),
1943		);
1944	}
1945
1946	mod genesis_transition_encoding {
1947		use bitcoin::hashes::{sha256, Hash};
1948		use bitcoin::secp256k1::{Keypair, PublicKey};
1949		use bitcoin::taproot::TapTweakHash;
1950		use std::str::FromStr;
1951
1952		use crate::encode::ProtocolEncoding;
1953		use crate::test_util::encoding_roundtrip;
1954		use super::genesis::{
1955			GenesisTransition, CosignedGenesis, HashLockedCosignedGenesis_v0, ArkoorGenesis,
1956		};
1957		use super::MaybePreimage;
1958
1959		fn test_pubkey() -> PublicKey {
1960			Keypair::from_str(
1961				"916da686cedaee9a9bfb731b77439f2a3f1df8664e16488fba46b8d2bfe15e92"
1962			).unwrap().public_key()
1963		}
1964
1965		fn test_signature() -> bitcoin::secp256k1::schnorr::Signature {
1966			"cc8b93e9f6fbc2506bb85ae8bbb530b178daac49704f5ce2e3ab69c266fd5932\
1967			 0b28d028eef212e3b9fdc42cfd2e0760a0359d3ea7d2e9e8cfe2040e3f1b71ea"
1968				.parse().unwrap()
1969		}
1970
1971		#[test]
1972		fn cosigned_with_signature() {
1973			let transition = GenesisTransition::Cosigned(CosignedGenesis {
1974				pubkeys: vec![test_pubkey()],
1975				signature: Some(test_signature()),
1976			});
1977			encoding_roundtrip(&transition);
1978		}
1979
1980		#[test]
1981		fn cosigned_without_signature() {
1982			let transition = GenesisTransition::Cosigned(CosignedGenesis {
1983				pubkeys: vec![test_pubkey()],
1984				signature: None,
1985			});
1986			encoding_roundtrip(&transition);
1987		}
1988
1989		#[test]
1990		fn cosigned_empty_pubkeys_rejected() {
1991			let mut buf = Vec::new();
1992			buf.push(super::GENESIS_TRANSITION_TYPE_COSIGNED);
1993			buf.push(0x00); // LengthPrefixedVector length = 0
1994			buf.push(0x00); // Option::<Signature> = None
1995			let err = GenesisTransition::deserialize(&mut buf.as_slice())
1996				.expect_err("empty pubkeys must be rejected");
1997			assert!(format!("{err}").contains("empty pubkey list"), "got: {err}");
1998		}
1999
2000		#[test]
2001		fn cosigned_multiple_pubkeys() {
2002			let pk1 = test_pubkey();
2003			let pk2 = Keypair::from_str(
2004				"fab9e598081a3e74b2233d470c4ad87bcc285b6912ed929568e62ac0e9409879"
2005			).unwrap().public_key();
2006
2007			let transition = GenesisTransition::Cosigned(CosignedGenesis {
2008				pubkeys: vec![pk1, pk2],
2009				signature: Some(test_signature()),
2010			});
2011			encoding_roundtrip(&transition);
2012		}
2013
2014		#[test]
2015		fn hash_locked_cosigned_with_preimage() {
2016			let preimage = [0x42u8; 32];
2017			let transition = GenesisTransition::HashLockedCosigned_v0(HashLockedCosignedGenesis_v0 {
2018				user_pubkey: test_pubkey(),
2019				signature: Some(test_signature()),
2020				unlock: MaybePreimage::Preimage(preimage),
2021			});
2022			encoding_roundtrip(&transition);
2023		}
2024
2025		#[test]
2026		fn hash_locked_cosigned_with_hash() {
2027			let hash = sha256::Hash::hash(b"test preimage");
2028			let transition = GenesisTransition::HashLockedCosigned_v0(HashLockedCosignedGenesis_v0 {
2029				user_pubkey: test_pubkey(),
2030				signature: Some(test_signature()),
2031				unlock: MaybePreimage::Hash(hash),
2032			});
2033			encoding_roundtrip(&transition);
2034		}
2035
2036		#[test]
2037		fn hash_locked_cosigned_without_signature() {
2038			let preimage = [0x42u8; 32];
2039			let transition = GenesisTransition::HashLockedCosigned_v0(HashLockedCosignedGenesis_v0 {
2040				user_pubkey: test_pubkey(),
2041				signature: None,
2042				unlock: MaybePreimage::Preimage(preimage),
2043			});
2044			encoding_roundtrip(&transition);
2045		}
2046
2047		#[test]
2048		fn arkoor_with_signature() {
2049			let tap_tweak = TapTweakHash::from_slice(&[0xabu8; 32]).unwrap();
2050			let transition = GenesisTransition::Arkoor(ArkoorGenesis {
2051				client_cosigners: vec![test_pubkey()],
2052				tap_tweak,
2053				signature: Some(test_signature()),
2054			});
2055			encoding_roundtrip(&transition);
2056		}
2057
2058		#[test]
2059		fn arkoor_without_signature() {
2060			let tap_tweak = TapTweakHash::from_slice(&[0xabu8; 32]).unwrap();
2061			let transition = GenesisTransition::Arkoor(ArkoorGenesis {
2062				client_cosigners: vec![test_pubkey()],
2063				tap_tweak,
2064				signature: None,
2065			});
2066			encoding_roundtrip(&transition);
2067		}
2068
2069		#[test]
2070		fn arkoor_out_of_range_tweak_rejected() {
2071			// A tap tweak at or above the secp256k1 curve order is not a valid
2072			// musig scalar and would panic in `musig::tweaked_key_agg` during
2073			// validation; decoding must reject it at the untrusted-input boundary.
2074			let valid = GenesisTransition::Arkoor(ArkoorGenesis {
2075				client_cosigners: vec![test_pubkey()],
2076				tap_tweak: TapTweakHash::from_slice(&[0xabu8; 32]).unwrap(),
2077				signature: None,
2078			});
2079			let mut bytes = valid.serialize();
2080			// Trailing layout is [tap_tweak: 32 bytes][signature: 64 bytes];
2081			// overwrite the tweak with all-ones, which exceeds the curve order.
2082			let n = bytes.len();
2083			for b in &mut bytes[n - 96 .. n - 64] {
2084				*b = 0xff;
2085			}
2086			let err = GenesisTransition::deserialize(&mut bytes.as_slice())
2087				.expect_err("out-of-range tap tweak must be rejected");
2088			assert!(
2089				format!("{err}").contains("not a valid secp256k1 scalar"),
2090				"got: {err}",
2091			);
2092		}
2093	}
2094}