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 let script_code_vec = script_code.0.clone();
194
195 // For pre-v5 (v4) transactions: zcashd serializes the raw
196 // hash_type byte into the sighash preimage (only masking with
197 // 0x1f for selection logic). Use the raw byte to match.
198 if self.transaction.version() < 5 {
199 let raw_byte = hash_type.raw_bits() as u8;
200 return Some(
201 self.sighasher()
202 .sighash_v4_raw(raw_byte, Some((input_index, script_code_vec)))
203 .0,
204 );
205 }
206
207 let mut our_hash_type = match hash_type.signed_outputs() {
208 zcash_script::signature::SignedOutputs::All => HashType::ALL,
209 zcash_script::signature::SignedOutputs::Single => HashType::SINGLE,
210 zcash_script::signature::SignedOutputs::None => HashType::NONE,
211 };
212 if hash_type.anyone_can_pay() {
213 our_hash_type |= HashType::ANYONECANPAY;
214 }
215 Some(
216 self.sighasher()
217 .sighash(our_hash_type, Some((input_index, script_code_vec)))
218 .0,
219 )
220 })();
221
222 // Workaround for the libzcash_script callback API: returning
223 // `None` from this callback does not propagate failure to the
224 // C++ verifier.
225 //
226 // Instead of returning `None` to indicate an error, we return a
227 // per-call randomly-generated dummy sighash so any signature
228 // fails to verify with overwhelming probability. Note that a
229 // fixed sentinel value would be unsafe: an attacker who knows
230 // it can construct an ECDSA signature that verifies against any
231 // 32-byte value under a chosen pubkey.
232 //
233 // This shim can be removed once libzcash_script propagates
234 // callback failure to the C++ verifier.
235 Some(computed.unwrap_or_else(|| {
236 use rand::RngCore;
237 let mut bytes = [0u8; 32];
238 rand::rngs::OsRng.fill_bytes(&mut bytes);
239 bytes
240 }))
241 };
242 let interpreter = get_interpreter(&calculate_sighash, lock_time, is_final);
243 interpreter
244 .verify_callback(&script, flags)
245 .map_err(|(_, e)| Error::from(e))
246 .and_then(|res| {
247 if res {
248 Ok(())
249 } else {
250 Err(Error::ScriptInvalid)
251 }
252 })
253 }
254}
255
256/// Trait for counting the number of transparent signature operations in the transparent inputs and
257/// outputs of a transaction.
258///
259/// Mirrors zcashd's [`GetLegacySigOpCount()`].
260///
261/// All transparent inputs are included, including the coinbase input script. zcashd charges
262/// coinbase `scriptSig` sigops against the block `MAX_BLOCK_SIGOPS` limit, so Zebra must do the
263/// same to avoid a consensus split.
264///
265/// [`GetLegacySigOpCount()`]: https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L826-L836
266pub trait Sigops {
267 /// Returns the number of transparent signature operations in the
268 /// transparent inputs and outputs of the given transaction.
269 fn sigops(&self) -> Result<u32, libzcash_script::Error> {
270 let interpreter = get_interpreter(&|_, _| None, 0, true);
271
272 Ok(self.scripts().try_fold(0, |acc, s| {
273 interpreter
274 .legacy_sigop_count_script(&script::Code(s))
275 .map(|n| acc + n)
276 })?)
277 }
278
279 /// Returns an iterator over the input and output scripts in the transaction.
280 ///
281 /// For consensus sigop accounting, this must include the coinbase input
282 /// script (height prefix followed by extra data), matching zcashd's
283 /// `GetLegacySigOpCount()`.
284 fn scripts(&self) -> impl Iterator<Item = Vec<u8>>;
285}
286
287impl Sigops for zebra_chain::transaction::Transaction {
288 fn scripts(&self) -> impl Iterator<Item = Vec<u8>> {
289 self.inputs()
290 .iter()
291 .map(|input| match input {
292 transparent::Input::PrevOut { unlock_script, .. } => {
293 unlock_script.as_raw_bytes().to_vec()
294 }
295 // Coinbase scriptSig = encoded height || extra data, which must be reconstructed
296 // for sigop counting. `coinbase_script()` round-trips through
297 // `write_coinbase_height`, which only fails when called on a malformed in-memory
298 // genesis coinbase. Any coinbase that was successfully deserialized round-trips
299 // cleanly, so this `expect` cannot fire on validation paths.
300 transparent::Input::Coinbase { .. } => input
301 .coinbase_script()
302 .expect("coinbase_script reconstructs from a deserialized coinbase input"),
303 })
304 .chain(
305 self.outputs()
306 .iter()
307 .map(|o| o.lock_script.as_raw_bytes().to_vec()),
308 )
309 }
310}
311
312impl Sigops for zebra_chain::transaction::UnminedTx {
313 fn scripts(&self) -> impl Iterator<Item = Vec<u8>> {
314 self.transaction.scripts()
315 }
316}
317
318impl Sigops for CachedFfiTransaction {
319 fn scripts(&self) -> impl Iterator<Item = Vec<u8>> {
320 self.transaction.scripts()
321 }
322}
323
324impl Sigops for zcash_primitives::transaction::Transaction {
325 fn scripts(&self) -> impl Iterator<Item = Vec<u8>> {
326 self.transparent_bundle().into_iter().flat_map(|bundle| {
327 // `zcash_primitives` stores the coinbase input's full serialized scriptSig (height
328 // prefix + extra data) in the synthesized input's script_sig, so it is included as-is
329 // for sigop counting.
330 bundle
331 .vin
332 .iter()
333 .map(|i| i.script_sig().0 .0.clone())
334 .chain(bundle.vout.iter().map(|o| o.script_pubkey().0 .0.clone()))
335 })
336 }
337}
338
339/// Extract the redeem script bytes from a P2SH scriptSig.
340///
341/// Mirrors zcashd's P2SH redeem-script extraction in
342/// [`CScript::GetSigOpCount(const CScript& scriptSig)`].
343///
344/// Iterates the scriptSig opcodes and returns the last successfully pushed data value. Returns
345/// `None` if any opcode fails to parse, OR if any opcode is not a push value (zcashd: `opcode >
346/// OP_16`). This matches zcashd's behavior of returning 0 P2SH sigops for malformed or
347/// non-push-only scriptSigs.
348///
349/// [`CScript::GetSigOpCount(const CScript& scriptSig)`]: https://github.com/zcash/zcash/blob/v6.11.0/src/script/script.cpp#L176-L199
350fn extract_p2sh_redeem_script(unlock_script: &transparent::Script) -> Option<Vec<u8>> {
351 let code = script::Code(unlock_script.as_raw_bytes().to_vec());
352 let mut last_push_data: Option<Vec<u8>> = None;
353 for opcode in code.parse() {
354 match opcode {
355 Ok(PossiblyBad::Good(Opcode::PushValue(pv))) => {
356 last_push_data = Some(pv.value());
357 }
358 // Non-push opcode (operation, control, or bad) or parse error: zcashd returns 0 sigops
359 // in this case. Match that behavior by discarding any data collected so far.
360 _ => return None,
361 }
362 }
363 last_push_data
364}
365
366/// Returns the P2SH sigop count for a single input.
367///
368/// Returns 0 for non-P2SH inputs, coinbase inputs, and P2SH inputs where no redeem script can be
369/// extracted from the scriptSig.
370fn p2sh_input_sigop_count(input: &transparent::Input, spent_output: &transparent::Output) -> u32 {
371 let unlock_script = match input {
372 transparent::Input::PrevOut { unlock_script, .. } => unlock_script,
373 transparent::Input::Coinbase { .. } => return 0,
374 };
375
376 let lock_code = script::Code(spent_output.lock_script.as_raw_bytes().to_vec());
377
378 if !lock_code.is_pay_to_script_hash() {
379 return 0;
380 }
381
382 let Some(redeemed_bytes) = extract_p2sh_redeem_script(unlock_script) else {
383 return 0;
384 };
385
386 script::Code(redeemed_bytes).sig_op_count(true)
387}
388
389/// Returns the total number of P2SH sigops across all inputs of `tx`.
390///
391/// Mirrors zcashd's [`GetP2SHSigOpCount()`].
392///
393/// Coinbase transactions always return zero, matching zcashd's early-return for `tx.IsCoinBase()`.
394/// Callers are therefore permitted to pass an empty `spent_outputs` slice for coinbase transactions
395/// (which is what the block-verifier does, since coinbase inputs have no previous output).
396///
397/// # Correctness
398///
399/// For non-coinbase transactions, `spent_outputs.len()` must equal the number of transparent inputs
400/// in `tx`. If the lengths differ, `zip()` silently truncates the longer iterator, causing an
401/// incorrect (undercount) result.
402///
403/// [`GetP2SHSigOpCount()`]: https://github.com/zcash/zcash/blob/v6.11.0/src/main.cpp#L840-L852
404pub fn p2sh_sigop_count(
405 tx: &zebra_chain::transaction::Transaction,
406 spent_outputs: &[transparent::Output],
407) -> u32 {
408 if tx.is_coinbase() {
409 return 0;
410 }
411
412 debug_assert_eq!(
413 tx.inputs().len(),
414 spent_outputs.len(),
415 "spent_outputs must align with transaction inputs for non-coinbase txs"
416 );
417
418 tx.inputs()
419 .iter()
420 .zip(spent_outputs.iter())
421 .map(|(input, spent_output)| p2sh_input_sigop_count(input, spent_output))
422 .sum()
423}