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