fedimint_wallet_common/
txoproof.rs

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/// A proof about a script owning a certain output. Verifiable using headers
19/// only.
20#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash, Encodable)]
21pub struct PegInProof {
22    txout_proof: TxOutProof,
23    // check that outputs are not more than u32::max (probably enforced if inclusion proof is
24    // checked first) and that the referenced output has a value that won't overflow when converted
25    // to msat
26    transaction: Transaction,
27    // Check that the idx is in range
28    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        // TODO: remove redundancy with serde validation
68        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}