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}
36
37impl VtxoValidationError {
38 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 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 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 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 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 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 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
172pub 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
184pub 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}