1
2use std::borrow::Cow;
3
4use bitcoin::{sighash, Amount, OutPoint, Transaction, TxOut};
5
6use bitcoin_ext::TxOutExt;
7
8use crate::SECP;
9use crate::vtxo::{GenesisTransition, Vtxo, VtxoPolicyKind};
10
11
12#[derive(Debug, PartialEq, Eq, thiserror::Error)]
13#[error("VTXO validation error")]
14pub enum VtxoValidationError {
15 #[error("the VTXO is invalid: {0}")]
16 Invalid(&'static str),
17 #[error("the chain anchor output doesn't match the VTXO; expected: {expected:?}, got: {got:?}")]
18 IncorrectChainAnchor {
19 expected: TxOut,
20 got: TxOut,
21 },
22 #[error("Cosigned genesis transitions don't have any common pubkeys")]
23 InconsistentCosignPubkeys,
24 #[error("error verifying one of the genesis transitions (idx={genesis_item_idx}): {error}")]
25 GenesisTransition {
26 error: &'static str,
27 genesis_item_idx: usize,
28 },
29 #[error("non-standard output on genesis item #{genesis_item_idx} other \
30 output #{other_output_idx}")]
31 NonStandardTxOut {
32 genesis_item_idx: usize,
33 other_output_idx: usize,
34 },
35 #[error("invalid arkoor policy of type {policy}: {msg}")]
36 InvalidArkoorPolicy {
37 policy: VtxoPolicyKind,
38 msg: &'static str,
39 }
40}
41
42impl VtxoValidationError {
43 fn transition(genesis_item_idx: usize, error: &'static str) -> Self {
45 VtxoValidationError::GenesisTransition { error, genesis_item_idx }
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
51pub enum ValidationResult {
52 Cosigned,
55 CosignedArkoor,
60}
61
62
63#[inline]
64fn verify_transition(
65 vtxo: &Vtxo,
66 genesis_idx: usize,
67 prev_tx: &Transaction,
68 prev_vout: usize,
69 next_amount: Amount,
70) -> Result<Transaction, &'static str> {
71 let item = vtxo.genesis.get(genesis_idx).expect("genesis_idx out of range");
72
73 let prev_txout = prev_tx.output.get(prev_vout).ok_or_else(|| "output idx out of range")?;
74
75 let next_output = vtxo.genesis.get(genesis_idx + 1).map(|item| {
76 item.transition.input_txout(
77 next_amount, vtxo.server_pubkey, vtxo.expiry_height, vtxo.exit_delta,
78 )
79 }).unwrap_or_else(|| {
80 vtxo.policy.txout(vtxo.amount, vtxo.server_pubkey, vtxo.exit_delta)
82 });
83
84 let prevout = OutPoint::new(prev_tx.compute_txid(), prev_vout as u32);
85 let tx = item.tx(prevout, next_output);
86
87 let sighash = {
88 let mut shc = sighash::SighashCache::new(&tx);
89 shc.taproot_key_spend_signature_hash(
90 0, &sighash::Prevouts::All(&[prev_txout]), sighash::TapSighashType::Default,
91 ).expect("correct prevouts")
92 };
93
94 let pubkey = {
95 let transition_taproot = item.transition.input_taproot(
96 vtxo.server_pubkey(), vtxo.expiry_height(), vtxo.exit_delta(),
97 );
98 transition_taproot.output_key().to_x_only_public_key()
99 };
100
101 let signature = match item.transition {
102 GenesisTransition::Cosigned { signature, .. } => signature,
103 GenesisTransition::Arkoor { signature: Some(signature), .. } => signature,
104 GenesisTransition::Arkoor { signature: None, .. } => {
105 return Err("missing arkoor signature");
106 },
107 };
108
109 SECP.verify_schnorr(&signature, &sighash.into(), &pubkey)
110 .map_err(|_| "invalid signature")?;
111
112 #[cfg(test)]
113 {
114 if let Err(e) = crate::test::verify_tx(&[prev_txout.clone()], 0, &tx) {
115 panic!("invalid tx in genesis of vtxo {}: idx={}: {:#}", vtxo.id(), genesis_idx, e);
116 }
117 }
118
119 Ok(tx)
120}
121
122#[inline]
123fn check_transitions_cosigned_then_arkoor<'a>(
124 transitions: impl Iterator<Item = &'a GenesisTransition> + Clone,
125) -> Result<(), VtxoValidationError> {
126 let cosigned = transitions.clone()
127 .take_while(|t| matches!(t, GenesisTransition::Cosigned { .. }));
128 if cosigned.count() < 1 {
129 return Err(VtxoValidationError::Invalid("should start with Cosigned genesis items"));
130 }
131 let mut after_cosigned = transitions.clone()
132 .skip_while(|t| matches!(t, GenesisTransition::Cosigned { .. }));
133 if !after_cosigned.all(|t| matches!(t, GenesisTransition::Arkoor { .. })) {
134 return Err(VtxoValidationError::Invalid(
135 "can only have Arkoor transitions after last Cosigned",
136 ));
137 }
138 Ok(())
139}
140
141pub fn validate(
144 vtxo: &Vtxo,
145 chain_anchor_tx: &Transaction,
146) -> Result<ValidationResult, VtxoValidationError> {
147 let anchor_txout = chain_anchor_tx.output.get(vtxo.chain_anchor().vout as usize)
149 .ok_or(VtxoValidationError::Invalid("chain anchor vout out of range"))?;
150 let onchain_amount = vtxo.amount() + vtxo.genesis.iter().map(|i| {
151 i.other_outputs.iter().map(|o| o.value).sum()
152 }).sum();
153 let expected_anchor_txout = vtxo.genesis.get(0).unwrap().transition.input_txout(
154 onchain_amount, vtxo.server_pubkey(), vtxo.expiry_height(), vtxo.exit_delta(),
155 );
156 if *anchor_txout != expected_anchor_txout {
157 return Err(VtxoValidationError::IncorrectChainAnchor { expected: expected_anchor_txout, got: anchor_txout.clone() });
158 }
159
160 let transitions = vtxo.genesis.iter().map(|i| &i.transition);
162
163 if vtxo.genesis.is_empty() {
166 return Err(VtxoValidationError::Invalid("no genesis items"));
167 }
168 check_transitions_cosigned_then_arkoor(transitions.clone())?;
169
170 let cosign_pubkeys = vtxo.round_cosign_pubkeys();
171 if cosign_pubkeys.is_empty() {
172 return Err(VtxoValidationError::InconsistentCosignPubkeys);
173 }
174
175 let mut has_arkoor = false;
176 let mut prev = (Cow::Borrowed(chain_anchor_tx), vtxo.chain_anchor().vout as usize, onchain_amount);
177 for (idx, item) in vtxo.genesis.iter().enumerate() {
178 match &item.transition {
180 GenesisTransition::Cosigned { .. } => {},
181 GenesisTransition::Arkoor { policy, .. } => {
182 has_arkoor = true;
183 if policy.arkoor_pubkey().is_none() {
184 return Err(VtxoValidationError::InvalidArkoorPolicy {
185 policy: policy.policy_type(),
186 msg: "arkoor transition without arkoor pubkey",
187 });
188 }
189 },
190 }
191
192 if let Some(out_idx) = item.other_outputs.iter().position(|o| !o.is_standard()) {
194 return Err(VtxoValidationError::NonStandardTxOut {
195 genesis_item_idx: idx,
196 other_output_idx: out_idx,
197 });
198 }
199
200 let next_amount = prev.2.checked_sub(item.other_outputs.iter().map(|o| o.value).sum())
201 .ok_or(VtxoValidationError::Invalid("insufficient onchain amount"))?;
202 let next_tx = verify_transition(&vtxo, idx, prev.0.as_ref(), prev.1, next_amount)
203 .map_err(|e| VtxoValidationError::transition(idx, e))?;
204 prev = (Cow::Owned(next_tx), item.output_idx as usize, next_amount);
205 }
206
207 Ok(if has_arkoor { ValidationResult::CosignedArkoor } else { ValidationResult::Cosigned })
208}
209
210#[cfg(test)]
211mod test {
212 use crate::vtxo::test::VTXO_VECTORS;
213
214 #[test]
215 pub fn validate_vtxos() {
216 let vtxos = &*VTXO_VECTORS;
217
218 let err = vtxos.board_vtxo.validate(&vtxos.anchor_tx).err();
219 assert!(err.is_none(), "err: {err:?}");
220
221 let err = vtxos.arkoor_htlc_out_vtxo.validate(&vtxos.anchor_tx).err();
222 assert!(err.is_none(), "err: {err:?}");
223
224 let err = vtxos.arkoor2_vtxo.validate(&vtxos.anchor_tx).err();
225 assert!(err.is_none(), "err: {err:?}");
226
227 let err = vtxos.round1_vtxo.validate(&vtxos.round_tx).err();
228 assert!(err.is_none(), "err: {err:?}");
229
230 let err = vtxos.round2_vtxo.validate(&vtxos.round_tx).err();
231 assert!(err.is_none(), "err: {err:?}");
232
233 let err = vtxos.arkoor3_vtxo.validate(&vtxos.round_tx).err();
234 assert!(err.is_none(), "err: {err:?}");
235 }
236}