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 Sapling bundle in `tx_data`.
290    pub fn sapling_bundle(
291        &self,
292    ) -> Option<sapling_crypto::Bundle<sapling_crypto::bundle::Authorized, ZatBalance>> {
293        self.tx_data.sapling_bundle().cloned()
294    }
295}
296
297/// Internal error type returned by [`sighash_inner`] when a sighash request
298/// violates one of the documented preconditions of [`sighash`] or
299/// [`sighash_v4_raw`].
300///
301/// Public callers (`SigHasher::sighash`, `SigHasher::sighash_v4_raw`) document
302/// these conditions as panics, so they unwrap the result at the public
303/// boundary. Keeping the internal code `Result`-shaped avoids spreading
304/// `.expect()` calls across multiple locations whose justifications all
305/// depend on the same caller invariants.
306#[derive(Debug)]
307enum SighashError {
308    /// Caller passed an `input_index` greater than or equal to the number of
309    /// transparent inputs the caller declared in `all_previous_outputs`.
310    InputIndexOutOfBounds {
311        input_index: usize,
312        input_count: usize,
313    },
314    /// Caller asked for a transparent sighash on a transaction that
315    /// `zcash_primitives` parsed without a transparent bundle. This contradicts
316    /// the precondition that `Some((input_index, _))` is only passed for
317    /// transactions with at least one transparent input.
318    NoTransparentBundle,
319    /// `input_index` is within bounds for `all_previous_outputs` but out of
320    /// bounds for the transparent bundle's `vin` returned by
321    /// `zcash_primitives`. Reaching this branch indicates a serialize /
322    /// deserialize round-trip inconsistency between Zebra's `Transaction` and
323    /// the parsed `zcash_primitives::Transaction`, which would be a bug in
324    /// either crate.
325    BundleInputCountMismatch {
326        input_index: usize,
327        bundle_vin_len: usize,
328        all_prev_outputs_len: usize,
329    },
330    /// The previous output's value could not be converted to `Zatoshis`.
331    /// Reaching this branch means the caller passed an output whose amount
332    /// was not validated by the consensus rules before sighash computation.
333    InvalidPreviousOutputAmount,
334}
335
336impl std::fmt::Display for SighashError {
337    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338        match self {
339            Self::InputIndexOutOfBounds {
340                input_index,
341                input_count,
342            } => write!(
343                f,
344                "input_index {input_index} is out of bounds (transaction has \
345                 {input_count} transparent inputs)"
346            ),
347            Self::NoTransparentBundle => f.write_str(
348                "transparent sighash requested for a transaction with no \
349                 transparent bundle (vin and vout both empty)",
350            ),
351            Self::BundleInputCountMismatch {
352                input_index,
353                bundle_vin_len,
354                all_prev_outputs_len,
355            } => write!(
356                f,
357                "input_index {input_index} valid for all_previous_outputs (len \
358                 {all_prev_outputs_len}) but out of bounds for the parsed \
359                 transparent bundle (vin len {bundle_vin_len}); this indicates \
360                 a serialize/deserialize round-trip inconsistency"
361            ),
362            Self::InvalidPreviousOutputAmount => f.write_str(
363                "previous output amount could not be converted to Zatoshis; \
364                 the amount should have been validated before sighash \
365                 computation",
366            ),
367        }
368    }
369}
370
371impl std::error::Error for SighashError {}
372
373/// Compute a signature hash using librustzcash.
374///
375/// # Inputs
376///
377/// - `precomputed_tx_data`: precomputed data for the transaction whose
378///   signature hash is being computed.
379/// - `hash_type`: the type of hash (SIGHASH) being used.
380/// - `input_index_script_code`: a tuple with the index of the transparent Input
381///   for which we are producing a sighash and the respective script code being
382///   validated, or None if it's a shielded input.
383///
384/// # Panics
385///
386/// - if `input_index_script_code` is `Some((input_index, _))` and `input_index`
387///   is out of bounds for `precomputed_tx_data.all_previous_outputs`. The
388///   public callers in `zebra-chain` document this as a precondition.
389/// - if the previous output at `input_index` has a value that cannot be
390///   converted to `Zatoshis`. Output values are validated before sighash
391///   computation, so this branch is unreachable in practice.
392pub(crate) fn sighash(
393    precomputed_tx_data: &PrecomputedTxData,
394    hash_type: HashType,
395    input_index_script_code: Option<(usize, Vec<u8>)>,
396) -> SigHash {
397    sighash_inner(
398        precomputed_tx_data,
399        hash_type.try_into().expect("hash type should be canonical"),
400        input_index_script_code,
401    )
402    .expect(
403        "sighash precondition violated: callers must pass an in-bounds \
404         input_index when computing a transparent sighash, and the transaction \
405         must contain the transparent input being signed",
406    )
407}
408
409/// Compute a pre-V5 (V4) signature hash using the raw `hash_type` byte.
410///
411/// `zcashd` serializes the full raw byte into the V4 sighash preimage and only
412/// masks with `SIGHASH_MASK` (0x1f) for selection logic. Callers handling V5+
413/// transactions must use [`sighash`] instead so ZIP-244 strictness is enforced.
414///
415/// # Panics
416///
417/// Same preconditions as [`sighash`].
418pub(crate) fn sighash_v4_raw(
419    precomputed_tx_data: &PrecomputedTxData,
420    raw_hash_type: u8,
421    input_index_script_code: Option<(usize, Vec<u8>)>,
422) -> SigHash {
423    sighash_inner(
424        precomputed_tx_data,
425        zcash_transparent::sighash::SighashType::from_raw(raw_hash_type),
426        input_index_script_code,
427    )
428    .expect(
429        "sighash precondition violated: callers must pass an in-bounds \
430         input_index when computing a transparent sighash, and the transaction \
431         must contain the transparent input being signed",
432    )
433}
434
435/// Internal sighash computation that surfaces precondition violations through
436/// `Result` instead of spreading `.expect()` calls across multiple sites.
437///
438/// All callers in `zebra-chain` unwrap the returned `Result` at the public
439/// boundary, but funnelling the error variants through one type makes it
440/// obvious which preconditions each call site relies on.
441fn sighash_inner(
442    precomputed_tx_data: &PrecomputedTxData,
443    sighash_type: zcash_transparent::sighash::SighashType,
444    input_index_script_code: Option<(usize, Vec<u8>)>,
445) -> Result<SigHash, SighashError> {
446    let lock_script: zcash_transparent::address::Script;
447    let unlock_script: zcash_transparent::address::Script;
448    let signable_input = match input_index_script_code {
449        Some((input_index, script_code)) => {
450            // The `all_previous_outputs` vector is supplied by the caller in
451            // 1:1 correspondence with `tx.inputs()`, and the transparent
452            // bundle was produced by round-tripping the same transaction
453            // bytes through `zcash_primitives::Transaction::read`. Both have
454            // length equal to `tx.inputs().len()`, so an out-of-bounds index
455            // is a caller error that should be reported once here.
456            let all_prev_outputs_len = precomputed_tx_data.all_previous_outputs.len();
457            let output = precomputed_tx_data
458                .all_previous_outputs
459                .get(input_index)
460                .ok_or(SighashError::InputIndexOutOfBounds {
461                    input_index,
462                    input_count: all_prev_outputs_len,
463                })?;
464            // `zcash_primitives::Transaction::read` returns
465            // `transparent_bundle = None` only when both `vin` and `vout` are
466            // empty. The caller only reaches this branch with `Some(_)` when
467            // the transaction has at least one transparent input, so reaching
468            // a `None` here means the caller violated the precondition or
469            // librustzcash changed its behaviour.
470            let bundle = precomputed_tx_data
471                .tx_data
472                .transparent_bundle()
473                .ok_or(SighashError::NoTransparentBundle)?;
474            lock_script = output.lock_script.clone().into();
475            unlock_script = zcash_transparent::address::Script(script::Code(script_code));
476            let value = output
477                .value
478                .try_into()
479                .map_err(|_| SighashError::InvalidPreviousOutputAmount)?;
480            let from_parts = zcash_transparent::sighash::SignableInput::from_parts(
481                bundle,
482                sighash_type,
483                input_index,
484                &unlock_script,
485                &lock_script,
486                value,
487            )
488            .map_err(|_| SighashError::BundleInputCountMismatch {
489                input_index,
490                bundle_vin_len: bundle.vin.len(),
491                all_prev_outputs_len,
492            })?;
493            zp_tx::sighash::SignableInput::Transparent(from_parts)
494        }
495        None => zp_tx::sighash::SignableInput::Shielded,
496    };
497
498    Ok(SigHash(
499        *zp_tx::sighash::signature_hash(
500            &precomputed_tx_data.tx_data,
501            &signable_input,
502            &precomputed_tx_data.txid_parts,
503        )
504        .as_ref(),
505    ))
506}
507
508/// Compute the authorizing data commitment of this transaction as specified in [ZIP-244].
509///
510/// # Panics
511///
512/// If passed a pre-v5 transaction.
513///
514/// [ZIP-244]: https://zips.z.cash/zip-0244
515pub(crate) fn auth_digest(tx: &Transaction) -> AuthDigest {
516    let nu = tx.network_upgrade().expect("V5 tx has a network upgrade");
517
518    AuthDigest(
519        tx.to_librustzcash(nu)
520            .expect("V5 tx is convertible to its `zcash_params` equivalent")
521            .auth_commitment()
522            .as_ref()
523            .try_into()
524            .expect("digest has the correct size"),
525    )
526}