-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Provide a Rustier wrapper for zcash_script #171
Changes from 6 commits
59642ca
b55a7f8
c6ff3fc
52927c9
f526f69
eca9469
f16ce84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
bitflags::bitflags! { | ||
/// The different SigHash types, as defined in <https://zips.z.cash/zip-0143> | ||
/// | ||
/// TODO: This is currently defined as `i32` to match the `c_int` constants in this package, but | ||
/// should use librustzcash’s `u8` constants once we’ve removed the C++. | ||
#[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 = 3; | ||
/// Anyone can add inputs to this transaction | ||
const AnyoneCanPay = 0x80; | ||
} | ||
} | ||
|
||
bitflags::bitflags! { | ||
#[derive(Copy, Clone, Debug, PartialEq, Eq)] | ||
/// Script verification flags | ||
pub struct VerificationFlags: u32 { | ||
sellout marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// Evaluate P2SH subscripts (softfork safe, | ||
/// [BIP16](https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki). | ||
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](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 5). | ||
const LowS = 1 << 3; | ||
|
||
/// verify dummy stack item consumed by CHECKMULTISIG is of zero-length (softfork safe, [BIP62](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 7). | ||
const NullDummy = 1 << 4; | ||
|
||
/// Using a non-push operator in the scriptSig causes script failure (softfork safe, [BIP62](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) 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](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 3). | ||
/// In addition, whenever a stack element is interpreted as a number, it must be of minimal length ([BIP62](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) 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](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 6) | ||
/// Note: CLEANSTACK should never be used without P2SH. | ||
const CleanStack = 1 << 8; | ||
|
||
/// Verify CHECKLOCKTIMEVERIFY | ||
/// | ||
/// See [BIP65](https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki) for details. | ||
const CHECKLOCKTIMEVERIFY = 1 << 9; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,201 @@ | ||
//! Zcash transparent script implementations. | ||
|
||
#![doc(html_logo_url = "https://www.zfnd.org/images/zebra-icon.png")] | ||
#![doc(html_root_url = "https://docs.rs/zcash_script/0.2.0")] | ||
#![doc(html_root_url = "https://docs.rs/zcash_script/0.3.0")] | ||
#![allow(unsafe_code)] | ||
|
||
mod cxx; | ||
pub use cxx::*; | ||
|
||
mod interpreter; | ||
pub use interpreter::{HashType, VerificationFlags}; | ||
mod zcash_script; | ||
pub use zcash_script::*; | ||
|
||
use std::os::raw::{c_int, c_uint, c_void}; | ||
|
||
/// A tag to indicate that the C++ implementation of zcash_script should be used. | ||
pub enum Cxx {} | ||
|
||
impl From<zcash_script_error_t> 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.into()), | ||
} | ||
} | ||
} | ||
|
||
/// The sighash callback to use with zcash_script. | ||
extern "C" fn sighash_callback( | ||
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, | ||
) { | ||
let checked_script_code_len = usize::try_from(script_code_len) | ||
.expect("This was converted from a `usize` in the first place"); | ||
// SAFETY: `script_code` is created from a Rust slice in `verify_callback`, passed through the | ||
// C++ code, eventually to `CallbackTransactionSignatureChecker::CheckSig`, which calls this | ||
// function. | ||
let script_code_vec = | ||
unsafe { std::slice::from_raw_parts(script_code, checked_script_code_len) }; | ||
let ctx = ctx as *const SighashCalculator; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be fine for now, but we definitely will want to improve how the callbacks are handled. I don't have a good sense of how the lifetimes are being passed around. |
||
// SAFETY: `ctx` is a valid `SighashCalculator` passed to `verify_callback` which forwards it to | ||
// the `CallbackTransactionSignatureChecker`. | ||
if let Some(sighash) = unsafe { *ctx }(script_code_vec, HashType::from_bits_retain(hash_type)) { | ||
assert_eq!(sighash_out_len, sighash.len().try_into().unwrap()); | ||
// SAFETY: `sighash_out` is a valid buffer created in | ||
// `CallbackTransactionSignatureChecker::CheckSig`. | ||
unsafe { std::ptr::copy_nonoverlapping(sighash.as_ptr(), sighash_out, sighash.len()) }; | ||
} | ||
} | ||
|
||
/// This steals a bit of the wrapper code from zebra_script, to provide the API that they want. | ||
impl ZcashScript for Cxx { | ||
fn verify_callback( | ||
sighash: SighashCalculator, | ||
lock_time: i64, | ||
is_final: bool, | ||
script_pub_key: &[u8], | ||
signature_script: &[u8], | ||
flags: VerificationFlags, | ||
) -> Result<(), Error> { | ||
let mut err = 0; | ||
|
||
// SAFETY: The `script` fields are created from a valid Rust `slice`. | ||
let ret = unsafe { | ||
zcash_script_verify_callback( | ||
(&sighash as *const SighashCalculator) as *const c_void, | ||
sellout marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Some(sighash_callback), | ||
lock_time, | ||
if is_final { 1 } else { 0 }, | ||
script_pub_key.as_ptr(), | ||
script_pub_key | ||
.len() | ||
.try_into() | ||
.map_err(Error::InvalidScriptSize)?, | ||
signature_script.as_ptr(), | ||
signature_script | ||
.len() | ||
.try_into() | ||
.map_err(Error::InvalidScriptSize)?, | ||
flags.bits(), | ||
&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]) -> Result<u32, Error> { | ||
script | ||
.len() | ||
.try_into() | ||
.map_err(Error::InvalidScriptSize) | ||
.map(|script_len| unsafe { | ||
zcash_script_legacy_sigop_count_script(script.as_ptr(), script_len) | ||
}) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
pub use super::*; | ||
use hex::FromHex; | ||
|
||
lazy_static::lazy_static! { | ||
pub static ref SCRIPT_PUBKEY: Vec<u8> = <Vec<u8>>::from_hex("a914c117756dcbe144a12a7c33a77cfa81aa5aeeb38187").unwrap(); | ||
pub static ref SCRIPT_SIG: Vec<u8> = <Vec<u8>>::from_hex("00483045022100d2ab3e6258fe244fa442cfb38f6cef9ac9a18c54e70b2f508e83fa87e20d040502200eead947521de943831d07a350e45af8e36c2166984a8636f0a8811ff03ed09401473044022013e15d865010c257eef133064ef69a780b4bc7ebe6eda367504e806614f940c3022062fdbc8c2d049f91db2042d6c9771de6f1ef0b3b1fea76c1ab5542e44ed29ed8014c69522103b2cc71d23eb30020a4893982a1e2d352da0d20ee657fa02901c432758909ed8f21029d1e9a9354c0d2aee9ffd0f0cea6c39bbf98c4066cf143115ba2279d0ba7dabe2103e32096b63fd57f3308149d238dcbb24d8d28aad95c0e4e74e3e5e6a11b61bcc453ae").expect("Block bytes are in valid hex representation"); | ||
} | ||
|
||
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 it_works() { | ||
let n_lock_time: i64 = 2410374; | ||
let is_final: 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, | ||
n_lock_time, | ||
is_final, | ||
script_pub_key, | ||
script_sig, | ||
flags, | ||
); | ||
|
||
assert!(ret.is_ok()); | ||
} | ||
|
||
#[test] | ||
fn it_fails_on_invalid_sighash() { | ||
let n_lock_time: i64 = 2410374; | ||
let is_final: 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, | ||
n_lock_time, | ||
is_final, | ||
script_pub_key, | ||
script_sig, | ||
flags, | ||
); | ||
|
||
assert_eq!(ret, Err(Error::Ok)); | ||
} | ||
|
||
#[test] | ||
fn it_fails_on_missing_sighash() { | ||
let n_lock_time: i64 = 2410374; | ||
let is_final: 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, | ||
n_lock_time, | ||
is_final, | ||
script_pub_key, | ||
script_sig, | ||
flags, | ||
); | ||
|
||
assert_eq!(ret, Err(Error::Ok)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,68 @@ | ||||||||||
use std::num::TryFromIntError; | ||||||||||
|
||||||||||
use super::interpreter::*; | ||||||||||
|
||||||||||
/// 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 { | ||||||||||
/// Any failure that results in the script being invalid. | ||||||||||
Ok = 0, | ||||||||||
/// An exception was caught. | ||||||||||
VerifyScript = 7, | ||||||||||
/// The script size can’t fit in a `u32`, as required by the C++ code. | ||||||||||
InvalidScriptSize(TryFromIntError), | ||||||||||
/// Some other failure value recovered from C++. | ||||||||||
/// | ||||||||||
/// __NB__: Linux uses `u32` for the underlying C++ enum while Windows uses `i32`, so `i64` can | ||||||||||
/// hold either. | ||||||||||
Unknown(i64), | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The #[repr(C)]
union ErrorRepr {
Ok: ErrorVariantOk,
VerifyScript: ErrorVariantVerifyScript,
InvalidScriptSize: ErrorVariantInvalidScriptSize,
Unknown: ErrorVariantUnknown,
}
#[repr(u32)]
enum ErrorTag {
Ok = 0,
VerifyScript = 7,
InvalidScriptSize, // Implicit = 8
Unknown, // Implicit = 9
}
#[repr(C)]
struct ErrorVariantOk(ErrorTag);
#[repr(C)]
struct ErrorVariantVerifyScript(ErrorTag);
#[repr(C)]
struct ErrorVariantInvalidScriptSize(ErrorTag, TryFromIntError);
#[repr(C)]
struct ErrorVariantUnknown(ErrorTag, i64); It is therefore unsuitable for passing across an FFI, which is the only reason we'd want to add
Remove the |
||||||||||
} | ||||||||||
|
||||||||||
/// All signature hashes are 32 bits, since they are necessarily produced by SHA256. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We've not used SHA-256 for sighashes since Overwinter. See ZIPs 143, 243, and 244.
Suggested change
|
||||||||||
pub const SIGHASH_SIZE: usize = 32; | ||||||||||
|
||||||||||
/// 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 `extern "C"` function that calls this 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? | ||||||||||
sellout marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
pub type SighashCalculator<'a> = &'a dyn Fn(&[u8], HashType) -> Option<[u8; SIGHASH_SIZE]>; | ||||||||||
|
||||||||||
/// The external API of zcash_script. This is defined to make it possible to compare the C++ and | ||||||||||
/// Rust implementations. | ||||||||||
pub trait ZcashScript { | ||||||||||
/// 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. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This field doesn't appear anywhere in the trait method. Is it meant to refer to |
||||||||||
/// - 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. | ||||||||||
/// | ||||||||||
/// Note that script verification failure is indicated by `Err(Error::Ok)`. | ||||||||||
fn verify_callback( | ||||||||||
sighash: SighashCalculator, | ||||||||||
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]) -> Result<u32, Error>; | ||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's incorrect to represent
HashType
as purely bitflags, because the lower bits are not valid to interpret as flags. That is,HashType::All | HashType::None
should not be considered equal toHashType::Single
, but that is what this type enables.It's not immediately obvious to me where the boundary between "upper bitflags" and "lower type number" should be, but that should be figured out at some point.