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