Skip to main content

zebra_script/
lib.rs

1//! Zebra script verification wrapping zcashd's zcash_script library
2#![doc(html_favicon_url = "https://zfnd.org/wp-content/uploads/2022/03/zebra-favicon-128.png")]
3#![doc(html_logo_url = "https://zfnd.org/wp-content/uploads/2022/03/zebra-icon.png")]
4#![doc(html_root_url = "https://docs.rs/zebra_script")]
5// We allow unsafe code, so we can call zcash_script
6#![allow(unsafe_code)]
7
8#[cfg(test)]
9mod tests;
10
11use core::fmt;
12use std::sync::Arc;
13
14use thiserror::Error;
15
16use libzcash_script::ZcashScript;
17
18use zcash_script::{opcode::PossiblyBad, script, script::Evaluable as _, Opcode};
19use zcash_transparent::bundle as zp_transparent;
20use zebra_chain::{
21    parameters::NetworkUpgrade,
22    transaction::{HashType, SigHasher},
23    transparent,
24};
25
26/// An Error type representing the error codes returned from zcash_script.
27#[derive(Clone, Debug, Error, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum Error {
30    /// script verification failed
31    ScriptInvalid,
32    /// input index out of bounds
33    TxIndex,
34    /// tx is a coinbase transaction and should not be verified
35    TxCoinbase,
36    /// unknown error from zcash_script: {0}
37    Unknown(libzcash_script::Error),
38    /// transaction is invalid according to zebra_chain (not a zcash_script error)
39    TxInvalid(#[from] zebra_chain::Error),
40}
41
42impl fmt::Display for Error {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.write_str(&match self {
45            Error::ScriptInvalid => "script verification failed".to_owned(),
46            Error::TxIndex => "input index out of bounds".to_owned(),
47            Error::TxCoinbase => {
48                "tx is a coinbase transaction and should not be verified".to_owned()
49            }
50            Error::Unknown(e) => format!("unknown error from zcash_script: {e:?}"),
51            Error::TxInvalid(e) => format!("tx is invalid: {e}"),
52        })
53    }
54}
55
56impl From<libzcash_script::Error> for Error {
57    #[allow(non_upper_case_globals)]
58    fn from(err_code: libzcash_script::Error) -> Error {
59        Error::Unknown(err_code)
60    }
61}
62
63/// Get the interpreter according to the feature flag
64fn get_interpreter(
65    sighash: zcash_script::interpreter::SighashCalculator<'_>,
66    lock_time: u32,
67    is_final: bool,
68) -> impl ZcashScript + use<'_> {
69    #[cfg(feature = "comparison-interpreter")]
70    return libzcash_script::cxx_rust_comparison_interpreter(sighash, lock_time, is_final);
71    #[cfg(not(feature = "comparison-interpreter"))]
72    libzcash_script::CxxInterpreter {
73        sighash,
74        lock_time,
75        is_final,
76    }
77}
78
79/// A preprocessed Transaction which can be used to verify scripts within said
80/// Transaction.
81#[derive(Debug)]
82pub struct CachedFfiTransaction {
83    /// The deserialized Zebra transaction.
84    ///
85    /// This field is private so that `transaction`, and `all_previous_outputs` always match.
86    transaction: Arc<zebra_chain::transaction::Transaction>,
87
88    /// The outputs from previous transactions that match each input in the transaction
89    /// being verified.
90    all_previous_outputs: Arc<Vec<transparent::Output>>,
91
92    /// The sighasher context to use to compute sighashes.
93    sighasher: SigHasher,
94}
95
96impl CachedFfiTransaction {
97    /// Construct a `CachedFfiTransaction` from a `Transaction` and the outputs
98    /// from previous transactions that match each input in the transaction
99    /// being verified.
100    pub fn new(
101        transaction: Arc<zebra_chain::transaction::Transaction>,
102        all_previous_outputs: Arc<Vec<transparent::Output>>,
103        nu: NetworkUpgrade,
104    ) -> Result<Self, Error> {
105        let sighasher = SigHasher::new(&transaction, nu, all_previous_outputs.clone())?;
106        Ok(Self {
107            transaction,
108            all_previous_outputs,
109            sighasher,
110        })
111    }
112
113    /// Returns the transparent inputs of this transaction, borrowed from the underlying
114    /// `zcash_primitives` transaction.
115    fn vin(&self) -> &[zp_transparent::TxIn<zp_transparent::Authorized>] {
116        self.transaction
117            .transparent_bundle()
118            .map(|bundle| bundle.vin.as_slice())
119            .unwrap_or_default()
120    }
121
122    /// Returns the outputs from previous transactions that match each input in the transaction
123    /// being verified.
124    pub fn all_previous_outputs(&self) -> &Vec<transparent::Output> {
125        &self.all_previous_outputs
126    }
127
128    /// Return the sighasher being used for this transaction.
129    pub fn sighasher(&self) -> &SigHasher {
130        &self.sighasher
131    }
132
133    /// Returns the total number of P2SH sigops across all inputs of this transaction.
134    ///
135    /// Mirrors zcashd's [`GetP2SHSigOpCount()`].
136    ///
137    /// For each P2SH input (where the spent `scriptPubKey` is P2SH), the redeem script (the last
138    /// data push in the `scriptSig`) is parsed in "accurate" mode and its sigops are counted.
139    /// Coinbase inputs contribute zero.
140    ///
141    /// This must be included in the block-wide `MAX_BLOCK_SIGOPS` total to match zcashd's consensus
142    /// behavior.
143    ///
144    /// [`GetP2SHSigOpCount()`]: https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L840-L852
145    pub fn p2sh_sigops(&self) -> u32 {
146        p2sh_sigop_count(&self.transaction, &self.all_previous_outputs)
147    }
148
149    /// Verify if the script in the input at `input_index` of a transaction correctly spends the
150    /// matching [`transparent::Output`] it refers to.
151    #[allow(clippy::unwrap_in_result)]
152    pub fn is_valid(&self, input_index: usize) -> Result<(), Error> {
153        let previous_output = self
154            .all_previous_outputs
155            .get(input_index)
156            .filter(|_| self.all_previous_outputs.len() == self.vin().len())
157            .ok_or(Error::TxIndex)?
158            .clone();
159
160        let transparent::Output {
161            value: _,
162            lock_script,
163        } = previous_output;
164        let script_pub_key: &[u8] = lock_script.as_raw_bytes();
165
166        let flags = zcash_script::interpreter::Flags::P2SH
167            | zcash_script::interpreter::Flags::CHECKLOCKTIMEVERIFY;
168
169        let lock_time = self.transaction.raw_lock_time();
170        let txin = self.vin().get(input_index).ok_or(Error::TxIndex)?;
171        let is_final = txin.sequence() == u32::MAX;
172
173        if *txin.prevout() == zp_transparent::OutPoint::NULL {
174            Err(Error::TxCoinbase)?;
175        }
176
177        let signature_script: &[u8] = &txin.script_sig().0 .0;
178
179        let script =
180            script::Raw::from_raw_parts(signature_script.to_vec(), script_pub_key.to_vec());
181
182        let calculate_sighash =
183            |script_code: &script::Code, hash_type: &zcash_script::signature::HashType| {
184                // Inner helper: returns None when the hash type is invalid
185                // and the callback should signal failure.
186                let computed: Option<[u8; 32]> = (|| {
187                    // For v5+ transactions: reject undefined hash_type values,
188                    // matching zcashd's SighashType::parse behavior.
189                    // Valid values: {0x01, 0x02, 0x03, 0x81, 0x82, 0x83}.
190                    if self.transaction.version() >= 5 {
191                        let valid_v5_types: &[i32] = &[0x01, 0x02, 0x03, 0x81, 0x82, 0x83];
192                        if !valid_v5_types.contains(&hash_type.raw_bits()) {
193                            return None;
194                        }
195                    }
196
197                    // For v5+ transactions: reject SIGHASH_SINGLE when there is
198                    // no corresponding output (an output at the same index as
199                    // the input being verified). ZIP-244 §S.2a marks this as a
200                    // consensus failure; zcashd throws in `SignatureHash` and
201                    // `CheckSig` catches the exception to fail the script.
202                    if self.transaction.version() >= 5
203                        && hash_type.signed_outputs()
204                            == zcash_script::signature::SignedOutputs::Single
205                        && input_index >= self.transaction.outputs().len()
206                    {
207                        return None;
208                    }
209
210                    let script_code_vec = script_code.0.clone();
211
212                    // For pre-v5 (v4) transactions: zcashd serializes the raw
213                    // hash_type byte into the sighash preimage (only masking with
214                    // 0x1f for selection logic). Use the raw byte to match.
215                    if self.transaction.version() < 5 {
216                        let raw_byte = hash_type.raw_bits() as u8;
217                        return Some(
218                            self.sighasher()
219                                .sighash_v4_raw(raw_byte, Some((input_index, script_code_vec)))
220                                .0,
221                        );
222                    }
223
224                    let mut our_hash_type = match hash_type.signed_outputs() {
225                        zcash_script::signature::SignedOutputs::All => HashType::ALL,
226                        zcash_script::signature::SignedOutputs::Single => HashType::SINGLE,
227                        zcash_script::signature::SignedOutputs::None => HashType::NONE,
228                    };
229                    if hash_type.anyone_can_pay() {
230                        our_hash_type |= HashType::ANYONECANPAY;
231                    }
232                    Some(
233                        self.sighasher()
234                            .sighash(our_hash_type, Some((input_index, script_code_vec)))
235                            .0,
236                    )
237                })();
238
239                // Workaround for the libzcash_script callback API: returning
240                // `None` from this callback does not propagate failure to the
241                // C++ verifier.
242                //
243                // Instead of returning `None` to indicate an error, we return a
244                // per-call randomly-generated dummy sighash so any signature
245                // fails to verify with overwhelming probability. Note that a
246                // fixed sentinel value would be unsafe: an attacker who knows
247                // it can construct an ECDSA signature that verifies against any
248                // 32-byte value under a chosen pubkey.
249                //
250                // This shim can be removed once libzcash_script propagates
251                // callback failure to the C++ verifier.
252                Some(computed.unwrap_or_else(|| {
253                    use rand::RngCore;
254                    let mut bytes = [0u8; 32];
255                    rand::rngs::OsRng.fill_bytes(&mut bytes);
256                    bytes
257                }))
258            };
259        let interpreter = get_interpreter(&calculate_sighash, lock_time, is_final);
260        interpreter
261            .verify_callback(&script, flags)
262            .map_err(|(_, e)| Error::from(e))
263            .and_then(|res| {
264                if res {
265                    Ok(())
266                } else {
267                    Err(Error::ScriptInvalid)
268                }
269            })
270    }
271}
272
273/// Trait for counting the number of transparent signature operations in the transparent inputs and
274/// outputs of a transaction.
275///
276/// Mirrors zcashd's [`GetLegacySigOpCount()`].
277///
278/// All transparent inputs are included, including the coinbase input script. zcashd charges
279/// coinbase `scriptSig` sigops against the block `MAX_BLOCK_SIGOPS` limit, so Zebra must do the
280/// same to avoid a consensus split.
281///
282/// [`GetLegacySigOpCount()`]: https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L826-L836
283pub trait Sigops {
284    /// Returns the number of transparent signature operations in the
285    /// transparent inputs and outputs of the given transaction.
286    fn sigops(&self) -> Result<u32, libzcash_script::Error> {
287        let interpreter = get_interpreter(&|_, _| None, 0, true);
288
289        Ok(self.scripts().into_iter().try_fold(0, |acc, s| {
290            interpreter
291                .legacy_sigop_count_script(&script::Code(s))
292                .map(|n| acc + n)
293        })?)
294    }
295
296    /// Returns the input and output scripts in the transaction as owned byte vectors.
297    ///
298    /// For consensus sigop accounting, this must include the coinbase input
299    /// script (height prefix followed by extra data), matching zcashd's
300    /// `GetLegacySigOpCount()`.
301    fn scripts(&self) -> Vec<Vec<u8>>;
302}
303
304impl Sigops for zebra_chain::transaction::Transaction {
305    fn scripts(&self) -> Vec<Vec<u8>> {
306        let mut scripts: Vec<Vec<u8>> = self
307            .inputs()
308            .into_iter()
309            .map(|input| match &input {
310                transparent::Input::PrevOut { unlock_script, .. } => {
311                    unlock_script.as_raw_bytes().to_vec()
312                }
313                // Coinbase scriptSig = encoded height || extra data, which must be reconstructed
314                // for sigop counting. `coinbase_script()` round-trips through
315                // `write_coinbase_height`, which only fails when called on a malformed in-memory
316                // genesis coinbase. Any coinbase that was successfully deserialized round-trips
317                // cleanly, so this `expect` cannot fire on validation paths.
318                transparent::Input::Coinbase { .. } => input
319                    .coinbase_script()
320                    .expect("coinbase_script reconstructs from a deserialized coinbase input"),
321            })
322            .collect();
323
324        scripts.extend(
325            self.outputs()
326                .into_iter()
327                .map(|o| o.lock_script.as_raw_bytes().to_vec()),
328        );
329
330        scripts
331    }
332}
333
334impl Sigops for zebra_chain::transaction::UnminedTx {
335    fn scripts(&self) -> Vec<Vec<u8>> {
336        self.transaction.scripts()
337    }
338}
339
340impl Sigops for CachedFfiTransaction {
341    fn scripts(&self) -> Vec<Vec<u8>> {
342        self.transaction.scripts()
343    }
344}
345
346impl Sigops for zcash_primitives::transaction::Transaction {
347    fn scripts(&self) -> Vec<Vec<u8>> {
348        self.transparent_bundle()
349            .into_iter()
350            .flat_map(|bundle| {
351                // `zcash_primitives` stores the coinbase input's full serialized scriptSig (height
352                // prefix + extra data) in the synthesized input's script_sig, so it is included
353                // as-is for sigop counting.
354                bundle
355                    .vin
356                    .iter()
357                    .map(|i| i.script_sig().0 .0.clone())
358                    .chain(bundle.vout.iter().map(|o| o.script_pubkey().0 .0.clone()))
359            })
360            .collect()
361    }
362}
363
364/// Extract the redeem script bytes from a P2SH scriptSig.
365///
366/// Mirrors zcashd's P2SH redeem-script extraction in
367/// [`CScript::GetSigOpCount(const CScript& scriptSig)`].
368///
369/// Iterates the scriptSig opcodes and returns the last successfully pushed data value. Returns
370/// `None` if any opcode fails to parse, OR if any opcode is not a push value (zcashd: `opcode >
371/// OP_16`). This matches zcashd's behavior of returning 0 P2SH sigops for malformed or
372/// non-push-only scriptSigs.
373///
374/// [`CScript::GetSigOpCount(const CScript& scriptSig)`]: https://github.com/zcash/zcash/blob/v6.11.0/src/script/script.cpp#L176-L199
375fn extract_p2sh_redeem_script(unlock_script: &transparent::Script) -> Option<Vec<u8>> {
376    let code = script::Code(unlock_script.as_raw_bytes().to_vec());
377    let mut last_push_data: Option<Vec<u8>> = None;
378    for opcode in code.parse() {
379        match opcode {
380            Ok(PossiblyBad::Good(Opcode::PushValue(pv))) => {
381                last_push_data = Some(pv.value());
382            }
383            // Non-push opcode (operation, control, or bad) or parse error: zcashd returns 0 sigops
384            // in this case. Match that behavior by discarding any data collected so far.
385            _ => return None,
386        }
387    }
388    last_push_data
389}
390
391/// Returns the P2SH sigop count for a single input.
392///
393/// `spent_output` must be the output spent by `input`.
394///
395/// Returns 0 for non-P2SH inputs, coinbase inputs, and P2SH inputs where no redeem script can be
396/// extracted from the scriptSig (mirroring zcashd's `CScript::GetSigOpCount(scriptSig)`, which
397/// returns 0 when the scriptSig is not push-only).
398///
399/// This is the per-input counting used by [`p2sh_sigop_count`] for the block-wide consensus sigop
400/// total, and by the mempool standardness gate that rejects high-sigop P2SH inputs before script
401/// verification.
402pub fn p2sh_input_sigop_count(
403    input: &transparent::Input,
404    spent_output: &transparent::Output,
405) -> u32 {
406    let unlock_script = match input {
407        transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
408        transparent::Input::Coinbase { .. } => return 0,
409    };
410
411    let lock_code = script::Code(spent_output.lock_script.as_raw_bytes().to_vec());
412
413    if !lock_code.is_pay_to_script_hash() {
414        return 0;
415    }
416
417    let Some(redeemed_bytes) = extract_p2sh_redeem_script(unlock_script) else {
418        return 0;
419    };
420
421    // Count the redeem script's sigops in zcashd's "accurate" mode, matching
422    // `GetP2SHSigOpCount` -> `CScript::GetSigOpCount(scriptSig)` -> `subscript.GetSigOpCount(true)`.
423    // Relies on the patched `zcash_script` (see `[patch.crates-io]`) whose `sig_op_count` no longer
424    // short-circuits on disabled opcodes (incl. OP_CODESEPARATOR), which would otherwise undercount.
425    script::Code(redeemed_bytes).sig_op_count(true)
426}
427
428/// Returns the total number of P2SH sigops across all inputs of `tx`.
429///
430/// Mirrors zcashd's [`GetP2SHSigOpCount()`].
431///
432/// Coinbase transactions always return zero, matching zcashd's early-return for `tx.IsCoinBase()`.
433/// Callers are therefore permitted to pass an empty `spent_outputs` slice for coinbase transactions
434/// (which is what the block-verifier does, since coinbase inputs have no previous output).
435///
436/// # Correctness
437///
438/// For non-coinbase transactions, `spent_outputs.len()` must equal the number of transparent inputs
439/// in `tx`. If the lengths differ, `zip()` silently truncates the longer iterator, causing an
440/// incorrect (undercount) result.
441///
442/// [`GetP2SHSigOpCount()`]: https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L840-L852
443pub fn p2sh_sigop_count(
444    tx: &zebra_chain::transaction::Transaction,
445    spent_outputs: &[transparent::Output],
446) -> u32 {
447    if tx.is_coinbase() {
448        return 0;
449    }
450
451    debug_assert_eq!(
452        tx.inputs().len(),
453        spent_outputs.len(),
454        "spent_outputs must align with transaction inputs for non-coinbase txs"
455    );
456
457    tx.inputs()
458        .iter()
459        .zip(spent_outputs.iter())
460        .map(|(input, spent_output)| p2sh_input_sigop_count(input, spent_output))
461        .sum()
462}