zebra-script 6.0.1

Zebra script verification wrapping zcashd's zcash_script library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
//! Zebra script verification wrapping zcashd's zcash_script library
#![doc(html_favicon_url = "https://zfnd.org/wp-content/uploads/2022/03/zebra-favicon-128.png")]
#![doc(html_logo_url = "https://zfnd.org/wp-content/uploads/2022/03/zebra-icon.png")]
#![doc(html_root_url = "https://docs.rs/zebra_script")]
// We allow unsafe code, so we can call zcash_script
#![allow(unsafe_code)]

#[cfg(test)]
mod tests;

use core::fmt;
use std::sync::Arc;

use thiserror::Error;

use libzcash_script::ZcashScript;

use zcash_script::{opcode::PossiblyBad, script, script::Evaluable as _, Opcode};
use zebra_chain::{
    parameters::NetworkUpgrade,
    transaction::{HashType, SigHasher},
    transparent,
};

/// An Error type representing the error codes returned from zcash_script.
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
    /// script verification failed
    ScriptInvalid,
    /// input index out of bounds
    TxIndex,
    /// tx is a coinbase transaction and should not be verified
    TxCoinbase,
    /// unknown error from zcash_script: {0}
    Unknown(libzcash_script::Error),
    /// transaction is invalid according to zebra_chain (not a zcash_script error)
    TxInvalid(#[from] zebra_chain::Error),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&match self {
            Error::ScriptInvalid => "script verification failed".to_owned(),
            Error::TxIndex => "input index out of bounds".to_owned(),
            Error::TxCoinbase => {
                "tx is a coinbase transaction and should not be verified".to_owned()
            }
            Error::Unknown(e) => format!("unknown error from zcash_script: {e:?}"),
            Error::TxInvalid(e) => format!("tx is invalid: {e}"),
        })
    }
}

impl From<libzcash_script::Error> for Error {
    #[allow(non_upper_case_globals)]
    fn from(err_code: libzcash_script::Error) -> Error {
        Error::Unknown(err_code)
    }
}

/// Get the interpreter according to the feature flag
fn get_interpreter(
    sighash: zcash_script::interpreter::SighashCalculator<'_>,
    lock_time: u32,
    is_final: bool,
) -> impl ZcashScript + use<'_> {
    #[cfg(feature = "comparison-interpreter")]
    return libzcash_script::cxx_rust_comparison_interpreter(sighash, lock_time, is_final);
    #[cfg(not(feature = "comparison-interpreter"))]
    libzcash_script::CxxInterpreter {
        sighash,
        lock_time,
        is_final,
    }
}

/// A preprocessed Transaction which can be used to verify scripts within said
/// Transaction.
#[derive(Debug)]
pub struct CachedFfiTransaction {
    /// The deserialized Zebra transaction.
    ///
    /// This field is private so that `transaction`, and `all_previous_outputs` always match.
    transaction: Arc<zebra_chain::transaction::Transaction>,

    /// The outputs from previous transactions that match each input in the transaction
    /// being verified.
    all_previous_outputs: Arc<Vec<transparent::Output>>,

    /// The sighasher context to use to compute sighashes.
    sighasher: SigHasher,
}

impl CachedFfiTransaction {
    /// Construct a `CachedFfiTransaction` from a `Transaction` and the outputs
    /// from previous transactions that match each input in the transaction
    /// being verified.
    pub fn new(
        transaction: Arc<zebra_chain::transaction::Transaction>,
        all_previous_outputs: Arc<Vec<transparent::Output>>,
        nu: NetworkUpgrade,
    ) -> Result<Self, Error> {
        let sighasher = transaction.sighasher(nu, all_previous_outputs.clone())?;
        Ok(Self {
            transaction,
            all_previous_outputs,
            sighasher,
        })
    }

    /// Returns the transparent inputs for this transaction.
    pub fn inputs(&self) -> &[transparent::Input] {
        self.transaction.inputs()
    }

    /// Returns the outputs from previous transactions that match each input in the transaction
    /// being verified.
    pub fn all_previous_outputs(&self) -> &Vec<transparent::Output> {
        &self.all_previous_outputs
    }

    /// Return the sighasher being used for this transaction.
    pub fn sighasher(&self) -> &SigHasher {
        &self.sighasher
    }

    /// Returns the total number of P2SH sigops across all inputs of this transaction.
    ///
    /// Mirrors zcashd's [`GetP2SHSigOpCount()`].
    ///
    /// For each P2SH input (where the spent `scriptPubKey` is P2SH), the redeem script (the last
    /// data push in the `scriptSig`) is parsed in "accurate" mode and its sigops are counted.
    /// Coinbase inputs contribute zero.
    ///
    /// This must be included in the block-wide `MAX_BLOCK_SIGOPS` total to match zcashd's consensus
    /// behavior.
    ///
    /// [`GetP2SHSigOpCount()`]: https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L840-L852
    pub fn p2sh_sigops(&self) -> u32 {
        p2sh_sigop_count(&self.transaction, &self.all_previous_outputs)
    }

    /// Verify if the script in the input at `input_index` of a transaction correctly spends the
    /// matching [`transparent::Output`] it refers to.
    #[allow(clippy::unwrap_in_result)]
    pub fn is_valid(&self, input_index: usize) -> Result<(), Error> {
        let previous_output = self
            .all_previous_outputs
            .get(input_index)
            .filter(|_| self.all_previous_outputs.len() == self.transaction.inputs().len())
            .ok_or(Error::TxIndex)?
            .clone();

        let transparent::Output {
            value: _,
            lock_script,
        } = previous_output;
        let script_pub_key: &[u8] = lock_script.as_raw_bytes();

        let flags = zcash_script::interpreter::Flags::P2SH
            | zcash_script::interpreter::Flags::CHECKLOCKTIMEVERIFY;

        let lock_time = self.transaction.raw_lock_time();
        let is_final = self.transaction.inputs()[input_index].sequence() == u32::MAX;
        let signature_script = match &self.transaction.inputs()[input_index] {
            transparent::Input::PrevOut {
                outpoint: _,
                unlock_script,
                sequence: _,
            } => unlock_script.as_raw_bytes(),
            transparent::Input::Coinbase { .. } => Err(Error::TxCoinbase)?,
        };

        let script =
            script::Raw::from_raw_parts(signature_script.to_vec(), script_pub_key.to_vec());

        let calculate_sighash =
            |script_code: &script::Code, hash_type: &zcash_script::signature::HashType| {
                // Inner helper: returns None when the hash type is invalid
                // and the callback should signal failure.
                let computed: Option<[u8; 32]> = (|| {
                    // For v5+ transactions: reject undefined hash_type values,
                    // matching zcashd's SighashType::parse behavior.
                    // Valid values: {0x01, 0x02, 0x03, 0x81, 0x82, 0x83}.
                    if self.transaction.version() >= 5 {
                        let valid_v5_types: &[i32] = &[0x01, 0x02, 0x03, 0x81, 0x82, 0x83];
                        if !valid_v5_types.contains(&hash_type.raw_bits()) {
                            return None;
                        }
                    }

                    // For v5+ transactions: reject SIGHASH_SINGLE when there is
                    // no corresponding output (an output at the same index as
                    // the input being verified). ZIP-244 §S.2a marks this as a
                    // consensus failure; zcashd throws in `SignatureHash` and
                    // `CheckSig` catches the exception to fail the script.
                    if self.transaction.version() >= 5
                        && hash_type.signed_outputs()
                            == zcash_script::signature::SignedOutputs::Single
                        && input_index >= self.transaction.outputs().len()
                    {
                        return None;
                    }

                    let script_code_vec = script_code.0.clone();

                    // For pre-v5 (v4) transactions: zcashd serializes the raw
                    // hash_type byte into the sighash preimage (only masking with
                    // 0x1f for selection logic). Use the raw byte to match.
                    if self.transaction.version() < 5 {
                        let raw_byte = hash_type.raw_bits() as u8;
                        return Some(
                            self.sighasher()
                                .sighash_v4_raw(raw_byte, Some((input_index, script_code_vec)))
                                .0,
                        );
                    }

                    let mut our_hash_type = match hash_type.signed_outputs() {
                        zcash_script::signature::SignedOutputs::All => HashType::ALL,
                        zcash_script::signature::SignedOutputs::Single => HashType::SINGLE,
                        zcash_script::signature::SignedOutputs::None => HashType::NONE,
                    };
                    if hash_type.anyone_can_pay() {
                        our_hash_type |= HashType::ANYONECANPAY;
                    }
                    Some(
                        self.sighasher()
                            .sighash(our_hash_type, Some((input_index, script_code_vec)))
                            .0,
                    )
                })();

                // Workaround for the libzcash_script callback API: returning
                // `None` from this callback does not propagate failure to the
                // C++ verifier.
                //
                // Instead of returning `None` to indicate an error, we return a
                // per-call randomly-generated dummy sighash so any signature
                // fails to verify with overwhelming probability. Note that a
                // fixed sentinel value would be unsafe: an attacker who knows
                // it can construct an ECDSA signature that verifies against any
                // 32-byte value under a chosen pubkey.
                //
                // This shim can be removed once libzcash_script propagates
                // callback failure to the C++ verifier.
                Some(computed.unwrap_or_else(|| {
                    use rand::RngCore;
                    let mut bytes = [0u8; 32];
                    rand::rngs::OsRng.fill_bytes(&mut bytes);
                    bytes
                }))
            };
        let interpreter = get_interpreter(&calculate_sighash, lock_time, is_final);
        interpreter
            .verify_callback(&script, flags)
            .map_err(|(_, e)| Error::from(e))
            .and_then(|res| {
                if res {
                    Ok(())
                } else {
                    Err(Error::ScriptInvalid)
                }
            })
    }
}

/// Trait for counting the number of transparent signature operations in the transparent inputs and
/// outputs of a transaction.
///
/// Mirrors zcashd's [`GetLegacySigOpCount()`].
///
/// All transparent inputs are included, including the coinbase input script. zcashd charges
/// coinbase `scriptSig` sigops against the block `MAX_BLOCK_SIGOPS` limit, so Zebra must do the
/// same to avoid a consensus split.
///
/// [`GetLegacySigOpCount()`]: https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L826-L836
pub trait Sigops {
    /// Returns the number of transparent signature operations in the
    /// transparent inputs and outputs of the given transaction.
    fn sigops(&self) -> Result<u32, libzcash_script::Error> {
        let interpreter = get_interpreter(&|_, _| None, 0, true);

        Ok(self.scripts().try_fold(0, |acc, s| {
            interpreter
                .legacy_sigop_count_script(&script::Code(s))
                .map(|n| acc + n)
        })?)
    }

    /// Returns an iterator over the input and output scripts in the transaction.
    ///
    /// For consensus sigop accounting, this must include the coinbase input
    /// script (height prefix followed by extra data), matching zcashd's
    /// `GetLegacySigOpCount()`.
    fn scripts(&self) -> impl Iterator<Item = Vec<u8>>;
}

impl Sigops for zebra_chain::transaction::Transaction {
    fn scripts(&self) -> impl Iterator<Item = Vec<u8>> {
        self.inputs()
            .iter()
            .map(|input| match input {
                transparent::Input::PrevOut { unlock_script, .. } => {
                    unlock_script.as_raw_bytes().to_vec()
                }
                // Coinbase scriptSig = encoded height || extra data, which must be reconstructed
                // for sigop counting. `coinbase_script()` round-trips through
                // `write_coinbase_height`, which only fails when called on a malformed in-memory
                // genesis coinbase. Any coinbase that was successfully deserialized round-trips
                // cleanly, so this `expect` cannot fire on validation paths.
                transparent::Input::Coinbase { .. } => input
                    .coinbase_script()
                    .expect("coinbase_script reconstructs from a deserialized coinbase input"),
            })
            .chain(
                self.outputs()
                    .iter()
                    .map(|o| o.lock_script.as_raw_bytes().to_vec()),
            )
    }
}

impl Sigops for zebra_chain::transaction::UnminedTx {
    fn scripts(&self) -> impl Iterator<Item = Vec<u8>> {
        self.transaction.scripts()
    }
}

impl Sigops for CachedFfiTransaction {
    fn scripts(&self) -> impl Iterator<Item = Vec<u8>> {
        self.transaction.scripts()
    }
}

impl Sigops for zcash_primitives::transaction::Transaction {
    fn scripts(&self) -> impl Iterator<Item = Vec<u8>> {
        self.transparent_bundle().into_iter().flat_map(|bundle| {
            // `zcash_primitives` stores the coinbase input's full serialized scriptSig (height
            // prefix + extra data) in the synthesized input's script_sig, so it is included as-is
            // for sigop counting.
            bundle
                .vin
                .iter()
                .map(|i| i.script_sig().0 .0.clone())
                .chain(bundle.vout.iter().map(|o| o.script_pubkey().0 .0.clone()))
        })
    }
}

/// Extract the redeem script bytes from a P2SH scriptSig.
///
/// Mirrors zcashd's P2SH redeem-script extraction in
/// [`CScript::GetSigOpCount(const CScript& scriptSig)`].
///
/// Iterates the scriptSig opcodes and returns the last successfully pushed data value. Returns
/// `None` if any opcode fails to parse, OR if any opcode is not a push value (zcashd: `opcode >
/// OP_16`). This matches zcashd's behavior of returning 0 P2SH sigops for malformed or
/// non-push-only scriptSigs.
///
/// [`CScript::GetSigOpCount(const CScript& scriptSig)`]: https://github.com/zcash/zcash/blob/v6.11.0/src/script/script.cpp#L176-L199
fn extract_p2sh_redeem_script(unlock_script: &transparent::Script) -> Option<Vec<u8>> {
    let code = script::Code(unlock_script.as_raw_bytes().to_vec());
    let mut last_push_data: Option<Vec<u8>> = None;
    for opcode in code.parse() {
        match opcode {
            Ok(PossiblyBad::Good(Opcode::PushValue(pv))) => {
                last_push_data = Some(pv.value());
            }
            // Non-push opcode (operation, control, or bad) or parse error: zcashd returns 0 sigops
            // in this case. Match that behavior by discarding any data collected so far.
            _ => return None,
        }
    }
    last_push_data
}

/// Returns the P2SH sigop count for a single input.
///
/// Returns 0 for non-P2SH inputs, coinbase inputs, and P2SH inputs where no redeem script can be
/// extracted from the scriptSig.
fn p2sh_input_sigop_count(input: &transparent::Input, spent_output: &transparent::Output) -> u32 {
    let unlock_script = match input {
        transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
        transparent::Input::Coinbase { .. } => return 0,
    };

    let lock_code = script::Code(spent_output.lock_script.as_raw_bytes().to_vec());

    if !lock_code.is_pay_to_script_hash() {
        return 0;
    }

    let Some(redeemed_bytes) = extract_p2sh_redeem_script(unlock_script) else {
        return 0;
    };

    script::Code(redeemed_bytes).sig_op_count(true)
}

/// Returns the total number of P2SH sigops across all inputs of `tx`.
///
/// Mirrors zcashd's [`GetP2SHSigOpCount()`].
///
/// Coinbase transactions always return zero, matching zcashd's early-return for `tx.IsCoinBase()`.
/// Callers are therefore permitted to pass an empty `spent_outputs` slice for coinbase transactions
/// (which is what the block-verifier does, since coinbase inputs have no previous output).
///
/// # Correctness
///
/// For non-coinbase transactions, `spent_outputs.len()` must equal the number of transparent inputs
/// in `tx`. If the lengths differ, `zip()` silently truncates the longer iterator, causing an
/// incorrect (undercount) result.
///
/// [`GetP2SHSigOpCount()`]: https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L840-L852
pub fn p2sh_sigop_count(
    tx: &zebra_chain::transaction::Transaction,
    spent_outputs: &[transparent::Output],
) -> u32 {
    if tx.is_coinbase() {
        return 0;
    }

    debug_assert_eq!(
        tx.inputs().len(),
        spent_outputs.len(),
        "spent_outputs must align with transaction inputs for non-coinbase txs"
    );

    tx.inputs()
        .iter()
        .zip(spent_outputs.iter())
        .map(|(input, spent_output)| p2sh_input_sigop_count(input, spent_output))
        .sum()
}