ark/vtxo/
validation.rs

1
2use std::borrow::Cow;
3
4use bitcoin::{sighash, Amount, OutPoint, TapLeafHash, Transaction, TxOut};
5
6use bitcoin_ext::TxOutExt;
7
8use crate::tree::signed::unlock_clause;
9use crate::{musig, SECP};
10use crate::vtxo::{GenesisTransition, Vtxo, VtxoPolicyKind};
11
12
13#[derive(Debug, PartialEq, Eq, thiserror::Error)]
14#[error("VTXO validation error")]
15pub enum VtxoValidationError {
16	#[error("the VTXO is invalid: {0}")]
17	Invalid(&'static str),
18	#[error("the chain anchor output doesn't match the VTXO; expected: {expected:?}, got: {got:?}")]
19	IncorrectChainAnchor {
20		expected: TxOut,
21		got: TxOut,
22	},
23	#[error("Cosigned genesis transitions don't have any common pubkeys")]
24	InconsistentCosignPubkeys,
25	#[error("error verifying one of the genesis transitions \
26		(idx={genesis_idx}/{genesis_len} type={transition_type}): {error}")]
27	GenesisTransition {
28		error: &'static str,
29		genesis_idx: usize,
30		genesis_len: usize,
31		transition_type: &'static str,
32	},
33	#[error("non-standard output on genesis item #{genesis_item_idx} other \
34		output #{other_output_idx}")]
35	NonStandardTxOut {
36		genesis_item_idx: usize,
37		other_output_idx: usize,
38	},
39	#[error("invalid arkoor policy of type {policy}: {msg}")]
40	InvalidArkoorPolicy {
41		policy: VtxoPolicyKind,
42		msg: &'static str,
43	}
44}
45
46impl VtxoValidationError {
47	/// Constructor for [VtxoValidationError::GenesisTransition].
48	fn transition(
49		genesis_idx: usize,
50		genesis_len: usize,
51		transition_type: &'static str,
52		error: &'static str,
53	) -> Self {
54		VtxoValidationError::GenesisTransition { error, genesis_idx, genesis_len, transition_type }
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, vtxo.expiry_height)
77	});
78
79	let prevout = OutPoint::new(prev_tx.compute_txid(), prev_vout as u32);
80	let tx = item.tx(prevout, next_output, vtxo.server_pubkey, vtxo.expiry_height);
81
82	let sighash = match item.transition {
83		GenesisTransition::HashLockedCosigned { user_pubkey, unlock, .. } => {
84			let mut shc = sighash::SighashCache::new(&tx);
85			let agg_pk = musig::combine_keys([user_pubkey, vtxo.server_pubkey]);
86			let script = unlock_clause(agg_pk, unlock.hash());
87			let leaf = TapLeafHash::from_script(&script, bitcoin::taproot::LeafVersion::TapScript);
88			shc.taproot_script_spend_signature_hash(
89				0, &sighash::Prevouts::All(&[prev_txout]), leaf, sighash::TapSighashType::Default,
90			).expect("correct prevouts")
91		},
92		GenesisTransition::Cosigned { .. } | GenesisTransition::Arkoor { .. } => {
93			let mut shc = sighash::SighashCache::new(&tx);
94			shc.taproot_key_spend_signature_hash(
95				0, &sighash::Prevouts::All(&[prev_txout]), sighash::TapSighashType::Default,
96			).expect("correct prevouts")
97		},
98	};
99
100	let pubkey = {
101		let transition_taproot = item.transition.input_taproot(
102			vtxo.server_pubkey(), vtxo.expiry_height(), vtxo.exit_delta(),
103		);
104		transition_taproot.output_key().to_x_only_public_key()
105	};
106
107	let signature = match item.transition {
108		GenesisTransition::Cosigned { signature, .. } => signature,
109		GenesisTransition::HashLockedCosigned { signature, .. } => signature,
110		GenesisTransition::Arkoor { signature: Some(signature), .. } => signature,
111		GenesisTransition::Arkoor { signature: None, .. } => {
112			return Err("missing arkoor signature");
113		},
114	};
115
116	SECP.verify_schnorr(&signature, &sighash.into(), &pubkey)
117		.map_err(|_| "invalid signature")?;
118
119	#[cfg(test)]
120	{
121		if let Err(e) = crate::test::verify_tx(&[prev_txout.clone()], 0, &tx) {
122			panic!("invalid tx in genesis of vtxo {}: idx={}: {:#}", vtxo.id(), genesis_idx, e);
123		}
124	}
125
126	Ok(tx)
127}
128
129/// Validate that the [Vtxo] is valid and can be constructed from its
130/// chain anchor.
131pub fn validate(
132	vtxo: &Vtxo,
133	chain_anchor_tx: &Transaction,
134) -> Result<(), VtxoValidationError> {
135	// We start by validating the chain anchor output.
136	let anchor_txout = chain_anchor_tx.output.get(vtxo.chain_anchor().vout as usize)
137		.ok_or(VtxoValidationError::Invalid("chain anchor vout out of range"))?;
138	let onchain_amount = vtxo.amount() + vtxo.genesis.iter().map(|i| {
139		i.other_outputs.iter().map(|o| o.value).sum()
140	}).sum();
141	let expected_anchor_txout = vtxo.genesis.get(0).unwrap().transition.input_txout(
142		onchain_amount, vtxo.server_pubkey(), vtxo.expiry_height(), vtxo.exit_delta(),
143	);
144	if *anchor_txout != expected_anchor_txout {
145		return Err(VtxoValidationError::IncorrectChainAnchor { expected: expected_anchor_txout, got: anchor_txout.clone() });
146	}
147
148	// Every VTXO should have one or more `Cosigned` transitions, followed by 0 or more
149	// `Arkoor` transitions.
150	if vtxo.genesis.is_empty() {
151		return Err(VtxoValidationError::Invalid("no genesis items"));
152	}
153
154	let mut prev = (Cow::Borrowed(chain_anchor_tx), vtxo.chain_anchor().vout as usize, onchain_amount);
155	let mut iter = vtxo.genesis.iter().enumerate().peekable();
156	while let Some((idx, item)) = iter.next() {
157		// transition-dependent validation
158		match &item.transition {
159			GenesisTransition::Cosigned { .. } => {},
160			GenesisTransition::HashLockedCosigned { .. } => {
161				// can only be followed by arkoor
162				if let Some((_idx, next)) = iter.peek() {
163					match &next.transition {
164						GenesisTransition::Arkoor { .. } => {},
165						GenesisTransition::Cosigned { .. }
166						| GenesisTransition::HashLockedCosigned { .. } => {
167							return Err(VtxoValidationError::transition(
168								idx, vtxo.genesis.len(), item.transition.transition_type(),
169								"hash-locked cosigned transition must \
170									be followed by arkoor transitions",
171							));
172						},
173					}
174				}
175			},
176			GenesisTransition::Arkoor { policy, .. } => {
177				if policy.arkoor_pubkey().is_none() {
178					return Err(VtxoValidationError::InvalidArkoorPolicy {
179						policy: policy.policy_type(),
180						msg: "arkoor transition without arkoor pubkey",
181					});
182				}
183
184				// can only be followed by more arkoor
185				if let Some((_idx, next)) = iter.peek() {
186					match &next.transition {
187						GenesisTransition::Arkoor { .. } => {},
188						GenesisTransition::Cosigned { .. }
189						| GenesisTransition::HashLockedCosigned { .. } => {
190							return Err(VtxoValidationError::transition(
191								idx, vtxo.genesis.len(), item.transition.transition_type(),
192								"Arkoor transition must be followed by arkoor transitions",
193							));
194						},
195					}
196				}
197			},
198		}
199
200		// All outputs have to be standard otherwise we can't relay.
201		if let Some(out_idx) = item.other_outputs.iter().position(|o| !o.is_standard()) {
202			return Err(VtxoValidationError::NonStandardTxOut {
203				genesis_item_idx: idx,
204				other_output_idx: out_idx,
205			});
206		}
207
208		let next_amount = prev.2.checked_sub(item.other_outputs.iter().map(|o| o.value).sum())
209			.ok_or(VtxoValidationError::Invalid("insufficient onchain amount"))?;
210		let next_tx = verify_transition(&vtxo, idx, prev.0.as_ref(), prev.1, next_amount)
211			.map_err(|e| VtxoValidationError::transition(
212				idx, vtxo.genesis.len(), item.transition.transition_type(), e,
213			))?;
214		prev = (Cow::Owned(next_tx), item.output_idx as usize, next_amount);
215	}
216
217	Ok(())
218}
219
220#[cfg(test)]
221mod test {
222	use crate::vtxo::test::VTXO_VECTORS;
223
224	#[test]
225	pub fn validate_vtxos() {
226		let vtxos = &*VTXO_VECTORS;
227
228		let err = vtxos.board_vtxo.validate(&vtxos.anchor_tx).err();
229		assert!(err.is_none(), "err: {err:?}");
230
231		let err = vtxos.arkoor_htlc_out_vtxo.validate(&vtxos.anchor_tx).err();
232		assert!(err.is_none(), "err: {err:?}");
233
234		let err = vtxos.arkoor2_vtxo.validate(&vtxos.anchor_tx).err();
235		assert!(err.is_none(), "err: {err:?}");
236
237		let err = vtxos.round1_vtxo.validate(&vtxos.round_tx).err();
238		assert!(err.is_none(), "err: {err:?}");
239
240		let err = vtxos.round2_vtxo.validate(&vtxos.round_tx).err();
241		assert!(err.is_none(), "err: {err:?}");
242
243		let err = vtxos.arkoor3_vtxo.validate(&vtxos.round_tx).err();
244		assert!(err.is_none(), "err: {err:?}");
245	}
246}