Skip to main content

zebra_chain/primitives/
zcash_primitives.rs

1//! Contains code that interfaces with the zcash_primitives crate from
2//! librustzcash.
3
4use std::{io, ops::Deref, sync::Arc};
5
6use zcash_primitives::transaction::{self as zp_tx, TxDigests};
7use zcash_protocol::value::{BalanceError, ZatBalance, Zatoshis};
8use zcash_script::script;
9
10use crate::{
11    amount::{Amount, NonNegative},
12    parameters::NetworkUpgrade,
13    serialization::ZcashSerialize,
14    transaction::{AuthDigest, HashType, SigHash, Transaction},
15    transparent::{self, Script},
16    Error,
17};
18
19// TODO: move copied and modified code to a separate module.
20//
21// Used by boilerplate code below.
22
23#[derive(Clone, Debug)]
24struct TransparentAuth {
25    all_prev_outputs: Arc<Vec<transparent::Output>>,
26}
27
28impl zcash_transparent::bundle::Authorization for TransparentAuth {
29    type ScriptSig = zcash_transparent::address::Script;
30}
31
32// In this block we convert our Output to a librustzcash to TxOut.
33// (We could do the serialize/deserialize route but it's simple enough to convert manually)
34impl zcash_transparent::sighash::TransparentAuthorizingContext for TransparentAuth {
35    fn input_amounts(&self) -> Vec<Zatoshis> {
36        self.all_prev_outputs
37            .iter()
38            .map(|prevout| {
39                prevout
40                    .value
41                    .try_into()
42                    .expect("will not fail since it was previously validated")
43            })
44            .collect()
45    }
46
47    fn input_scriptpubkeys(&self) -> Vec<zcash_transparent::address::Script> {
48        self.all_prev_outputs
49            .iter()
50            .map(|prevout| {
51                zcash_transparent::address::Script(script::Code(
52                    prevout.lock_script.as_raw_bytes().into(),
53                ))
54            })
55            .collect()
56    }
57}
58
59// Boilerplate mostly copied from `zcash/src/rust/src/transaction_ffi.rs` which is required
60// to compute sighash.
61// TODO: remove/change if they improve the API to not require this.
62
63struct MapTransparent {
64    auth: TransparentAuth,
65}
66
67impl zcash_transparent::bundle::MapAuth<zcash_transparent::bundle::Authorized, TransparentAuth>
68    for MapTransparent
69{
70    fn map_script_sig(
71        &self,
72        s: <zcash_transparent::bundle::Authorized as zcash_transparent::bundle::Authorization>::ScriptSig,
73    ) -> <TransparentAuth as zcash_transparent::bundle::Authorization>::ScriptSig {
74        s
75    }
76
77    fn map_authorization(&self, _: zcash_transparent::bundle::Authorized) -> TransparentAuth {
78        // TODO: This map should consume self, so we can move self.auth
79        self.auth.clone()
80    }
81}
82
83struct IdentityMap;
84
85impl
86    zp_tx::components::sapling::MapAuth<
87        sapling_crypto::bundle::Authorized,
88        sapling_crypto::bundle::Authorized,
89    > for IdentityMap
90{
91    fn map_spend_proof(
92        &mut self,
93        p: <sapling_crypto::bundle::Authorized as sapling_crypto::bundle::Authorization>::SpendProof,
94    ) -> <sapling_crypto::bundle::Authorized as sapling_crypto::bundle::Authorization>::SpendProof
95    {
96        p
97    }
98
99    fn map_output_proof(
100        &mut self,
101        p: <sapling_crypto::bundle::Authorized as sapling_crypto::bundle::Authorization>::OutputProof,
102    ) -> <sapling_crypto::bundle::Authorized as sapling_crypto::bundle::Authorization>::OutputProof
103    {
104        p
105    }
106
107    fn map_auth_sig(
108        &mut self,
109        s: <sapling_crypto::bundle::Authorized as sapling_crypto::bundle::Authorization>::AuthSig,
110    ) -> <sapling_crypto::bundle::Authorized as sapling_crypto::bundle::Authorization>::AuthSig
111    {
112        s
113    }
114
115    fn map_authorization(
116        &mut self,
117        a: sapling_crypto::bundle::Authorized,
118    ) -> sapling_crypto::bundle::Authorized {
119        a
120    }
121}
122
123impl zp_tx::components::orchard::MapAuth<orchard::bundle::Authorized, orchard::bundle::Authorized>
124    for IdentityMap
125{
126    fn map_spend_auth(
127        &self,
128        s: <orchard::bundle::Authorized as orchard::bundle::Authorization>::SpendAuth,
129    ) -> <orchard::bundle::Authorized as orchard::bundle::Authorization>::SpendAuth {
130        s
131    }
132
133    fn map_authorization(&self, a: orchard::bundle::Authorized) -> orchard::bundle::Authorized {
134        a
135    }
136}
137
138#[derive(Debug)]
139struct PrecomputedAuth {}
140
141impl zp_tx::Authorization for PrecomputedAuth {
142    type TransparentAuth = TransparentAuth;
143    type SaplingAuth = sapling_crypto::bundle::Authorized;
144    type OrchardAuth = orchard::bundle::Authorized;
145
146    #[cfg(zcash_unstable = "zfuture")]
147    type TzeAuth = zp_tx::components::tze::Authorized;
148}
149
150// End of (mostly) copied code
151
152/// Convert a Zebra transparent::Output into a librustzcash one.
153impl TryFrom<&transparent::Output> for zcash_transparent::bundle::TxOut {
154    type Error = io::Error;
155
156    #[allow(clippy::unwrap_in_result)]
157    fn try_from(output: &transparent::Output) -> Result<Self, Self::Error> {
158        let serialized_output_bytes = output
159            .zcash_serialize_to_vec()
160            .expect("zcash_primitives and Zebra transparent output formats must be compatible");
161
162        zcash_transparent::bundle::TxOut::read(&mut serialized_output_bytes.as_slice())
163    }
164}
165
166/// Convert a Zebra transparent::Output into a librustzcash one.
167impl TryFrom<transparent::Output> for zcash_transparent::bundle::TxOut {
168    type Error = io::Error;
169
170    // The borrow is actually needed to use TryFrom<&transparent::Output>
171    #[allow(clippy::needless_borrow)]
172    fn try_from(output: transparent::Output) -> Result<Self, Self::Error> {
173        (&output).try_into()
174    }
175}
176
177/// Convert a Zebra non-negative Amount into a librustzcash one.
178impl TryFrom<Amount<NonNegative>> for zcash_protocol::value::Zatoshis {
179    type Error = BalanceError;
180
181    fn try_from(amount: Amount<NonNegative>) -> Result<Self, Self::Error> {
182        zcash_protocol::value::Zatoshis::from_nonnegative_i64(amount.into())
183    }
184}
185
186impl TryFrom<Amount> for ZatBalance {
187    type Error = BalanceError;
188
189    fn try_from(amount: Amount) -> Result<Self, Self::Error> {
190        ZatBalance::from_i64(amount.into())
191    }
192}
193
194/// Convert a Zebra Script into a librustzcash one.
195impl From<&Script> for zcash_transparent::address::Script {
196    fn from(script: &Script) -> Self {
197        zcash_transparent::address::Script(script::Code(script.as_raw_bytes().to_vec()))
198    }
199}
200
201/// Convert a Zebra Script into a librustzcash one.
202impl From<Script> for zcash_transparent::address::Script {
203    // The borrow is actually needed to use From<&Script>
204    #[allow(clippy::needless_borrow)]
205    fn from(script: Script) -> Self {
206        (&script).into()
207    }
208}
209
210/// Precomputed data used for sighash or txid computation.
211#[derive(Debug)]
212pub(crate) struct PrecomputedTxData {
213    tx_data: zp_tx::TransactionData<PrecomputedAuth>,
214    txid_parts: TxDigests<blake2b_simd::Hash>,
215    all_previous_outputs: Arc<Vec<transparent::Output>>,
216}
217
218impl PrecomputedTxData {
219    /// Computes the data used for sighash or txid computation.
220    ///
221    /// # Inputs
222    ///
223    /// - `tx`: the relevant transaction.
224    /// - `nu`: the network upgrade to which the transaction belongs.
225    /// - `all_previous_outputs`: the transparent Output matching each transparent input in `tx`.
226    ///
227    /// # Errors
228    ///
229    /// - If `tx` can't be converted to its `librustzcash` equivalent.
230    /// - If `nu` doesn't contain a consensus branch id convertible to its `librustzcash`
231    ///   equivalent.
232    ///
233    /// # Consensus
234    ///
235    /// > [NU5 only, pre-NU6] All transactions MUST use the NU5 consensus branch ID `0xF919A198` as
236    /// > defined in [ZIP-252].
237    ///
238    /// > [NU6 only] All transactions MUST use the NU6 consensus branch ID `0xC8E71055` as defined
239    /// > in  [ZIP-253].
240    ///
241    /// # Notes
242    ///
243    /// The check that ensures compliance with the two consensus rules stated above takes place in
244    /// the [`Transaction::to_librustzcash`] method. If the check fails, the tx can't be converted
245    /// to its `librustzcash` equivalent, which leads to an error. The check relies on the passed
246    /// `nu` parameter, which uniquely represents a consensus branch id and can, therefore, be used
247    /// as an equivalent to a consensus branch id. The desired `nu` is set either by the script or
248    /// tx verifier in `zebra-consensus`.
249    ///
250    /// [ZIP-252]: <https://zips.z.cash/zip-0252>
251    /// [ZIP-253]: <https://zips.z.cash/zip-0253>
252    pub(crate) fn new(
253        tx: &Transaction,
254        nu: NetworkUpgrade,
255        all_previous_outputs: Arc<Vec<transparent::Output>>,
256    ) -> Result<PrecomputedTxData, Error> {
257        let tx = tx.to_librustzcash(nu)?;
258
259        let txid_parts = tx.deref().digest(zp_tx::txid::TxIdDigester);
260
261        let f_transparent = MapTransparent {
262            auth: TransparentAuth {
263                all_prev_outputs: all_previous_outputs.clone(),
264            },
265        };
266
267        let tx_data: zp_tx::TransactionData<PrecomputedAuth> = tx.into_data().map_authorization(
268            f_transparent,
269            IdentityMap,
270            IdentityMap,
271            #[cfg(zcash_unstable = "zfuture")]
272            (),
273        );
274
275        Ok(PrecomputedTxData {
276            tx_data,
277            txid_parts,
278            all_previous_outputs,
279        })
280    }
281
282    /// Returns the Orchard bundle in `tx_data`.
283    pub fn orchard_bundle(
284        &self,
285    ) -> Option<orchard::bundle::Bundle<orchard::bundle::Authorized, ZatBalance>> {
286        self.tx_data.orchard_bundle().cloned()
287    }
288
289    /// Returns the Ironwood bundle in `tx_data` (NU6.3 onward).
290    ///
291    /// The Ironwood bundle reuses the Orchard bundle type; it differs only by pool (separate tree,
292    /// nullifier set, and value balance) and is verified under the NU6.3 Action circuit key.
293    pub fn ironwood_bundle(
294        &self,
295    ) -> Option<orchard::bundle::Bundle<orchard::bundle::Authorized, ZatBalance>> {
296        self.tx_data.ironwood_bundle().cloned()
297    }
298
299    /// Returns the Sapling bundle in `tx_data`.
300    pub fn sapling_bundle(
301        &self,
302    ) -> Option<sapling_crypto::Bundle<sapling_crypto::bundle::Authorized, ZatBalance>> {
303        self.tx_data.sapling_bundle().cloned()
304    }
305}
306
307/// Internal error type returned by [`sighash_inner`] when a sighash request
308/// violates one of the documented preconditions of [`sighash`] or
309/// [`sighash_v4_raw`].
310///
311/// Public callers (`SigHasher::sighash`, `SigHasher::sighash_v4_raw`) document
312/// these conditions as panics, so they unwrap the result at the public
313/// boundary. Keeping the internal code `Result`-shaped avoids spreading
314/// `.expect()` calls across multiple locations whose justifications all
315/// depend on the same caller invariants.
316#[derive(Debug)]
317enum SighashError {
318    /// Caller passed an `input_index` greater than or equal to the number of
319    /// transparent inputs the caller declared in `all_previous_outputs`.
320    InputIndexOutOfBounds {
321        input_index: usize,
322        input_count: usize,
323    },
324    /// Caller asked for a transparent sighash on a transaction that
325    /// `zcash_primitives` parsed without a transparent bundle. This contradicts
326    /// the precondition that `Some((input_index, _))` is only passed for
327    /// transactions with at least one transparent input.
328    NoTransparentBundle,
329    /// `input_index` is within bounds for `all_previous_outputs` but out of
330    /// bounds for the transparent bundle's `vin` returned by
331    /// `zcash_primitives`. Reaching this branch indicates a serialize /
332    /// deserialize round-trip inconsistency between Zebra's `Transaction` and
333    /// the parsed `zcash_primitives::Transaction`, which would be a bug in
334    /// either crate.
335    BundleInputCountMismatch {
336        input_index: usize,
337        bundle_vin_len: usize,
338        all_prev_outputs_len: usize,
339    },
340    /// The previous output's value could not be converted to `Zatoshis`.
341    /// Reaching this branch means the caller passed an output whose amount
342    /// was not validated by the consensus rules before sighash computation.
343    InvalidPreviousOutputAmount,
344}
345
346impl std::fmt::Display for SighashError {
347    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
348        match self {
349            Self::InputIndexOutOfBounds {
350                input_index,
351                input_count,
352            } => write!(
353                f,
354                "input_index {input_index} is out of bounds (transaction has \
355                 {input_count} transparent inputs)"
356            ),
357            Self::NoTransparentBundle => f.write_str(
358                "transparent sighash requested for a transaction with no \
359                 transparent bundle (vin and vout both empty)",
360            ),
361            Self::BundleInputCountMismatch {
362                input_index,
363                bundle_vin_len,
364                all_prev_outputs_len,
365            } => write!(
366                f,
367                "input_index {input_index} valid for all_previous_outputs (len \
368                 {all_prev_outputs_len}) but out of bounds for the parsed \
369                 transparent bundle (vin len {bundle_vin_len}); this indicates \
370                 a serialize/deserialize round-trip inconsistency"
371            ),
372            Self::InvalidPreviousOutputAmount => f.write_str(
373                "previous output amount could not be converted to Zatoshis; \
374                 the amount should have been validated before sighash \
375                 computation",
376            ),
377        }
378    }
379}
380
381impl std::error::Error for SighashError {}
382
383/// Compute a signature hash using librustzcash.
384///
385/// # Inputs
386///
387/// - `precomputed_tx_data`: precomputed data for the transaction whose
388///   signature hash is being computed.
389/// - `hash_type`: the type of hash (SIGHASH) being used.
390/// - `input_index_script_code`: a tuple with the index of the transparent Input
391///   for which we are producing a sighash and the respective script code being
392///   validated, or None if it's a shielded input.
393///
394/// # Panics
395///
396/// - if `input_index_script_code` is `Some((input_index, _))` and `input_index`
397///   is out of bounds for `precomputed_tx_data.all_previous_outputs`. The
398///   public callers in `zebra-chain` document this as a precondition.
399/// - if the previous output at `input_index` has a value that cannot be
400///   converted to `Zatoshis`. Output values are validated before sighash
401///   computation, so this branch is unreachable in practice.
402pub(crate) fn sighash(
403    precomputed_tx_data: &PrecomputedTxData,
404    hash_type: HashType,
405    input_index_script_code: Option<(usize, Vec<u8>)>,
406) -> SigHash {
407    sighash_inner(
408        precomputed_tx_data,
409        hash_type.try_into().expect("hash type should be canonical"),
410        input_index_script_code,
411    )
412    .expect(
413        "sighash precondition violated: callers must pass an in-bounds \
414         input_index when computing a transparent sighash, and the transaction \
415         must contain the transparent input being signed",
416    )
417}
418
419/// Compute a pre-V5 (V4) signature hash using the raw `hash_type` byte.
420///
421/// `zcashd` serializes the full raw byte into the V4 sighash preimage and only
422/// masks with `SIGHASH_MASK` (0x1f) for selection logic. Callers handling V5+
423/// transactions must use [`sighash`] instead so ZIP-244 strictness is enforced.
424///
425/// # Panics
426///
427/// Same preconditions as [`sighash`].
428pub(crate) fn sighash_v4_raw(
429    precomputed_tx_data: &PrecomputedTxData,
430    raw_hash_type: u8,
431    input_index_script_code: Option<(usize, Vec<u8>)>,
432) -> SigHash {
433    sighash_inner(
434        precomputed_tx_data,
435        zcash_transparent::sighash::SighashType::from_raw(raw_hash_type),
436        input_index_script_code,
437    )
438    .expect(
439        "sighash precondition violated: callers must pass an in-bounds \
440         input_index when computing a transparent sighash, and the transaction \
441         must contain the transparent input being signed",
442    )
443}
444
445/// Internal sighash computation that surfaces precondition violations through
446/// `Result` instead of spreading `.expect()` calls across multiple sites.
447///
448/// All callers in `zebra-chain` unwrap the returned `Result` at the public
449/// boundary, but funnelling the error variants through one type makes it
450/// obvious which preconditions each call site relies on.
451fn sighash_inner(
452    precomputed_tx_data: &PrecomputedTxData,
453    sighash_type: zcash_transparent::sighash::SighashType,
454    input_index_script_code: Option<(usize, Vec<u8>)>,
455) -> Result<SigHash, SighashError> {
456    let lock_script: zcash_transparent::address::Script;
457    let unlock_script: zcash_transparent::address::Script;
458    let signable_input = match input_index_script_code {
459        Some((input_index, script_code)) => {
460            // The `all_previous_outputs` vector is supplied by the caller in
461            // 1:1 correspondence with `tx.inputs()`, and the transparent
462            // bundle was produced by round-tripping the same transaction
463            // bytes through `zcash_primitives::Transaction::read`. Both have
464            // length equal to `tx.inputs().len()`, so an out-of-bounds index
465            // is a caller error that should be reported once here.
466            let all_prev_outputs_len = precomputed_tx_data.all_previous_outputs.len();
467            let output = precomputed_tx_data
468                .all_previous_outputs
469                .get(input_index)
470                .ok_or(SighashError::InputIndexOutOfBounds {
471                    input_index,
472                    input_count: all_prev_outputs_len,
473                })?;
474            // `zcash_primitives::Transaction::read` returns
475            // `transparent_bundle = None` only when both `vin` and `vout` are
476            // empty. The caller only reaches this branch with `Some(_)` when
477            // the transaction has at least one transparent input, so reaching
478            // a `None` here means the caller violated the precondition or
479            // librustzcash changed its behaviour.
480            let bundle = precomputed_tx_data
481                .tx_data
482                .transparent_bundle()
483                .ok_or(SighashError::NoTransparentBundle)?;
484            lock_script = output.lock_script.clone().into();
485            unlock_script = zcash_transparent::address::Script(script::Code(script_code));
486            let value = output
487                .value
488                .try_into()
489                .map_err(|_| SighashError::InvalidPreviousOutputAmount)?;
490            let from_parts = zcash_transparent::sighash::SignableInput::from_parts(
491                bundle,
492                sighash_type,
493                input_index,
494                &unlock_script,
495                &lock_script,
496                value,
497            )
498            .map_err(|_| SighashError::BundleInputCountMismatch {
499                input_index,
500                bundle_vin_len: bundle.vin.len(),
501                all_prev_outputs_len,
502            })?;
503            zp_tx::sighash::SignableInput::Transparent(from_parts)
504        }
505        None => zp_tx::sighash::SignableInput::Shielded,
506    };
507
508    Ok(SigHash(
509        *zp_tx::sighash::signature_hash(
510            &precomputed_tx_data.tx_data,
511            &signable_input,
512            &precomputed_tx_data.txid_parts,
513        )
514        .as_ref(),
515    ))
516}
517
518/// Compute the authorizing data commitment of this transaction as specified in [ZIP-244].
519///
520/// # Panics
521///
522/// If passed a pre-v5 transaction.
523///
524/// [ZIP-244]: https://zips.z.cash/zip-0244
525pub(crate) fn auth_digest(tx: &Transaction) -> AuthDigest {
526    let nu = tx.network_upgrade().expect("V5 tx has a network upgrade");
527
528    AuthDigest(
529        tx.to_librustzcash(nu)
530            .expect("V5 tx is convertible to its `zcash_params` equivalent")
531            .auth_commitment()
532            .as_ref()
533            .try_into()
534            .expect("digest has the correct size"),
535    )
536}