Skip to main content

fedimint_wallet_common/
txoproof.rs

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/// A proof about a script owning a certain output. Verifiable using headers
18/// only.
19#[derive(Clone, Debug, PartialEq, Serialize, Eq, Hash, Encodable)]
20pub struct PegInProof {
21    txout_proof: TxOutProof,
22    // check that outputs are not more than u32::max (probably enforced if inclusion proof is
23    // checked first) and that the referenced output has a value that won't overflow when converted
24    // to msat
25    transaction: Transaction,
26    // Check that the idx is in range
27    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        // TODO: remove redundancy with serde validation
67        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/// Why a decoded [`PegInProof`] is not valid.
199#[derive(Debug, Error)]
200enum PegInProofValidationError {
201    /// The proof does not contain the transaction.
202    #[error("Supplied transaction is not included in proof")]
203    TransactionNotInProof,
204
205    /// The transaction has more outputs than an output index can address.
206    #[error("Supplied transaction has too many outputs")]
207    TooManyOutputs,
208
209    /// The proven output's amount is more than all bitcoin there can be.
210    #[error("Txout amount out of range")]
211    AmountOutOfRange,
212
213    /// The transaction has no output at the proof's output index.
214    #[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;