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