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