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