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