Skip to main content

ark/vtxo/
validation.rs

1
2use std::borrow::Cow;
3
4use bitcoin::{Amount, OutPoint, Transaction, TxOut};
5
6use crate::vtxo::{Full, Policy, Vtxo, VtxoPolicyKind};
7use crate::vtxo::genesis::{GenesisTransition, TransitionKind};
8
9#[derive(Debug, PartialEq, Eq, thiserror::Error)]
10#[error("VTXO validation error")]
11pub enum VtxoValidationError {
12	#[error("the VTXO is invalid: {0}")]
13	Invalid(&'static str),
14	#[error("the chain anchor output doesn't match the VTXO; expected: {expected:?}, got: {got:?}")]
15	IncorrectChainAnchor {
16		expected: TxOut,
17		got: TxOut,
18	},
19	#[error("Cosigned genesis transitions don't have any common pubkeys")]
20	InconsistentCosignPubkeys,
21	#[error("error verifying one of the genesis transitions \
22		(idx={genesis_idx}/{genesis_len} type={transition_kind}): {error}")]
23	GenesisTransition {
24		error: &'static str,
25		genesis_idx: usize,
26		genesis_len: usize,
27		// NB we use str here because we don't want to expose the kind enum
28		transition_kind: &'static str,
29	},
30	#[error("invalid arkoor policy of type {policy}: {msg}")]
31	InvalidArkoorPolicy {
32		policy: VtxoPolicyKind,
33		msg: &'static str,
34	},
35	#[error("Expected genesis items but found none")]
36	MissingGenesisItems,
37	#[error("Genesis items were found but none were expected")]
38	UnexpectedGenesisItems,
39}
40
41impl VtxoValidationError {
42	/// Constructor for [VtxoValidationError::GenesisTransition].
43	fn transition(
44		genesis_idx: usize,
45		genesis_len: usize,
46		transition_kind: TransitionKind,
47		error: &'static str,
48	) -> Self {
49		let transition_kind = transition_kind.as_str();
50		VtxoValidationError::GenesisTransition { error, genesis_idx, genesis_len, transition_kind }
51	}
52}
53
54#[inline]
55#[allow(unused_variables)]
56fn verify_transition<P: Policy>(
57	vtxo: &Vtxo<Full, P>,
58	genesis_idx: usize,
59	prev_tx: &Transaction,
60	prev_vout: usize,
61	next_amount: Amount,
62	check_signatures: bool,
63) -> Result<Transaction, &'static str> {
64	let item = vtxo.genesis.items.get(genesis_idx).expect("genesis_idx out of range");
65
66	let prev_txout = prev_tx.output.get(prev_vout).ok_or_else(|| "output idx out of range")?;
67
68	let next_output = vtxo.genesis.items.get(genesis_idx.saturating_add(1)).map(|item| {
69		item.transition.input_txout(
70			next_amount, vtxo.server_pubkey, vtxo.expiry_height, vtxo.exit_delta,
71		)
72	}).unwrap_or_else(|| {
73		// when we reach the end of the chain, we take the eventual output of the vtxo
74		vtxo.policy.txout(vtxo.amount, vtxo.server_pubkey, vtxo.exit_delta, vtxo.expiry_height)
75	});
76
77	let prevout = OutPoint::new(prev_tx.compute_txid(), prev_vout as u32);
78	let tx = item.tx(prevout, next_output, vtxo.server_pubkey, vtxo.expiry_height);
79
80	if check_signatures {
81		match &item.transition {
82			GenesisTransition::Cosigned(inner) => {
83				inner.validate_sigs(&tx, 0, prev_txout, vtxo.server_pubkey, vtxo.expiry_height)?
84			}
85			GenesisTransition::Arkoor(inner) => {
86				inner.validate_sigs(&tx, 0, prev_txout, vtxo.server_pubkey())?
87			}
88			GenesisTransition::HashLockedCosigned(inner) => {
89				inner.validate_sigs(&tx, 0, prev_txout, vtxo.server_pubkey, vtxo.expiry_height)?
90			}
91		};
92
93		#[cfg(test)]
94		{
95			if let Err(e) = crate::test_util::verify_tx(&[prev_txout.clone()], 0, &tx) {
96				// just print error because this is unit test context
97				println!("TX VALIDATION FAILED: invalid tx in genesis of vtxo {}: idx={}: {}",
98					vtxo.id(), genesis_idx, e,
99				);
100				return Err("transaction validation failed");
101			}
102		}
103	}
104
105
106	Ok(tx)
107}
108
109fn validate_inner<P: Policy>(
110	vtxo: &Vtxo<Full, P>,
111	chain_anchor_tx: &Transaction,
112	check_signatures: bool,
113) -> Result<(), VtxoValidationError> {
114	// We start by validating the chain anchor output.
115	let anchor_txout = chain_anchor_tx.output.get(vtxo.chain_anchor().vout as usize)
116		.ok_or(VtxoValidationError::Invalid("chain anchor vout out of range"))?;
117
118	// For empty genesis, validate that the chain anchor output matches the policy's txout
119	if vtxo.genesis.items.is_empty() {
120		let expected_anchor_txout = vtxo.policy.txout(
121			vtxo.amount(),
122			vtxo.server_pubkey(),
123			vtxo.exit_delta(),
124			vtxo.expiry_height(),
125		);
126		if *anchor_txout != expected_anchor_txout {
127			return Err(VtxoValidationError::IncorrectChainAnchor {
128				expected: expected_anchor_txout,
129				got: anchor_txout.clone(),
130			});
131		}
132
133		if vtxo.point != vtxo.chain_anchor() {
134			return Err(VtxoValidationError::Invalid(
135				"point of empty genesis vtxo doesn't match anchor point",
136			));
137		}
138		return Ok(());
139	}
140
141	// For non-empty genesis, validate using the first genesis item's transition
142	let onchain_amount = vtxo.chain_anchor_amount()
143		.ok_or_else(|| VtxoValidationError::Invalid("onchain amount overflow"))?;
144	let expected_anchor_txout = vtxo.genesis.items.get(0).unwrap().transition.input_txout(
145		onchain_amount, vtxo.server_pubkey(), vtxo.expiry_height(), vtxo.exit_delta(),
146	);
147	if *anchor_txout != expected_anchor_txout {
148		return Err(VtxoValidationError::IncorrectChainAnchor {
149			expected: expected_anchor_txout,
150			got: anchor_txout.clone(),
151		});
152	}
153
154	let mut prev = (Cow::Borrowed(chain_anchor_tx), vtxo.chain_anchor().vout as usize, onchain_amount);
155	for (idx, item) in vtxo.genesis.items.iter().enumerate() {
156		let output_sum = item.other_output_sum()
157			.ok_or(VtxoValidationError::Invalid("output sum overflow"))?;
158		let next_amount = prev.2.checked_sub(output_sum)
159			.ok_or(VtxoValidationError::Invalid("insufficient onchain amount"))?;
160		let next_tx = verify_transition(&vtxo, idx, prev.0.as_ref(), prev.1, next_amount, check_signatures)
161			.map_err(|e| VtxoValidationError::transition(
162				idx, vtxo.genesis.items.len(), item.transition.kind(), e,
163			))?;
164		prev = (Cow::Owned(next_tx), item.output_idx as usize, next_amount);
165	}
166
167	// Verify the point field matches the computed exit outpoint
168	let expected_point = OutPoint::new(prev.0.compute_txid(), prev.1 as u32);
169	if vtxo.point != expected_point {
170		return Err(VtxoValidationError::Invalid("point doesn't match computed exit outpoint"));
171	}
172
173	Ok(())
174}
175
176/// Validate that the [Vtxo] is valid and can be constructed from its
177/// chain anchor.
178///
179/// General checks and chain-anchor related checks are performed first,
180/// transitions are checked last.
181pub fn validate<P: Policy>(
182	vtxo: &Vtxo<Full, P>,
183	chain_anchor_tx: &Transaction,
184) -> Result<(), VtxoValidationError> {
185	validate_inner(vtxo, chain_anchor_tx, true)
186}
187
188/// Validate VTXO structure without checking signatures.
189pub fn validate_unsigned<P: Policy>(
190	vtxo: &Vtxo<Full, P>,
191	chain_anchor_tx: &Transaction,
192) -> Result<(), VtxoValidationError> {
193	validate_inner(vtxo, chain_anchor_tx, false)
194}
195
196#[cfg(test)]
197mod test {
198	use crate::test_util::VTXO_VECTORS;
199
200	#[test]
201	pub fn validate_vtxos() {
202		let vtxos = &*VTXO_VECTORS;
203
204		assert!(vtxos.board_vtxo.is_standard());
205		let err = vtxos.board_vtxo.validate(&vtxos.anchor_tx).err();
206		assert!(err.is_none(), "err: {err:?}");
207
208		assert!(vtxos.arkoor_htlc_out_vtxo.is_standard());
209		let err = vtxos.arkoor_htlc_out_vtxo.validate(&vtxos.anchor_tx).err();
210		assert!(err.is_none(), "err: {err:?}");
211
212		assert!(vtxos.arkoor2_vtxo.is_standard());
213		let err = vtxos.arkoor2_vtxo.validate(&vtxos.anchor_tx).err();
214		assert!(err.is_none(), "err: {err:?}");
215
216		assert!(vtxos.round1_vtxo.is_standard());
217		let err = vtxos.round1_vtxo.validate(&vtxos.round_tx).err();
218		assert!(err.is_none(), "err: {err:?}");
219
220		assert!(vtxos.round2_vtxo.is_standard());
221		let err = vtxos.round2_vtxo.validate(&vtxos.round_tx).err();
222		assert!(err.is_none(), "err: {err:?}");
223
224		assert!(vtxos.arkoor3_vtxo.is_standard());
225		let err = vtxos.arkoor3_vtxo.validate(&vtxos.round_tx).err();
226		assert!(err.is_none(), "err: {err:?}");
227	}
228}