Skip to main content

sapling_crypto/pczt/
tx_extractor.rs

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