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 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 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
129pub fn validate(
132 vtxo: &Vtxo,
133 chain_anchor_tx: &Transaction,
134) -> Result<(), VtxoValidationError> {
135 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 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 match &item.transition {
159 GenesisTransition::Cosigned { .. } => {},
160 GenesisTransition::HashLockedCosigned { .. } => {
161 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 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 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}