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}