1use std::convert::Infallible;
2use std::hash::Hash;
3
4use bitcoin::secp256k1::{PublicKey, Secp256k1, Signing, Verification};
5use bitcoin::{Amount, BlockHash, OutPoint, Transaction};
6use fedimint_core::encoding::{Decodable, DecodeError, Encodable};
7use fedimint_core::module::registry::ModuleDecoderRegistry;
8use fedimint_core::txoproof::TxOutProof;
9use miniscript::{Descriptor, TranslatePk, translate_hash_fail};
10use serde::de::Error;
11use serde::{Deserialize, Deserializer, Serialize};
12use thiserror::Error;
13
14use crate::keys::CompressedPublicKey;
15use crate::tweakable::{Contract, Tweakable};
16
17#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash, Encodable)]
20pub struct PegInProof {
21 txout_proof: TxOutProof,
22 transaction: Transaction,
26 output_idx: u32,
28 tweak_contract_key: PublicKey,
29}
30
31impl<'de> Deserialize<'de> for PegInProof {
32 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
33 where
34 D: Deserializer<'de>,
35 {
36 #[derive(Deserialize)]
37 struct PegInProofInner {
38 txout_proof: TxOutProof,
39 transaction: Transaction,
40 output_idx: u32,
41 tweak_contract_key: PublicKey,
42 }
43
44 let pegin_proof_inner = PegInProofInner::deserialize(deserializer)?;
45
46 let pegin_proof = PegInProof {
47 txout_proof: pegin_proof_inner.txout_proof,
48 transaction: pegin_proof_inner.transaction,
49 output_idx: pegin_proof_inner.output_idx,
50 tweak_contract_key: pegin_proof_inner.tweak_contract_key,
51 };
52
53 validate_peg_in_proof(&pegin_proof).map_err(D::Error::custom)?;
54
55 Ok(pegin_proof)
56 }
57}
58
59impl PegInProof {
60 pub fn new(
61 txout_proof: TxOutProof,
62 transaction: Transaction,
63 output_idx: u32,
64 tweak_contract_key: PublicKey,
65 ) -> Result<PegInProof, PegInProofError> {
66 if !txout_proof.contains_tx(transaction.compute_txid()) {
68 return Err(PegInProofError::TransactionNotInProof);
69 }
70
71 if transaction.output.len() > u32::MAX as usize {
72 return Err(PegInProofError::TooManyTransactionOutputs);
73 }
74
75 if transaction.output.get(output_idx as usize).is_none() {
76 return Err(PegInProofError::OutputIndexOutOfRange(
77 u64::from(output_idx),
78 transaction.output.len() as u64,
79 ));
80 }
81
82 Ok(PegInProof {
83 txout_proof,
84 transaction,
85 output_idx,
86 tweak_contract_key,
87 })
88 }
89
90 pub fn verify<C: Verification + Signing>(
91 &self,
92 secp: &Secp256k1<C>,
93 untweaked_pegin_descriptor: &Descriptor<CompressedPublicKey>,
94 ) -> Result<(), PegInProofError> {
95 let script = untweaked_pegin_descriptor
96 .tweak(&self.tweak_contract_key, secp)
97 .script_pubkey();
98
99 let txo = self
100 .transaction
101 .output
102 .get(self.output_idx as usize)
103 .expect("output_idx in-rangeness is an invariant guaranteed by constructors");
104
105 if txo.script_pubkey != script {
106 return Err(PegInProofError::ScriptDoesNotMatch);
107 }
108
109 Ok(())
110 }
111
112 pub fn proof_block(&self) -> BlockHash {
113 self.txout_proof.block()
114 }
115
116 pub fn tweak_key(&self) -> PublicKey {
117 self.tweak_contract_key
118 }
119
120 pub fn identity(&self) -> (PublicKey, bitcoin::Txid) {
121 (self.tweak_contract_key, self.transaction.compute_txid())
122 }
123
124 pub fn tx_output(&self) -> bitcoin::TxOut {
125 self.transaction
126 .output
127 .get(self.output_idx as usize)
128 .expect("output_idx in-rangeness is an invariant guaranteed by constructors")
129 .clone()
130 }
131
132 pub fn outpoint(&self) -> bitcoin::OutPoint {
133 OutPoint {
134 txid: self.transaction.compute_txid(),
135 vout: self.output_idx,
136 }
137 }
138}
139
140impl Tweakable for Descriptor<CompressedPublicKey> {
141 fn tweak<Ctx: Verification + Signing, Ctr: Contract>(
142 &self,
143 tweak: &Ctr,
144 secp: &Secp256k1<Ctx>,
145 ) -> Self {
146 struct CompressedPublicKeyTranslator<'t, 's, Ctx: Verification, Ctr: Contract> {
147 tweak: &'t Ctr,
148 secp: &'s Secp256k1<Ctx>,
149 }
150
151 impl<Ctx: Verification + Signing, Ctr: Contract>
152 miniscript::Translator<CompressedPublicKey, CompressedPublicKey, Infallible>
153 for CompressedPublicKeyTranslator<'_, '_, Ctx, Ctr>
154 {
155 fn pk(&mut self, pk: &CompressedPublicKey) -> Result<CompressedPublicKey, Infallible> {
156 Ok(CompressedPublicKey::new(
157 pk.key.tweak(self.tweak, self.secp),
158 ))
159 }
160
161 translate_hash_fail!(
162 CompressedPublicKey,
163 miniscript::bitcoin::PublicKey,
164 Infallible
165 );
166 }
167 self.translate_pk(&mut CompressedPublicKeyTranslator { tweak, secp })
168 .expect("can't fail")
169 }
170}
171
172fn validate_peg_in_proof(proof: &PegInProof) -> Result<(), PegInProofValidationError> {
173 if !proof
174 .txout_proof
175 .contains_tx(proof.transaction.compute_txid())
176 {
177 return Err(PegInProofValidationError::TransactionNotInProof);
178 }
179
180 if proof.transaction.output.len() > u32::MAX as usize {
181 return Err(PegInProofValidationError::TooManyOutputs);
182 }
183
184 match proof.transaction.output.get(proof.output_idx as usize) {
185 Some(txo) => {
186 if txo.value > Amount::MAX_MONEY {
187 return Err(PegInProofValidationError::AmountOutOfRange);
188 }
189 }
190 None => {
191 return Err(PegInProofValidationError::OutputIndexOutOfRange);
192 }
193 }
194
195 Ok(())
196}
197
198#[derive(Debug, Error)]
200enum PegInProofValidationError {
201 #[error("Supplied transaction is not included in proof")]
203 TransactionNotInProof,
204
205 #[error("Supplied transaction has too many outputs")]
207 TooManyOutputs,
208
209 #[error("Txout amount out of range")]
211 AmountOutOfRange,
212
213 #[error("Output index out of range")]
215 OutputIndexOutOfRange,
216}
217
218impl Decodable for PegInProof {
219 fn consensus_decode_partial<D: std::io::Read>(
220 d: &mut D,
221 modules: &ModuleDecoderRegistry,
222 ) -> Result<Self, DecodeError> {
223 let slf = PegInProof {
224 txout_proof: TxOutProof::consensus_decode_partial(d, modules)?,
225 transaction: Transaction::consensus_decode_partial(d, modules)?,
226 output_idx: u32::consensus_decode_partial(d, modules)?,
227 tweak_contract_key: PublicKey::consensus_decode_partial(d, modules)?,
228 };
229
230 validate_peg_in_proof(&slf).map_err(|error| DecodeError::Invalid(error.into()))?;
231 Ok(slf)
232 }
233}
234
235#[derive(Debug, Error, Encodable, Decodable, Hash, Clone, Eq, PartialEq)]
236pub enum PegInProofError {
237 #[error("Supplied transaction is not included in proof")]
238 TransactionNotInProof,
239 #[error("Supplied transaction has too many outputs")]
240 TooManyTransactionOutputs,
241 #[error("The output with index {0} referred to does not exist (tx has {1} outputs)")]
242 OutputIndexOutOfRange(u64, u64),
243 #[error("The expected script given the tweak did not match the actual script")]
244 ScriptDoesNotMatch,
245}
246
247#[cfg(test)]
248mod tests;