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}