1
2use std::borrow::Cow;
3
4use bitcoin::{sighash, Amount, OutPoint, Transaction, TxOut};
5use bitcoin::secp256k1::PublicKey;
6use bitcoin_ext::TxOutExt;
7
8use crate::SECP;
9use crate::vtxo::{Vtxo, GenesisTransition};
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 a common pubkey (idx={genesis_item_idx}): \
22 cosign_pubkey={cosign_pubkey}")]
23 InconsistentCosignPubkeys {
24 cosign_pubkey: PublicKey,
27 genesis_item_idx: usize,
29 },
30 #[error("error verifying one of the genesis transitions (idx={genesis_item_idx}): {error}")]
31 GenesisTransition {
32 error: &'static str,
33 genesis_item_idx: usize,
34 },
35 #[error("non-standard output on genesis item #{genesis_item_idx} other \
36 output #{other_output_idx}")]
37 NonStandardTxOut {
38 genesis_item_idx: usize,
39 other_output_idx: usize,
40 },
41}
42
43impl VtxoValidationError {
44 fn transition(genesis_item_idx: usize, error: &'static str) -> Self {
46 VtxoValidationError::GenesisTransition { error, genesis_item_idx }
47 }
48}
49
50pub enum Validation {
51 Trusted {
52 cosign_pubkey: PublicKey,
53 },
54 Arkoor,
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)
77 });
78
79 let prevout = OutPoint::new(prev_tx.compute_txid(), prev_vout as u32);
80 let tx = item.tx(prevout, next_output);
81
82 let sighash = {
83 let mut shc = sighash::SighashCache::new(&tx);
84 shc.taproot_key_spend_signature_hash(
85 0, &sighash::Prevouts::All(&[prev_txout]), sighash::TapSighashType::Default,
86 ).expect("correct prevouts")
87 };
88
89 let pubkey = {
90 let transition_taproot = item.transition.input_taproot(
91 vtxo.server_pubkey(), vtxo.expiry_height(), vtxo.exit_delta(),
92 );
93 transition_taproot.output_key().to_x_only_public_key()
94 };
95
96 let signature = match item.transition {
97 GenesisTransition::Cosigned { signature, .. } => signature,
98 GenesisTransition::Arkoor { signature: Some(signature), .. } => signature,
99 GenesisTransition::Arkoor { signature: None, .. } => {
100 return Err("missing arkoor signature");
101 },
102 };
103
104 SECP.verify_schnorr(&signature, &sighash.into(), &pubkey)
105 .map_err(|_| "invalid signature")?;
106
107 Ok(tx)
108}
109
110#[inline]
111fn check_transitions_cosigned_then_arkoor<'a>(
112 transitions: impl Iterator<Item = &'a GenesisTransition> + Clone,
113) -> Result<(), VtxoValidationError> {
114 let cosigned = transitions.clone()
115 .take_while(|t| matches!(t, GenesisTransition::Cosigned { .. }));
116 if cosigned.count() < 1 {
117 return Err(VtxoValidationError::Invalid("should start with Cosigned genesis items"));
118 }
119 let mut after_cosigned = transitions.clone()
120 .skip_while(|t| matches!(t, GenesisTransition::Cosigned { .. }));
121 if !after_cosigned.all(|t| matches!(t, GenesisTransition::Arkoor { .. })) {
122 return Err(VtxoValidationError::Invalid(
123 "can only have Arkoor transitions after last Cosigned",
124 ));
125 }
126 Ok(())
127}
128
129#[inline]
132fn determine_cosign_pubkey<'a>(
133 transitions: impl Iterator<Item = &'a GenesisTransition> + DoubleEndedIterator,
134) -> Result<PublicKey, VtxoValidationError> {
135 let last_cosign_pubkeys = transitions.rev().find_map(|t| match t {
138 GenesisTransition::Cosigned { pubkeys, .. } => Some(pubkeys),
139 GenesisTransition::Arkoor { .. } => None,
140 }).unwrap();
141 if last_cosign_pubkeys.len() != 2 {
142 return Err(VtxoValidationError::Invalid("invalid last cosign genesis"));
143 }
144 Ok(last_cosign_pubkeys.first().copied().expect("have more than one transition"))
145}
146
147pub fn validate(
150 vtxo: &Vtxo,
151 chain_anchor_tx: &Transaction,
152) -> Result<Validation, VtxoValidationError> {
153 let anchor_txout = chain_anchor_tx.output.get(vtxo.chain_anchor().vout as usize)
155 .ok_or(VtxoValidationError::Invalid("chain anchor vout out of range"))?;
156 let onchain_amount = vtxo.amount() + vtxo.genesis.iter().map(|i| {
157 i.other_outputs.iter().map(|o| o.value).sum()
158 }).sum();
159 let expected_anchor_txout = vtxo.genesis.get(0).unwrap().transition.input_txout(
160 onchain_amount, vtxo.server_pubkey(), vtxo.expiry_height(), vtxo.exit_delta(),
161 );
162 if *anchor_txout != expected_anchor_txout {
163 return Err(VtxoValidationError::IncorrectChainAnchor { expected: expected_anchor_txout });
164 }
165
166 let transitions = vtxo.genesis.iter().map(|i| &i.transition);
168
169 if vtxo.genesis.is_empty() {
172 return Err(VtxoValidationError::Invalid("no genesis items"));
173 }
174 check_transitions_cosigned_then_arkoor(transitions.clone())?;
175
176 let cosign_pubkey = determine_cosign_pubkey(transitions.clone())?;
177
178 let mut prev = (Cow::Borrowed(chain_anchor_tx), vtxo.chain_anchor().vout as usize, onchain_amount);
179 for (idx, item) in vtxo.genesis.iter().enumerate() {
180 if let GenesisTransition::Cosigned { ref pubkeys, .. } = item.transition {
182 if !pubkeys.contains(&cosign_pubkey) {
183 return Err(VtxoValidationError::InconsistentCosignPubkeys {
184 cosign_pubkey: cosign_pubkey,
185 genesis_item_idx: idx,
186 });
187 }
188 }
189
190 if let Some(out_idx) = item.other_outputs.iter().position(|o| !o.is_standard()) {
192 return Err(VtxoValidationError::NonStandardTxOut {
193 genesis_item_idx: idx,
194 other_output_idx: out_idx,
195 });
196 }
197
198 let next_amount = prev.2.checked_sub(item.other_outputs.iter().map(|o| o.value).sum())
199 .ok_or(VtxoValidationError::Invalid("insufficient onchain amount"))?;
200 let next_tx = verify_transition(&vtxo, idx, prev.0.as_ref(), prev.1, next_amount)
201 .map_err(|e| VtxoValidationError::transition(idx, e))?;
202 prev = (Cow::Owned(next_tx), item.output_idx as usize, next_amount);
203 }
204
205 Ok(if transitions.clone().all(|t| matches!(t, GenesisTransition::Cosigned { .. })) {
206 Validation::Trusted { cosign_pubkey }
207 } else {
208 Validation::Arkoor
209 })
210}
211
212#[cfg(test)]
213mod test {
214 use crate::vtxo::test::VTXO_VECTORS;
215
216 #[test]
217 pub fn validate_vtxos() {
218 let vtxos = &*VTXO_VECTORS;
219
220 let err = vtxos.board_vtxo.validate(&vtxos.anchor_tx).err();
221 assert!(err.is_none(), "err: {err:?}");
222
223 let err = vtxos.arkoor_htlc_out_vtxo.validate(&vtxos.anchor_tx).err();
224 assert!(err.is_none(), "err: {err:?}");
225
226 let err = vtxos.arkoor2_vtxo.validate(&vtxos.anchor_tx).err();
227 assert!(err.is_none(), "err: {err:?}");
228
229 let err = vtxos.round1_vtxo.validate(&vtxos.round_tx).err();
230 assert!(err.is_none(), "err: {err:?}");
231
232 let err = vtxos.round2_vtxo.validate(&vtxos.round_tx).err();
233 assert!(err.is_none(), "err: {err:?}");
234
235 let err = vtxos.arkoor3_vtxo.validate(&vtxos.round_tx).err();
236 assert!(err.is_none(), "err: {err:?}");
237 }
238}