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 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 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 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(), u32::try_from(prev_vout).expect("vout fits in 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 GenesisTransition::HashLockedCosigned_v0(inner) => {
92 inner.validate_sigs(&tx, 0, prev_txout, vtxo.server_pubkey, vtxo.expiry_height)?
93 }
94 };
95
96 #[cfg(test)]
97 {
98 if let Err(e) = crate::test_util::verify_tx(&[prev_txout.clone()], 0, &tx) {
99 println!("TX VALIDATION FAILED: invalid tx in genesis of vtxo {}: idx={}: {}",
101 vtxo.id(), genesis_idx, e,
102 );
103 return Err("transaction validation failed");
104 }
105 }
106 }
107
108
109 Ok(tx)
110}
111
112fn validate_inner<P: Policy>(
113 vtxo: &Vtxo<Full, P>,
114 chain_anchor_tx: &Transaction,
115 check_signatures: bool,
116) -> Result<(), VtxoValidationError> {
117 let anchor_txout = chain_anchor_tx.output.get(vtxo.chain_anchor().vout as usize)
119 .ok_or(VtxoValidationError::Invalid("chain anchor vout out of range"))?;
120
121 if vtxo.genesis.items.is_empty() {
123 let expected_anchor_txout = vtxo.policy.txout(
124 vtxo.amount(),
125 vtxo.server_pubkey(),
126 vtxo.exit_delta(),
127 vtxo.expiry_height(),
128 );
129 if *anchor_txout != expected_anchor_txout {
130 return Err(VtxoValidationError::IncorrectChainAnchor {
131 expected: expected_anchor_txout,
132 got: anchor_txout.clone(),
133 });
134 }
135
136 if vtxo.point != vtxo.chain_anchor() {
137 return Err(VtxoValidationError::Invalid(
138 "point of empty genesis vtxo doesn't match anchor point",
139 ));
140 }
141 return Ok(());
142 }
143
144 let onchain_amount = vtxo.chain_anchor_amount()
146 .ok_or_else(|| VtxoValidationError::Invalid("onchain amount overflow"))?;
147 let expected_anchor_txout = vtxo.genesis.items.get(0).unwrap().transition.input_txout(
148 onchain_amount, vtxo.server_pubkey(), vtxo.expiry_height(), vtxo.exit_delta(),
149 );
150 if *anchor_txout != expected_anchor_txout {
151 return Err(VtxoValidationError::IncorrectChainAnchor {
152 expected: expected_anchor_txout,
153 got: anchor_txout.clone(),
154 });
155 }
156
157 let mut prev = (Cow::Borrowed(chain_anchor_tx), vtxo.chain_anchor().vout as usize, onchain_amount);
158 for (idx, item) in vtxo.genesis.items.iter().enumerate() {
159 let output_sum = item.other_output_sum()
160 .ok_or(VtxoValidationError::Invalid("output sum overflow"))?;
161 let next_amount = prev.2.checked_sub(output_sum)
162 .ok_or(VtxoValidationError::Invalid("insufficient onchain amount"))?;
163 let next_tx = verify_transition(&vtxo, idx, prev.0.as_ref(), prev.1, next_amount, check_signatures)
164 .map_err(|e| VtxoValidationError::transition(
165 idx, vtxo.genesis.items.len(), item.transition.kind(), e,
166 ))?;
167 prev = (Cow::Owned(next_tx), item.output_idx as usize, next_amount);
168 }
169
170 let expected_point = OutPoint::new(prev.0.compute_txid(), u32::try_from(prev.1).expect("vout fits in u32"));
172 if vtxo.point != expected_point {
173 return Err(VtxoValidationError::Invalid("point doesn't match computed exit outpoint"));
174 }
175
176 Ok(())
177}
178
179pub fn validate<P: Policy>(
185 vtxo: &Vtxo<Full, P>,
186 chain_anchor_tx: &Transaction,
187) -> Result<(), VtxoValidationError> {
188 validate_inner(vtxo, chain_anchor_tx, true)
189}
190
191pub fn validate_unsigned<P: Policy>(
193 vtxo: &Vtxo<Full, P>,
194 chain_anchor_tx: &Transaction,
195) -> Result<(), VtxoValidationError> {
196 validate_inner(vtxo, chain_anchor_tx, false)
197}
198
199#[cfg(test)]
200mod test {
201 use bitcoin::{OutPoint, Transaction};
202
203 use crate::{ProtocolEncoding, Vtxo};
204 use crate::vtxo::Full;
205 use crate::test_util::VTXO_VECTORS;
206
207 #[test]
208 pub fn validate_vtxos() {
209 let vtxos = &*VTXO_VECTORS;
210
211 assert!(vtxos.board_vtxo.is_standard());
212 let err = vtxos.board_vtxo.validate(&vtxos.anchor_tx).err();
213 assert!(err.is_none(), "err: {err:?}");
214
215 assert!(vtxos.arkoor_htlc_out_vtxo.is_standard());
216 let err = vtxos.arkoor_htlc_out_vtxo.validate(&vtxos.anchor_tx).err();
217 assert!(err.is_none(), "err: {err:?}");
218
219 assert!(vtxos.arkoor2_vtxo.is_standard());
220 let err = vtxos.arkoor2_vtxo.validate(&vtxos.anchor_tx).err();
221 assert!(err.is_none(), "err: {err:?}");
222
223 assert!(vtxos.round1_vtxo.is_standard());
224 let err = vtxos.round1_vtxo.validate(&vtxos.round_tx).err();
225 assert!(err.is_none(), "err: {err:?}");
226
227 assert!(vtxos.round2_vtxo.is_standard());
228 let err = vtxos.round2_vtxo.validate(&vtxos.round_tx).err();
229 assert!(err.is_none(), "err: {err:?}");
230
231 assert!(vtxos.arkoor3_vtxo.is_standard());
232 let err = vtxos.arkoor3_vtxo.validate(&vtxos.round_tx).err();
233 assert!(err.is_none(), "err: {err:?}");
234 }
235
236 #[test]
237 fn terminal_output_idx_out_of_range_is_rejected() {
238 fn check(mut vtxo: Vtxo<Full>, anchor: &Transaction) {
239 vtxo.validate(anchor).expect("baseline vtxo must validate");
241 assert_eq!(vtxo.point().vout, 0, "fixtures place the funds at vout 0");
242 let genesis_txid = vtxo.point().txid;
243
244 let nb_outputs = {
247 let item = vtxo.genesis.items.last_mut().unwrap();
248 let nb = item.other_outputs.len() + 1;
249 item.output_idx = nb as u8; nb
251 };
252 vtxo.point = OutPoint::new(genesis_txid, nb_outputs as u32);
253
254 let bytes = vtxo.serialize();
258 let res = Vtxo::<Full>::deserialize(&bytes);
259 assert!(res.is_err(),
260 "decoder must reject a genesis item with output_idx >= nb_outputs; \
261 accepted a VTXO whose point is the fee anchor: {:?}",
262 res.map(|v| v.point()));
263 }
264
265 check(VTXO_VECTORS.board_vtxo.clone(), &VTXO_VECTORS.anchor_tx);
268 check(VTXO_VECTORS.arkoor2_vtxo.clone(), &VTXO_VECTORS.anchor_tx);
269 }
270}