sapling_crypto/pczt/
tx_extractor.rs

1use rand::{CryptoRng, RngCore};
2
3use crate::{
4    bundle::{
5        Authorization, Authorized, EffectsOnly, GrothProofBytes, OutputDescription,
6        SpendDescription,
7    },
8    Bundle,
9};
10
11use super::{Output, Spend};
12
13impl super::Bundle {
14    /// Extracts the effects of this PCZT bundle as a [regular `Bundle`].
15    ///
16    /// This is used by the Signer role to produce the transaction sighash.
17    ///
18    /// [regular `Bundle`]: crate::Bundle
19    pub fn extract_effects<V: TryFrom<i64>>(
20        &self,
21    ) -> Result<Option<crate::Bundle<EffectsOnly, V>>, TxExtractorError> {
22        self.to_tx_data(|_| Ok(()), |_| Ok(()), |_| Ok(()), |_| Ok(EffectsOnly))
23    }
24
25    /// Extracts a fully authorized [regular `Bundle`] from this PCZT bundle.
26    ///
27    /// This is used by the Transaction Extractor role to produce the final transaction.
28    ///
29    /// [regular `Bundle`]: crate::Bundle
30    pub fn extract<V: TryFrom<i64>>(
31        self,
32    ) -> Result<Option<crate::Bundle<Unbound, V>>, TxExtractorError> {
33        self.to_tx_data(
34            |spend| spend.zkproof.ok_or(TxExtractorError::MissingProof),
35            |spend| {
36                spend
37                    .spend_auth_sig
38                    .ok_or(TxExtractorError::MissingSpendAuthSig)
39            },
40            |output| output.zkproof.ok_or(TxExtractorError::MissingProof),
41            |bundle| {
42                Ok(Unbound {
43                    bsk: bundle
44                        .bsk
45                        .ok_or(TxExtractorError::MissingBindingSignatureSigningKey)?,
46                })
47            },
48        )
49    }
50
51    fn to_tx_data<A, V, E, F, G, H, I>(
52        &self,
53        spend_proof: F,
54        spend_auth: G,
55        output_proof: H,
56        bundle_auth: I,
57    ) -> Result<Option<crate::Bundle<A, V>>, E>
58    where
59        A: Authorization,
60        E: From<TxExtractorError>,
61        F: Fn(&Spend) -> Result<<A as Authorization>::SpendProof, E>,
62        G: Fn(&Spend) -> Result<<A as Authorization>::AuthSig, E>,
63        H: Fn(&Output) -> Result<<A as Authorization>::OutputProof, E>,
64        I: FnOnce(&Self) -> Result<A, E>,
65        V: TryFrom<i64>,
66    {
67        let spends = self
68            .spends
69            .iter()
70            .map(|spend| {
71                Ok(SpendDescription::from_parts(
72                    spend.cv.clone(),
73                    self.anchor.inner(),
74                    spend.nullifier,
75                    spend.rk,
76                    spend_proof(spend)?,
77                    spend_auth(spend)?,
78                ))
79            })
80            .collect::<Result<_, E>>()?;
81
82        let outputs = self
83            .outputs
84            .iter()
85            .map(|output| {
86                Ok(OutputDescription::from_parts(
87                    output.cv.clone(),
88                    output.cmu,
89                    output.ephemeral_key.clone(),
90                    output.enc_ciphertext,
91                    output.out_ciphertext,
92                    output_proof(output)?,
93                ))
94            })
95            .collect::<Result<_, E>>()?;
96
97        let value_balance = i64::try_from(self.value_sum)
98            .ok()
99            .and_then(|v| v.try_into().ok())
100            .ok_or(TxExtractorError::ValueSumOutOfRange)?;
101
102        let authorization = bundle_auth(self)?;
103
104        Ok(Bundle::from_parts(
105            spends,
106            outputs,
107            value_balance,
108            authorization,
109        ))
110    }
111}
112
113/// Errors that can occur while extracting a regular Sapling bundle from a PCZT bundle.
114#[derive(Debug)]
115pub enum TxExtractorError {
116    /// The Transaction Extractor role requires `bsk` to be set.
117    MissingBindingSignatureSigningKey,
118    /// The Transaction Extractor role requires all `zkproof` fields to be set.
119    MissingProof,
120    /// The Transaction Extractor role requires all `spend_auth_sig` fields to be set.
121    MissingSpendAuthSig,
122    /// The value sum does not fit into a `valueBalance`.
123    ValueSumOutOfRange,
124}
125
126/// Authorizing data for a bundle of actions that is just missing a binding signature.
127#[derive(Debug)]
128pub struct Unbound {
129    bsk: redjubjub::SigningKey<redjubjub::Binding>,
130}
131
132impl Authorization for Unbound {
133    type SpendProof = GrothProofBytes;
134    type OutputProof = GrothProofBytes;
135    type AuthSig = redjubjub::Signature<redjubjub::SpendAuth>;
136}
137
138impl<V> crate::Bundle<Unbound, V> {
139    /// Verifies the given sighash with every `spend_auth_sig`, and then binds the bundle.
140    ///
141    /// Returns `None` if the given sighash does not validate against every `spend_auth_sig`.
142    pub fn apply_binding_signature<R: RngCore + CryptoRng>(
143        self,
144        sighash: [u8; 32],
145        rng: R,
146    ) -> Option<crate::Bundle<Authorized, V>> {
147        if self
148            .shielded_spends()
149            .iter()
150            .all(|spend| spend.rk().verify(&sighash, spend.spend_auth_sig()).is_ok())
151        {
152            Some(self.map_authorization(
153                &mut (),
154                |_, p| p,
155                |_, p| p,
156                |_, s| s,
157                |_, Unbound { bsk }| Authorized {
158                    binding_sig: bsk.sign(rng, &sighash),
159                },
160            ))
161        } else {
162            None
163        }
164    }
165}