zcash_transparent/
builder.rs

1//! Types and functions for building transparent transaction components.
2
3use alloc::collections::BTreeMap;
4use alloc::vec::Vec;
5use core::fmt;
6
7use zcash_protocol::value::{BalanceError, ZatBalance, Zatoshis};
8
9use crate::{
10    address::{Script, TransparentAddress},
11    bundle::{Authorization, Authorized, Bundle, TxIn, TxOut},
12    pczt,
13    sighash::{SignableInput, TransparentAuthorizingContext},
14};
15
16#[cfg(feature = "transparent-inputs")]
17use {
18    crate::{
19        bundle::OutPoint,
20        sighash::{SighashType, SIGHASH_ALL},
21    },
22    sha2::Digest,
23};
24
25#[derive(Debug, PartialEq, Eq)]
26pub enum Error {
27    InvalidAddress,
28    InvalidAmount,
29    /// A bundle could not be built because a required signing keys was missing.
30    MissingSigningKey,
31}
32
33impl fmt::Display for Error {
34    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
35        match self {
36            Error::InvalidAddress => write!(f, "Invalid address"),
37            Error::InvalidAmount => write!(f, "Invalid amount"),
38            Error::MissingSigningKey => write!(f, "Missing signing key"),
39        }
40    }
41}
42
43/// A set of transparent signing keys.
44///
45/// When the `transparent-inputs` feature flag is enabled, transparent signing keys can be
46/// stored in this set and used to authorize transactions with transparent inputs.
47pub struct TransparentSigningSet {
48    #[cfg(feature = "transparent-inputs")]
49    secp: secp256k1::Secp256k1<secp256k1::SignOnly>,
50    #[cfg(feature = "transparent-inputs")]
51    keys: Vec<(secp256k1::SecretKey, secp256k1::PublicKey)>,
52}
53
54impl Default for TransparentSigningSet {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl TransparentSigningSet {
61    /// Constructs an empty set of signing keys.
62    pub fn new() -> Self {
63        Self {
64            #[cfg(feature = "transparent-inputs")]
65            secp: secp256k1::Secp256k1::gen_new(),
66            #[cfg(feature = "transparent-inputs")]
67            keys: vec![],
68        }
69    }
70
71    /// Adds a signing key to the set.
72    ///
73    /// Returns the corresponding pubkey.
74    #[cfg(feature = "transparent-inputs")]
75    pub fn add_key(&mut self, sk: secp256k1::SecretKey) -> secp256k1::PublicKey {
76        let pubkey = secp256k1::PublicKey::from_secret_key(&self.secp, &sk);
77        // Cache the pubkey for ease of matching later.
78        self.keys.push((sk, pubkey));
79        pubkey
80    }
81}
82
83// TODO: This feature gate can be removed.
84#[cfg(feature = "transparent-inputs")]
85#[derive(Debug, Clone)]
86pub struct TransparentInputInfo {
87    pubkey: secp256k1::PublicKey,
88    utxo: OutPoint,
89    coin: TxOut,
90}
91
92#[cfg(feature = "transparent-inputs")]
93impl TransparentInputInfo {
94    pub fn outpoint(&self) -> &OutPoint {
95        &self.utxo
96    }
97
98    pub fn coin(&self) -> &TxOut {
99        &self.coin
100    }
101}
102
103pub struct TransparentBuilder {
104    #[cfg(feature = "transparent-inputs")]
105    inputs: Vec<TransparentInputInfo>,
106    vout: Vec<TxOut>,
107}
108
109#[derive(Debug, Clone)]
110pub struct Unauthorized {
111    #[cfg(feature = "transparent-inputs")]
112    inputs: Vec<TransparentInputInfo>,
113}
114
115impl Authorization for Unauthorized {
116    type ScriptSig = ();
117}
118
119impl TransparentBuilder {
120    /// Constructs a new TransparentBuilder
121    pub fn empty() -> Self {
122        TransparentBuilder {
123            #[cfg(feature = "transparent-inputs")]
124            inputs: vec![],
125            vout: vec![],
126        }
127    }
128
129    /// Returns the list of transparent inputs that will be consumed by the transaction being
130    /// constructed.
131    #[cfg(feature = "transparent-inputs")]
132    pub fn inputs(&self) -> &[TransparentInputInfo] {
133        &self.inputs
134    }
135
136    /// Returns the transparent outputs that will be produced by the transaction being constructed.
137    pub fn outputs(&self) -> &[TxOut] {
138        &self.vout
139    }
140
141    /// Adds a coin (the output of a previous transaction) to be spent to the transaction.
142    #[cfg(feature = "transparent-inputs")]
143    pub fn add_input(
144        &mut self,
145        pubkey: secp256k1::PublicKey,
146        utxo: OutPoint,
147        coin: TxOut,
148    ) -> Result<(), Error> {
149        // Ensure that the RIPEMD-160 digest of the public key associated with the
150        // provided secret key matches that of the address to which the provided
151        // output may be spent.
152        match coin.script_pubkey.address() {
153            Some(TransparentAddress::PublicKeyHash(hash)) => {
154                use ripemd::Ripemd160;
155                use sha2::Sha256;
156
157                if hash[..] != Ripemd160::digest(Sha256::digest(pubkey.serialize()))[..] {
158                    return Err(Error::InvalidAddress);
159                }
160            }
161            _ => return Err(Error::InvalidAddress),
162        }
163
164        self.inputs
165            .push(TransparentInputInfo { pubkey, utxo, coin });
166
167        Ok(())
168    }
169
170    pub fn add_output(&mut self, to: &TransparentAddress, value: Zatoshis) -> Result<(), Error> {
171        self.vout.push(TxOut {
172            value,
173            script_pubkey: to.script(),
174        });
175
176        Ok(())
177    }
178
179    pub fn value_balance(&self) -> Result<ZatBalance, BalanceError> {
180        #[cfg(feature = "transparent-inputs")]
181        let input_sum = self
182            .inputs
183            .iter()
184            .map(|input| input.coin.value)
185            .sum::<Option<Zatoshis>>()
186            .ok_or(BalanceError::Overflow)?;
187
188        #[cfg(not(feature = "transparent-inputs"))]
189        let input_sum = Zatoshis::ZERO;
190
191        let output_sum = self
192            .vout
193            .iter()
194            .map(|vo| vo.value)
195            .sum::<Option<Zatoshis>>()
196            .ok_or(BalanceError::Overflow)?;
197
198        (ZatBalance::from(input_sum) - ZatBalance::from(output_sum)).ok_or(BalanceError::Underflow)
199    }
200
201    pub fn build(self) -> Option<Bundle<Unauthorized>> {
202        #[cfg(feature = "transparent-inputs")]
203        let vin: Vec<TxIn<Unauthorized>> = self
204            .inputs
205            .iter()
206            .map(|i| TxIn::new(i.utxo.clone()))
207            .collect();
208
209        #[cfg(not(feature = "transparent-inputs"))]
210        let vin: Vec<TxIn<Unauthorized>> = vec![];
211
212        if vin.is_empty() && self.vout.is_empty() {
213            None
214        } else {
215            Some(Bundle {
216                vin,
217                vout: self.vout,
218                authorization: Unauthorized {
219                    #[cfg(feature = "transparent-inputs")]
220                    inputs: self.inputs,
221                },
222            })
223        }
224    }
225
226    /// Builds a bundle containing the given inputs and outputs, for inclusion in a PCZT.
227    pub fn build_for_pczt(self) -> Option<pczt::Bundle> {
228        #[cfg(feature = "transparent-inputs")]
229        let inputs = self
230            .inputs
231            .iter()
232            .map(|i| pczt::Input {
233                prevout_txid: i.utxo.hash,
234                prevout_index: i.utxo.n,
235                sequence: None,
236                required_time_lock_time: None,
237                required_height_lock_time: None,
238                script_sig: None,
239                value: i.coin.value,
240                script_pubkey: i.coin.script_pubkey.clone(),
241                // We don't currently support spending P2SH coins.
242                redeem_script: None,
243                partial_signatures: BTreeMap::new(),
244                sighash_type: SighashType::ALL,
245                bip32_derivation: BTreeMap::new(),
246                ripemd160_preimages: BTreeMap::new(),
247                sha256_preimages: BTreeMap::new(),
248                hash160_preimages: BTreeMap::new(),
249                hash256_preimages: BTreeMap::new(),
250                proprietary: BTreeMap::new(),
251            })
252            .collect::<Vec<_>>();
253
254        #[cfg(not(feature = "transparent-inputs"))]
255        let inputs = vec![];
256
257        if inputs.is_empty() && self.vout.is_empty() {
258            None
259        } else {
260            let outputs = self
261                .vout
262                .into_iter()
263                .map(|o| pczt::Output {
264                    value: o.value,
265                    script_pubkey: o.script_pubkey,
266                    // We don't currently support spending P2SH coins, so we only ever see
267                    // external P2SH recipients here, for which we never know the redeem
268                    // script.
269                    redeem_script: None,
270                    bip32_derivation: BTreeMap::new(),
271                    user_address: None,
272                    proprietary: BTreeMap::new(),
273                })
274                .collect();
275
276            Some(pczt::Bundle { inputs, outputs })
277        }
278    }
279}
280
281impl TxIn<Unauthorized> {
282    #[cfg(feature = "transparent-inputs")]
283    pub fn new(prevout: OutPoint) -> Self {
284        TxIn {
285            prevout,
286            script_sig: (),
287            sequence: u32::MAX,
288        }
289    }
290}
291
292#[cfg(not(feature = "transparent-inputs"))]
293impl TransparentAuthorizingContext for Unauthorized {
294    fn input_amounts(&self) -> Vec<Zatoshis> {
295        vec![]
296    }
297
298    fn input_scriptpubkeys(&self) -> Vec<Script> {
299        vec![]
300    }
301}
302
303#[cfg(feature = "transparent-inputs")]
304impl TransparentAuthorizingContext for Unauthorized {
305    fn input_amounts(&self) -> Vec<Zatoshis> {
306        self.inputs.iter().map(|txin| txin.coin.value).collect()
307    }
308
309    fn input_scriptpubkeys(&self) -> Vec<Script> {
310        self.inputs
311            .iter()
312            .map(|txin| txin.coin.script_pubkey.clone())
313            .collect()
314    }
315}
316
317impl Bundle<Unauthorized> {
318    #[cfg_attr(not(feature = "transparent-inputs"), allow(unused_variables))]
319    pub fn apply_signatures<F>(
320        self,
321        calculate_sighash: F,
322        signing_set: &TransparentSigningSet,
323    ) -> Result<Bundle<Authorized>, Error>
324    where
325        F: Fn(SignableInput) -> [u8; 32],
326    {
327        #[cfg(feature = "transparent-inputs")]
328        let script_sigs = self
329            .authorization
330            .inputs
331            .iter()
332            .enumerate()
333            .map(|(index, info)| {
334                // Find the matching signing key.
335                let (sk, _) = signing_set
336                    .keys
337                    .iter()
338                    .find(|(_, pubkey)| pubkey == &info.pubkey)
339                    .ok_or(Error::MissingSigningKey)?;
340
341                let sighash = calculate_sighash(SignableInput {
342                    hash_type: SighashType::ALL,
343                    index,
344                    script_code: &info.coin.script_pubkey, // for p2pkh, always the same as script_pubkey
345                    script_pubkey: &info.coin.script_pubkey,
346                    value: info.coin.value,
347                });
348
349                let msg =
350                    secp256k1::Message::from_digest_slice(sighash.as_ref()).expect("32 bytes");
351                let sig = signing_set.secp.sign_ecdsa(&msg, sk);
352
353                // Signature has to have "SIGHASH_ALL" appended to it
354                let mut sig_bytes: Vec<u8> = sig.serialize_der()[..].to_vec();
355                sig_bytes.extend([SIGHASH_ALL]);
356
357                // P2PKH scriptSig
358                Ok(Script::default() << &sig_bytes[..] << &info.pubkey.serialize()[..])
359            });
360
361        #[cfg(not(feature = "transparent-inputs"))]
362        let script_sigs = core::iter::empty::<Result<Script, Error>>();
363
364        Ok(Bundle {
365            vin: self
366                .vin
367                .iter()
368                .zip(script_sigs)
369                .map(|(txin, sig)| {
370                    Ok(TxIn {
371                        prevout: txin.prevout.clone(),
372                        script_sig: sig?,
373                        sequence: txin.sequence,
374                    })
375                })
376                .collect::<Result<_, _>>()?,
377            vout: self.vout,
378            authorization: Authorized,
379        })
380    }
381}