From 3c353bd61e7771fd06ecfcb7da92ce7069c60ae3 Mon Sep 17 00:00:00 2001 From: Greg Pfeil Date: Wed, 7 Aug 2024 23:11:09 -0600 Subject: [PATCH] Provide a Rustier wrapper for zcash_script This adds a `Script` trait that exposes slightly Rustier types in order to have a common interface for the existing C++ implementation as well as the upcoming Rust implementation (and a third instance that runs both and checks that the Rust result matches the C++ one). --- Cargo.lock | 11 +-- Cargo.toml | 3 +- src/api.rs | 130 +++++++++++++++++++++++++++++++++++ src/lib.rs | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 327 insertions(+), 13 deletions(-) create mode 100644 src/api.rs diff --git a/Cargo.lock b/Cargo.lock index 3a94d6378..7f491493d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,9 +36,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cc" @@ -79,9 +79,9 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "errno" @@ -412,9 +412,10 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "zcash_script" -version = "0.2.0" +version = "0.3.0" dependencies = [ "bindgen", + "bitflags", "cc", "hex", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 3bf442428..a36e3ee26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zcash_script" -version = "0.2.0" +version = "0.3.0" authors = ["Tamas Blummer ", "Zcash Foundation "] license = "Apache-2.0" readme = "README.md" @@ -60,6 +60,7 @@ path = "src/lib.rs" external-secp = [] [dependencies] +bitflags = "2.5.0" [build-dependencies] # The `bindgen` dependency should automatically upgrade to match the version used by zebra-state's `rocksdb` dependency in: diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 000000000..94f823bfd --- /dev/null +++ b/src/api.rs @@ -0,0 +1,130 @@ +/// This maps to `zcash_script_error_t`, but most of those cases aren’t used any more. This only +/// replicates the still-used cases, and then an `Unknown` bucket for anything else that might +/// happen. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u32)] +pub enum Error { + Ok = 0, + VerifyScript = 7, + Unknown(u32), +} + +bitflags::bitflags! { + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct VerificationFlags: u32 { + const None = 0; + + /// Evaluate P2SH subscripts (softfork safe, BIP16). + const P2SH = 1 << 0; + + /// Passing a non-strict-DER signature or one with undefined hashtype to a checksig operation causes script failure. + /// Evaluating a pubkey that is not (0x04 + 64 bytes) or (0x02 or 0x03 + 32 bytes) by checksig causes script failure. + /// (softfork safe, but not used or intended as a consensus rule). + const StrictEnc = 1 << 1; + + /// Passing a non-strict-DER signature or one with S > order/2 to a checksig operation causes script failure + /// (softfork safe, BIP62 rule 5). + const LowS = 1 << 3; + + /// verify dummy stack item consumed by CHECKMULTISIG is of zero-length (softfork safe, BIP62 rule 7). + const NullDummy = 1 << 4; + + /// Using a non-push operator in the scriptSig causes script failure (softfork safe, BIP62 rule 2). + const SigPushOnly = 1 << 5; + + /// Require minimal encodings for all push operations (OP_0... OP_16, OP_1NEGATE where possible, direct + /// pushes up to 75 bytes, OP_PUSHDATA up to 255 bytes, OP_PUSHDATA2 for anything larger). Evaluating + /// any other push causes the script to fail (BIP62 rule 3). + /// In addition, whenever a stack element is interpreted as a number, it must be of minimal length (BIP62 rule 4). + /// (softfork safe) + const MinimalData = 1 << 6; + + /// Discourage use of NOPs reserved for upgrades (NOP1-10) + /// + /// Provided so that nodes can avoid accepting or mining transactions + /// containing executed NOP's whose meaning may change after a soft-fork, + /// thus rendering the script invalid; with this flag set executing + /// discouraged NOPs fails the script. This verification flag will never be + /// a mandatory flag applied to scripts in a block. NOPs that are not + /// executed, e.g. within an unexecuted IF ENDIF block, are *not* rejected. + const DiscourageUpgradableNOPs = 1 << 7; + + /// Require that only a single stack element remains after evaluation. This changes the success criterion from + /// "At least one stack element must remain, and when interpreted as a boolean, it must be true" to + /// "Exactly one stack element must remain, and when interpreted as a boolean, it must be true". + /// (softfork safe, BIP62 rule 6) + /// Note: CLEANSTACK should never be used without P2SH. + const CleanStack = 1 << 8; + + /// Verify CHECKLOCKTIMEVERIFY + /// + /// See BIP65 for details. + const CHECKLOCKTIMEVERIFY = 1 << 9; + } +} + +bitflags::bitflags! { + /// The different SigHash types, as defined in + /// + /// TODO: There are three implementations of this (with three distinct primitive types): + /// - u8 constants in librustzcash, + /// - i32 (well, c_int) bitflags from the C++ constants here, and + /// - u32 bitflags in zebra-chain. + /// + /// Ideally we could unify on bitflags in librustzcash. + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct HashType: i32 { + /// Sign all the outputs + const All = 1; + /// Sign none of the outputs - anyone can spend + const None = 2; + /// Sign one of the outputs - anyone can spend the rest + const Single = Self::All.bits() | Self::None.bits(); + /// Anyone can add inputs to this transaction + const AnyoneCanPay = 0x80; + } +} + +/// A function which is called to obtain the sighash. +/// - script_code: the scriptCode being validated. Note that this not always +/// matches script_sig, i.e. for P2SH. +/// - hash_type: the hash type being used. +/// +/// The underlying C++ callback doesn’t give much opportunity for rich failure reporting, but +/// returning `None` indicates _some_ failure to produce the desired hash. +/// +/// TODO: Can we get the “32” from somewhere rather than hardcoding it? +pub type SighashCallback = dyn Fn(&[u8], HashType) -> Option<[u8; 32]>; + +/// The external API of zcash_script. This is defined to make it possible to compare the C++ and +/// Rust implementations. +pub trait Script { + /// Returns `Ok(())` if the a transparent input correctly spends the matching output + /// under the additional constraints specified by `flags`. This function + /// receives only the required information to validate the spend and not + /// the transaction itself. In particular, the sighash for the spend + /// is obtained using a callback function. + /// + /// - sighash_callback: a callback function which is called to obtain the sighash. + /// - n_lock_time: the lock time of the transaction being validated. + /// - is_final: a boolean indicating whether the input being validated is final + /// (i.e. its sequence number is 0xFFFFFFFF). + /// - script_pub_key: the scriptPubKey of the output being spent. + /// - script_sig: the scriptSig of the input being validated. + /// - flags: the script verification flags to use. + /// - err: if not NULL, err will contain an error/success code for the operation. + /// + /// Note that script verification failure is indicated by `Err(Error::Ok)`. + fn verify_callback( + sighash_callback: &SighashCallback, + n_lock_time: i64, + is_final: bool, + script_pub_key: &[u8], + script_sig: &[u8], + flags: VerificationFlags, + ) -> Result<(), Error>; + + /// Returns the number of transparent signature operations in the input or + /// output script pointed to by script. + fn legacy_sigop_count_script(script: &[u8]) -> u32; +} diff --git a/src/lib.rs b/src/lib.rs index bcb36f928..f921b95ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,11 +13,115 @@ // Use the generated C++ bindings include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + +pub mod api; +use api::*; +use std::os::raw::{c_int, c_uint, c_void}; + +impl From for Error { + #[allow(non_upper_case_globals)] + fn from(err_code: zcash_script_error_t) -> Error { + match err_code { + zcash_script_error_t_zcash_script_ERR_OK => Error::Ok, + zcash_script_error_t_zcash_script_ERR_VERIFY_SCRIPT => Error::VerifyScript, + unknown => Error::Unknown(unknown), + } + } +} + +/// The sighash callback to use with zcash_script. +extern "C" fn sighash( + sighash_out: *mut u8, + sighash_out_len: c_uint, + ctx: *const c_void, + script_code: *const u8, + script_code_len: c_uint, + hash_type: c_int, +) { + // SAFETY: `ctx` is a valid SighashCallbackt because it is always passed to + // `verify_callback` which simply forwards it to the callback. + // `script_code` and `sighash_out` are valid buffers since they are always + // specified when the callback is called. + unsafe { + let ctx = ctx as *const &SighashCallback; + let script_code_vec = + std::slice::from_raw_parts(script_code, script_code_len as usize); + let sighash = (*ctx)( + &script_code_vec, + HashType::from_bits(hash_type).expect("was built from HashType") + ); + sighash.map(|sighash| { + // Sanity check; must always be true. + assert_eq!(sighash_out_len, sighash.len() as c_uint); + std::ptr::copy_nonoverlapping(sighash.as_ptr(), sighash_out, sighash.len()); + }); + } +} + +pub enum Cxx {} + +/// This steals a bit of the wrapper code from zebra_script, to provide the API that they want. +impl Script for Cxx { + fn verify_callback( + sighash_callback: &SighashCallback, + lock_time: i64, + is_final: bool, + script_pub_key: &[u8], + signature_script: &[u8], + flags: VerificationFlags, + ) -> Result<(), Error> { + let mut err = 0; + + let flags = flags.bits(); + + let is_final = if is_final { + 1 + } else { + 0 + }; + + let ctx = Box::new(sighash_callback); + // SAFETY: The `script_*` fields are created from a valid Rust `slice`. + let ret = unsafe { + zcash_script_verify_callback( + (&*ctx as *const &SighashCallback) as *const c_void, + Some(sighash), + lock_time, + is_final, + script_pub_key.as_ptr(), + script_pub_key.len() as u32, + signature_script.as_ptr(), + signature_script.len() as u32, + flags, + &mut err, + ) + }; + + if ret == 1 { + Ok(()) + } else { + Err(Error::from(err)) + } + } + + /// Returns the number of transparent signature operations in the + /// transparent inputs and outputs of this transaction. + fn legacy_sigop_count_script(script: &[u8]) -> u32 { + unsafe { + zcash_script_legacy_sigop_count_script( + script.as_ptr(), + script.len() as u32, + ) + } + } +} + #[cfg(test)] mod tests { use std::ffi::{c_int, c_uint, c_void}; - pub use super::zcash_script_error_t; + pub use super::api::*; + pub use super::{Cxx, zcash_script_error_t}; use hex::FromHex; lazy_static::lazy_static! { @@ -25,7 +129,7 @@ mod tests { pub static ref SCRIPT_SIG: Vec = >::from_hex("00483045022100d2ab3e6258fe244fa442cfb38f6cef9ac9a18c54e70b2f508e83fa87e20d040502200eead947521de943831d07a350e45af8e36c2166984a8636f0a8811ff03ed09401473044022013e15d865010c257eef133064ef69a780b4bc7ebe6eda367504e806614f940c3022062fdbc8c2d049f91db2042d6c9771de6f1ef0b3b1fea76c1ab5542e44ed29ed8014c69522103b2cc71d23eb30020a4893982a1e2d352da0d20ee657fa02901c432758909ed8f21029d1e9a9354c0d2aee9ffd0f0cea6c39bbf98c4066cf143115ba2279d0ba7dabe2103e32096b63fd57f3308149d238dcbb24d8d28aad95c0e4e74e3e5e6a11b61bcc453ae").expect("Block bytes are in valid hex representation"); } - extern "C" fn sighash( + extern "C" fn extern_sighash( sighash_out: *mut u8, sighash_out_len: c_uint, ctx: *const c_void, @@ -43,7 +147,7 @@ mod tests { } } - extern "C" fn invalid_sighash( + extern "C" fn invalid_extern_sighash( sighash_out: *mut u8, sighash_out_len: c_uint, ctx: *const c_void, @@ -62,7 +166,7 @@ mod tests { } #[test] - fn it_works() { + fn bindgen_works() { let nLockTime: i64 = 2410374; let isFinal: u8 = 1; let script_pub_key = &*SCRIPT_PUBKEY; @@ -73,7 +177,7 @@ mod tests { let ret = unsafe { super::zcash_script_verify_callback( std::ptr::null(), - Some(sighash), + Some(extern_sighash), nLockTime, isFinal, script_pub_key.as_ptr(), @@ -89,7 +193,7 @@ mod tests { } #[test] - fn it_fails_on_invalid_sighash() { + fn bindgen_fails_on_invalid_sighash() { let nLockTime: i64 = 2410374; let isFinal: u8 = 1; let script_pub_key = &*SCRIPT_PUBKEY; @@ -100,7 +204,7 @@ mod tests { let ret = unsafe { super::zcash_script_verify_callback( std::ptr::null(), - Some(invalid_sighash), + Some(invalid_extern_sighash), nLockTime, isFinal, script_pub_key.as_ptr(), @@ -114,4 +218,82 @@ mod tests { assert!(ret != 1); } + + fn sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> { + hex::decode("e8c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c") + .unwrap() + .as_slice() + .first_chunk::<32>() + .map(|hash| *hash) + } + + fn invalid_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> { + hex::decode("08c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c") + .unwrap() + .as_slice() + .first_chunk::<32>() + .map(|hash| *hash) + } + + fn missing_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> { None } + + #[test] + fn cxx_trait_works() { + let nLockTime: i64 = 2410374; + let isFinal: bool = true; + let script_pub_key = &SCRIPT_PUBKEY; + let script_sig = &SCRIPT_SIG; + let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY; + + let ret = Cxx::verify_callback( + &sighash, + nLockTime, + isFinal, + script_pub_key, + script_sig, + flags, + ); + + assert!(ret.is_ok()); + } + + #[test] + fn cxx_trait_fails_on_invalid_sighash() { + let nLockTime: i64 = 2410374; + let isFinal: bool = true; + let script_pub_key = &SCRIPT_PUBKEY; + let script_sig = &SCRIPT_SIG; + let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY; + + let ret = Cxx::verify_callback( + &invalid_sighash, + nLockTime, + isFinal, + script_pub_key, + script_sig, + flags, + ); + + assert_eq!(ret, Err(Error::Ok)); + } + + #[test] + fn cxx_trait_fails_on_missing_sighash() { + let nLockTime: i64 = 2410374; + let isFinal: bool = true; + let script_pub_key = &SCRIPT_PUBKEY; + let script_sig = &SCRIPT_SIG; + let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY; + + let ret = Cxx::verify_callback( + &missing_sighash, + nLockTime, + isFinal, + script_pub_key, + script_sig, + flags, + ); + + assert_eq!(ret, Err(Error::Ok)); + } }