ark/vtxo/
validation.rs

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