Skip to main content

zakura_script/
lib.rs

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