Skip to content

Commit 9d16e79

Browse files
authored
Provide a Rustier wrapper for zcash_script (#171)
* Move C++ bindings out of lib.rs Have lib.rs re-export them, but make room for the upcoming Rust implementation. * 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). The module structure (interpreter.rs, zcash_script.rs) and locations of definitions are intended to mirror the structure of the C++ code, especially as we get the Rust implementation in place, for easier comparison. That organization is very likely to change once everything has been checked. * Address review feedback Thanks to @nuttycom and @arya2 for getting the closure to work. * Additional cleanup * Use `try_from`/`_into` instead of `as` * Address review feedback * Widen the `Unknown` error type This should fix the Windows build.
1 parent c9d7507 commit 9d16e79

File tree

6 files changed

+432
-92
lines changed

6 files changed

+432
-92
lines changed

Cargo.lock

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ path = "src/lib.rs"
6060
external-secp = []
6161

6262
[dependencies]
63+
bitflags = "2.5"
6364

6465
[build-dependencies]
6566
# The `bindgen` dependency should automatically upgrade to match the version used by zebra-state's `rocksdb` dependency in:

src/cxx.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//! Rust bindings for Zcash transparent scripts.
2+
3+
#![allow(missing_docs)]
4+
#![allow(clippy::needless_lifetimes)]
5+
#![allow(non_upper_case_globals)]
6+
#![allow(non_camel_case_types)]
7+
#![allow(non_snake_case)]
8+
#![allow(unsafe_code)]
9+
#![allow(unused_imports)]
10+
#![allow(clippy::unwrap_or_default)]
11+
12+
// Use the generated C++ bindings
13+
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
14+
#[cfg(test)]
15+
mod tests {
16+
use std::ffi::{c_int, c_uint, c_void};
17+
18+
pub use super::zcash_script_error_t;
19+
use hex::FromHex;
20+
21+
lazy_static::lazy_static! {
22+
pub static ref SCRIPT_PUBKEY: Vec<u8> = <Vec<u8>>::from_hex("a914c117756dcbe144a12a7c33a77cfa81aa5aeeb38187").unwrap();
23+
pub static ref SCRIPT_SIG: Vec<u8> = <Vec<u8>>::from_hex("00483045022100d2ab3e6258fe244fa442cfb38f6cef9ac9a18c54e70b2f508e83fa87e20d040502200eead947521de943831d07a350e45af8e36c2166984a8636f0a8811ff03ed09401473044022013e15d865010c257eef133064ef69a780b4bc7ebe6eda367504e806614f940c3022062fdbc8c2d049f91db2042d6c9771de6f1ef0b3b1fea76c1ab5542e44ed29ed8014c69522103b2cc71d23eb30020a4893982a1e2d352da0d20ee657fa02901c432758909ed8f21029d1e9a9354c0d2aee9ffd0f0cea6c39bbf98c4066cf143115ba2279d0ba7dabe2103e32096b63fd57f3308149d238dcbb24d8d28aad95c0e4e74e3e5e6a11b61bcc453ae").expect("Block bytes are in valid hex representation");
24+
}
25+
26+
extern "C" fn sighash(
27+
sighash_out: *mut u8,
28+
sighash_out_len: c_uint,
29+
ctx: *const c_void,
30+
_script_code: *const u8,
31+
_script_code_len: c_uint,
32+
_hash_type: c_int,
33+
) {
34+
unsafe {
35+
assert!(ctx.is_null());
36+
let sighash =
37+
hex::decode("e8c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c")
38+
.unwrap();
39+
assert!(sighash_out_len == sighash.len() as c_uint);
40+
std::ptr::copy_nonoverlapping(sighash.as_ptr(), sighash_out, sighash.len());
41+
}
42+
}
43+
44+
extern "C" fn invalid_sighash(
45+
sighash_out: *mut u8,
46+
sighash_out_len: c_uint,
47+
ctx: *const c_void,
48+
_script_code: *const u8,
49+
_script_code_len: c_uint,
50+
_hash_type: c_int,
51+
) {
52+
unsafe {
53+
assert!(ctx.is_null());
54+
let sighash =
55+
hex::decode("08c7bdac77f6bb1f3aba2eaa1fada551a9c8b3b5ecd1ef86e6e58a5f1aab952c")
56+
.unwrap();
57+
assert!(sighash_out_len == sighash.len() as c_uint);
58+
std::ptr::copy_nonoverlapping(sighash.as_ptr(), sighash_out, sighash.len());
59+
}
60+
}
61+
62+
#[test]
63+
fn it_works() {
64+
let nLockTime: i64 = 2410374;
65+
let isFinal: u8 = 1;
66+
let script_pub_key = &*SCRIPT_PUBKEY;
67+
let script_sig = &*SCRIPT_SIG;
68+
let flags: c_uint = 513;
69+
let mut err = 0;
70+
71+
let ret = unsafe {
72+
super::zcash_script_verify_callback(
73+
std::ptr::null(),
74+
Some(sighash),
75+
nLockTime,
76+
isFinal,
77+
script_pub_key.as_ptr(),
78+
script_pub_key.len() as c_uint,
79+
script_sig.as_ptr(),
80+
script_sig.len() as c_uint,
81+
flags,
82+
&mut err,
83+
)
84+
};
85+
86+
assert!(ret == 1);
87+
}
88+
89+
#[test]
90+
fn it_fails_on_invalid_sighash() {
91+
let nLockTime: i64 = 2410374;
92+
let isFinal: u8 = 1;
93+
let script_pub_key = &*SCRIPT_PUBKEY;
94+
let script_sig = &*SCRIPT_SIG;
95+
let flags: c_uint = 513;
96+
let mut err = 0;
97+
98+
let ret = unsafe {
99+
super::zcash_script_verify_callback(
100+
std::ptr::null(),
101+
Some(invalid_sighash),
102+
nLockTime,
103+
isFinal,
104+
script_pub_key.as_ptr(),
105+
script_pub_key.len() as c_uint,
106+
script_sig.as_ptr(),
107+
script_sig.len() as c_uint,
108+
flags,
109+
&mut err,
110+
)
111+
};
112+
113+
assert!(ret != 1);
114+
}
115+
}

src/interpreter.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
bitflags::bitflags! {
2+
/// The different SigHash types, as defined in <https://zips.z.cash/zip-0143>
3+
///
4+
/// TODO: This is currently defined as `i32` to match the `c_int` constants in this package, but
5+
/// should use librustzcash’s `u8` constants once we’ve removed the C++.
6+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
7+
pub struct HashType: i32 {
8+
/// Sign all the outputs
9+
const All = 1;
10+
/// Sign none of the outputs - anyone can spend
11+
const None = 2;
12+
/// Sign one of the outputs - anyone can spend the rest
13+
const Single = 3;
14+
/// Anyone can add inputs to this transaction
15+
const AnyoneCanPay = 0x80;
16+
}
17+
}
18+
19+
bitflags::bitflags! {
20+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
21+
/// Script verification flags
22+
pub struct VerificationFlags: u32 {
23+
/// Evaluate P2SH subscripts (softfork safe,
24+
/// [BIP16](https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki).
25+
const P2SH = 1 << 0;
26+
27+
/// Passing a non-strict-DER signature or one with undefined hashtype to a checksig operation causes script failure.
28+
/// Evaluating a pubkey that is not (0x04 + 64 bytes) or (0x02 or 0x03 + 32 bytes) by checksig causes script failure.
29+
/// (softfork safe, but not used or intended as a consensus rule).
30+
const StrictEnc = 1 << 1;
31+
32+
/// Passing a non-strict-DER signature or one with S > order/2 to a checksig operation causes script failure
33+
/// (softfork safe, [BIP62](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 5).
34+
const LowS = 1 << 3;
35+
36+
/// 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).
37+
const NullDummy = 1 << 4;
38+
39+
/// 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).
40+
const SigPushOnly = 1 << 5;
41+
42+
/// Require minimal encodings for all push operations (OP_0... OP_16, OP_1NEGATE where possible, direct
43+
/// pushes up to 75 bytes, OP_PUSHDATA up to 255 bytes, OP_PUSHDATA2 for anything larger). Evaluating
44+
/// any other push causes the script to fail ([BIP62](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 3).
45+
/// 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).
46+
/// (softfork safe)
47+
const MinimalData = 1 << 6;
48+
49+
/// Discourage use of NOPs reserved for upgrades (NOP1-10)
50+
///
51+
/// Provided so that nodes can avoid accepting or mining transactions
52+
/// containing executed NOP's whose meaning may change after a soft-fork,
53+
/// thus rendering the script invalid; with this flag set executing
54+
/// discouraged NOPs fails the script. This verification flag will never be
55+
/// a mandatory flag applied to scripts in a block. NOPs that are not
56+
/// executed, e.g. within an unexecuted IF ENDIF block, are *not* rejected.
57+
const DiscourageUpgradableNOPs = 1 << 7;
58+
59+
/// Require that only a single stack element remains after evaluation. This changes the success criterion from
60+
/// "At least one stack element must remain, and when interpreted as a boolean, it must be true" to
61+
/// "Exactly one stack element must remain, and when interpreted as a boolean, it must be true".
62+
/// (softfork safe, [BIP62](https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki) rule 6)
63+
/// Note: CLEANSTACK should never be used without P2SH.
64+
const CleanStack = 1 << 8;
65+
66+
/// Verify CHECKLOCKTIMEVERIFY
67+
///
68+
/// See [BIP65](https://github.com/bitcoin/bips/blob/master/bip-0065.mediawiki) for details.
69+
const CHECKLOCKTIMEVERIFY = 1 << 9;
70+
}
71+
}

0 commit comments

Comments
 (0)