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