#![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")]
#![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,
};
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
ScriptInvalid,
TxIndex,
TxCoinbase,
Unknown(libzcash_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)
}
}
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,
}
}
#[derive(Debug)]
pub struct CachedFfiTransaction {
transaction: Arc<zebra_chain::transaction::Transaction>,
all_previous_outputs: Arc<Vec<transparent::Output>>,
sighasher: SigHasher,
}
impl CachedFfiTransaction {
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,
})
}
pub fn inputs(&self) -> &[transparent::Input] {
self.transaction.inputs()
}
pub fn all_previous_outputs(&self) -> &Vec<transparent::Output> {
&self.all_previous_outputs
}
pub fn sighasher(&self) -> &SigHasher {
&self.sighasher
}
pub fn p2sh_sigops(&self) -> u32 {
p2sh_sigop_count(&self.transaction, &self.all_previous_outputs)
}
#[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| {
let computed: Option<[u8; 32]> = (|| {
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;
}
}
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();
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,
)
})();
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)
}
})
}
}
pub trait Sigops {
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)
})?)
}
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()
}
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| {
bundle
.vin
.iter()
.map(|i| i.script_sig().0 .0.clone())
.chain(bundle.vout.iter().map(|o| o.script_pubkey().0 .0.clone()))
})
}
}
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());
}
_ => return None,
}
}
last_push_data
}
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)
}
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()
}