ark/vtxo/
validation.rs

1
2use std::borrow::Cow;
3
4use bitcoin::{sighash, Amount, OutPoint, Transaction, TxOut};
5
6use bitcoin_ext::TxOutExt;
7
8use crate::SECP;
9use crate::vtxo::{GenesisTransition, Vtxo, VtxoPolicyKind};
10
11
12#[derive(Debug, PartialEq, Eq, thiserror::Error)]
13#[error("VTXO validation error")]
14pub enum VtxoValidationError {
15	#[error("the VTXO is invalid: {0}")]
16	Invalid(&'static str),
17	#[error("the chain anchor output doesn't match the VTXO; expected: {expected:?}, got: {got:?}")]
18	IncorrectChainAnchor {
19		expected: TxOut,
20		got: TxOut,
21	},
22	#[error("Cosigned genesis transitions don't have any common pubkeys")]
23	InconsistentCosignPubkeys,
24	#[error("error verifying one of the genesis transitions (idx={genesis_item_idx}): {error}")]
25	GenesisTransition {
26		error: &'static str,
27		genesis_item_idx: usize,
28	},
29	#[error("non-standard output on genesis item #{genesis_item_idx} other \
30		output #{other_output_idx}")]
31	NonStandardTxOut {
32		genesis_item_idx: usize,
33		other_output_idx: usize,
34	},
35	#[error("invalid arkoor policy of type {policy}: {msg}")]
36	InvalidArkoorPolicy {
37		policy: VtxoPolicyKind,
38		msg: &'static str,
39	}
40}
41
42impl VtxoValidationError {
43	/// Constructor for [VtxoValidationError::GenesisTransition].
44	fn transition(genesis_item_idx: usize, error: &'static str) -> Self {
45		VtxoValidationError::GenesisTransition { error, genesis_item_idx }
46	}
47}
48
49/// Result of VTXO validation
50#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
51pub enum ValidationResult {
52	/// The VTXO is safe as long as one of the given cosign pubkeys
53	/// is trusted to not create a double spend.
54	Cosigned,
55	/// The VTXO is safe as long as
56	/// - one of the cosign pubkeys does not create a double spend, and
57	/// - none of the arkoor pubkeys colludes with the server to create a
58	///   double spend.
59	CosignedArkoor,
60}
61
62
63#[inline]
64fn verify_transition(
65	vtxo: &Vtxo,
66	genesis_idx: usize,
67	prev_tx: &Transaction,
68	prev_vout: usize,
69	next_amount: Amount,
70) -> Result<Transaction, &'static str> {
71	let item = vtxo.genesis.get(genesis_idx).expect("genesis_idx out of range");
72
73	let prev_txout = prev_tx.output.get(prev_vout).ok_or_else(|| "output idx out of range")?;
74
75	let next_output = vtxo.genesis.get(genesis_idx + 1).map(|item| {
76		item.transition.input_txout(
77			next_amount, vtxo.server_pubkey, vtxo.expiry_height, vtxo.exit_delta,
78		)
79	}).unwrap_or_else(|| {
80		// when we reach the end of the chain, we take the eventual output of the vtxo
81		vtxo.policy.txout(vtxo.amount, vtxo.server_pubkey, vtxo.exit_delta)
82	});
83
84	let prevout = OutPoint::new(prev_tx.compute_txid(), prev_vout as u32);
85	let tx = item.tx(prevout, next_output);
86
87	let sighash = {
88		let mut shc = sighash::SighashCache::new(&tx);
89		shc.taproot_key_spend_signature_hash(
90			0, &sighash::Prevouts::All(&[prev_txout]), sighash::TapSighashType::Default,
91		).expect("correct prevouts")
92	};
93
94	let pubkey = {
95		let transition_taproot = item.transition.input_taproot(
96			vtxo.server_pubkey(), vtxo.expiry_height(), vtxo.exit_delta(),
97		);
98		transition_taproot.output_key().to_x_only_public_key()
99	};
100
101	let signature = match item.transition {
102		GenesisTransition::Cosigned { signature, .. } => signature,
103		GenesisTransition::Arkoor { signature: Some(signature), .. } => signature,
104		GenesisTransition::Arkoor { signature: None, .. } => {
105			return Err("missing arkoor signature");
106		},
107	};
108
109	SECP.verify_schnorr(&signature, &sighash.into(), &pubkey)
110		.map_err(|_| "invalid signature")?;
111
112	#[cfg(test)]
113	{
114		if let Err(e) = crate::test::verify_tx(&[prev_txout.clone()], 0, &tx) {
115			panic!("invalid tx in genesis of vtxo {}: idx={}: {:#}", vtxo.id(), genesis_idx, e);
116		}
117	}
118
119	Ok(tx)
120}
121
122#[inline]
123fn check_transitions_cosigned_then_arkoor<'a>(
124	transitions: impl Iterator<Item = &'a GenesisTransition> + Clone,
125) -> Result<(), VtxoValidationError> {
126	let cosigned = transitions.clone()
127		.take_while(|t| matches!(t, GenesisTransition::Cosigned { .. }));
128	if cosigned.count() < 1 {
129		return Err(VtxoValidationError::Invalid("should start with Cosigned genesis items"));
130	}
131	let mut after_cosigned = transitions.clone()
132		.skip_while(|t| matches!(t, GenesisTransition::Cosigned { .. }));
133	if !after_cosigned.all(|t| matches!(t, GenesisTransition::Arkoor { .. })) {
134		return Err(VtxoValidationError::Invalid(
135			"can only have Arkoor transitions after last Cosigned",
136		));
137	}
138	Ok(())
139}
140
141/// Validate that the [Vtxo] is valid and can be constructed from its
142/// chain anchor.
143pub fn validate(
144	vtxo: &Vtxo,
145	chain_anchor_tx: &Transaction,
146) -> Result<ValidationResult, VtxoValidationError> {
147	// We start by validating the chain anchor output.
148	let anchor_txout = chain_anchor_tx.output.get(vtxo.chain_anchor().vout as usize)
149		.ok_or(VtxoValidationError::Invalid("chain anchor vout out of range"))?;
150	let onchain_amount = vtxo.amount() + vtxo.genesis.iter().map(|i| {
151		i.other_outputs.iter().map(|o| o.value).sum()
152	}).sum();
153	let expected_anchor_txout = vtxo.genesis.get(0).unwrap().transition.input_txout(
154		onchain_amount, vtxo.server_pubkey(), vtxo.expiry_height(), vtxo.exit_delta(),
155	);
156	if *anchor_txout != expected_anchor_txout {
157		return Err(VtxoValidationError::IncorrectChainAnchor { expected: expected_anchor_txout, got: anchor_txout.clone() });
158	}
159
160	// Then let's go over each transition.
161	let transitions = vtxo.genesis.iter().map(|i| &i.transition);
162
163	// Every VTXO should have one or more `Cosigned` transitions, followed by 0 or more
164	// `Arkoor` transitions.
165	if vtxo.genesis.is_empty() {
166		return Err(VtxoValidationError::Invalid("no genesis items"));
167	}
168	check_transitions_cosigned_then_arkoor(transitions.clone())?;
169
170	let cosign_pubkeys = vtxo.round_cosign_pubkeys();
171	if cosign_pubkeys.is_empty() {
172		return Err(VtxoValidationError::InconsistentCosignPubkeys);
173	}
174
175	let mut has_arkoor = false;
176	let mut prev = (Cow::Borrowed(chain_anchor_tx), vtxo.chain_anchor().vout as usize, onchain_amount);
177	for (idx, item) in vtxo.genesis.iter().enumerate() {
178		// transition-dependent validation
179		match &item.transition {
180			GenesisTransition::Cosigned { .. } => {},
181			GenesisTransition::Arkoor { policy, .. } => {
182				has_arkoor = true;
183				if policy.arkoor_pubkey().is_none() {
184					return Err(VtxoValidationError::InvalidArkoorPolicy {
185						policy: policy.policy_type(),
186						msg: "arkoor transition without arkoor pubkey",
187					});
188				}
189			},
190		}
191
192		// All outputs have to be standard otherwise we can't relay.
193		if let Some(out_idx) = item.other_outputs.iter().position(|o| !o.is_standard()) {
194			return Err(VtxoValidationError::NonStandardTxOut {
195				genesis_item_idx: idx,
196				other_output_idx: out_idx,
197			});
198		}
199
200		let next_amount = prev.2.checked_sub(item.other_outputs.iter().map(|o| o.value).sum())
201			.ok_or(VtxoValidationError::Invalid("insufficient onchain amount"))?;
202		let next_tx = verify_transition(&vtxo, idx, prev.0.as_ref(), prev.1, next_amount)
203			.map_err(|e| VtxoValidationError::transition(idx, e))?;
204		prev = (Cow::Owned(next_tx), item.output_idx as usize, next_amount);
205	}
206
207	Ok(if has_arkoor { ValidationResult::CosignedArkoor } else { ValidationResult::Cosigned })
208}
209
210#[cfg(test)]
211mod test {
212	use crate::vtxo::test::VTXO_VECTORS;
213
214	#[test]
215	pub fn validate_vtxos() {
216		let vtxos = &*VTXO_VECTORS;
217
218		let err = vtxos.board_vtxo.validate(&vtxos.anchor_tx).err();
219		assert!(err.is_none(), "err: {err:?}");
220
221		let err = vtxos.arkoor_htlc_out_vtxo.validate(&vtxos.anchor_tx).err();
222		assert!(err.is_none(), "err: {err:?}");
223
224		let err = vtxos.arkoor2_vtxo.validate(&vtxos.anchor_tx).err();
225		assert!(err.is_none(), "err: {err:?}");
226
227		let err = vtxos.round1_vtxo.validate(&vtxos.round_tx).err();
228		assert!(err.is_none(), "err: {err:?}");
229
230		let err = vtxos.round2_vtxo.validate(&vtxos.round_tx).err();
231		assert!(err.is_none(), "err: {err:?}");
232
233		let err = vtxos.arkoor3_vtxo.validate(&vtxos.round_tx).err();
234		assert!(err.is_none(), "err: {err:?}");
235	}
236}