Initial Rust implementation (#174)

* [DRAFT]

* Rearrange Rust impl to match C++ more closely

The only changes here other than moving chunks of code around are
- moving `evaluate` out of `impl Script`, which required changing
 `&self` to `script: &Script`; and
- unifying `ExecutionOptions` with `VerificationFlags`.

* Rename Rust identifiers to match C++

For easier side-by-side comparison.

* Connected the new API, but fails

* Existing unit tests succeed

* The rest of the owl

* Reverting to C++ style, and some other changes

* Appease Clippy

* Replace `ScriptNum` panics with error case

The C++ impl uses exceptions for `ScriptNum`, but catches them.

* Add some shallow property tests

These tests run both the C++ and Rust impls. One uses completely
arbitrary inputs, and the other limits script_sig to data pushes.

* Add shallow fuzz testing

* Preserve richer errors on the Rust side

For now, the underlying errors are discarded when comparing against the
C++ results, but there are corresponding changes on the C++ side in a
separate branch.

* Address @nuttycom’s review comments

- remove `uint256` module
- create a specific type for the C++/Rust comparison implementation
- rename some identifiers
- rephrase some comments

* Some changes to ease zebrad integration

- Switch from `log` to `tracing` for `warn!`,
- Export `SignedOutputs`, which is needed to create a `HashType`, and
- Upgrade zcash_primitives to the same version used by zebrad.

* Appease Clippy

* Remove dependency on zcash_primitives

This was only needed for the `TxVersion` enum. However, the `StrictEnc`
flag is a proxy for the v5 tx requirements, so instead of checking the
`TxVersion` explicitly, we expect callers to include `StrictEnc` for
verification of v5 transactions.

* Moving testing dependencies

libfuzzer-sys is Linux-specific, and it & proptest are only used for tests.

* Normalize Rust errors in comparison interpreter

This was a minor oversight, but this correction should only eliminate
false mismatches.

* Address @nuttycom’s PR feedback

* Eliminate a `panic!`

This `panic!` appears to be unreachable in the current implementation, but there is no need for it.
It doesn’t introduce any new failure cases.

Thanks to @conradoplg for noticing it.

* Use (`Try`)`From` for `ScriptNum` conversions

This also makes the `ScriptNum` field private so that `bn.0` can’t
extract the unconstrained `i64` value.

* Remove some `From` instances that do too much

* Subtract from `OP_RESERVED` instead of `OP_1 - 1`

`OP_RESERVED` is in the ‘0’ offset position of the `OP_n` opcodes. Just
use this even though it isn’t obviously a number to improve readability.

---------

Co-authored-by: Sean Bowe <ewillbefull@gmail.com>
Co-authored-by: Conrado Gouvea <conradoplg@gmail.com>
This commit is contained in:
Greg Pfeil 2025-01-29 15:31:52 -07:00 committed by GitHub
parent 4edd9009a2
commit 335ae9a2a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 2634 additions and 849 deletions

View File

@ -123,3 +123,24 @@ jobs:
with:
command: clippy
args: -- -D warnings
fuzz:
name: Fuzz
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- nightly
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- run: cargo install cargo-fuzz
- uses: actions-rs/cargo@v1
with:
command: fuzz
args: run compare -- -max_len=20000 -max_total_time=100

1107
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@ include = [
"/README.md",
"build.rs",
"src/*.rs",
"src/*/*.rs",
"/depend/check_uint128_t.c",
"/depend/zcash/src/amount.cpp",
"/depend/zcash/src/amount.h",
@ -58,10 +59,16 @@ path = "src/lib.rs"
[features]
external-secp = []
test-dependencies = []
[dependencies]
bitflags = "2.5"
zcash_primitives = "0.17"
enum_primitive = "0.1"
ripemd = "0.1"
secp256k1 = "0.29"
sha-1 = "0.10"
sha2 = "0.10"
tracing = "0.1.39"
[build-dependencies]
# The `bindgen` dependency should automatically upgrade to match the version used by zebra-state's `rocksdb` dependency in:
@ -82,6 +89,10 @@ cc = { version = "1.1.10", features = ["parallel"] }
# Treat minor versions with a zero major version as compatible (cargo doesn't by default).
hex = ">= 0.4.3"
lazy_static = "1.5.0"
proptest = "0.9"
[target.'cfg(linux)'.dev-dependencies]
libfuzzer-sys = "0.4"
[[package.metadata.release.pre-release-replacements]]
file = "CHANGELOG.md"

4
fuzz/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
target
corpus
artifacts
coverage

19
fuzz/Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "zcash_script-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
zcash_script = { path = "..", features = ["test-dependencies"] }
[[bin]]
name = "compare"
path = "fuzz_targets/compare.rs"
test = false
doc = false
bench = false

View File

@ -0,0 +1,29 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
extern crate zcash_script;
use zcash_script::*;
fn missing_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> {
None
}
fuzz_target!(|tup: (i64, bool, &[u8], &[u8], u32)| {
// `fuzz_target!` doesnt support pattern matching in the parameter list.
let (lock_time, is_final, pub_key, sig, flags) = tup;
let ret = check_verify_callback::<CxxInterpreter, RustInterpreter>(
&missing_sighash,
lock_time,
is_final,
pub_key,
sig,
testing::repair_flags(VerificationFlags::from_bits_truncate(flags)),
);
assert_eq!(
ret.0,
ret.1.map_err(normalize_error),
"original Rust result: {:?}",
ret.1
);
});

3
src/external/mod.rs vendored Normal file
View File

@ -0,0 +1,3 @@
//! Modules that parallel the C++ implementation, but which live outside the script directory.
pub mod pubkey;

56
src/external/pubkey.rs vendored Normal file
View File

@ -0,0 +1,56 @@
use secp256k1::{ecdsa, Message, PublicKey, Secp256k1};
/// FIXME: `PUBLIC_KEY_SIZE` is meant to be an upper bound, it seems. Maybe parameterize the type
/// over the size.
pub struct PubKey<'a>(pub &'a [u8]);
impl PubKey<'_> {
pub const PUBLIC_KEY_SIZE: usize = 65;
pub const COMPRESSED_PUBLIC_KEY_SIZE: usize = 33;
/// Check syntactic correctness.
///
/// Note that this is consensus critical as CheckSig() calls it!
pub fn is_valid(&self) -> bool {
!self.0.is_empty()
}
/// Verify a DER signature (~72 bytes).
/// If this public key is not fully valid, the return value will be false.
pub fn verify(&self, hash: &[u8; 32], vch_sig: &[u8]) -> bool {
if !self.is_valid() {
return false;
};
if let Ok(pubkey) = PublicKey::from_slice(self.0) {
// let sig: secp256k1_ecdsa_signature;
if vch_sig.is_empty() {
return false;
};
// Zcash, unlike Bitcoin, has always enforced strict DER signatures.
if let Ok(mut sig) = ecdsa::Signature::from_der(vch_sig) {
// libsecp256k1's ECDSA verification requires lower-S signatures, which have
// not historically been enforced in Bitcoin or Zcash, so normalize them first.
sig.normalize_s();
let secp = Secp256k1::verification_only();
secp.verify_ecdsa(&Message::from_digest(*hash), &sig, &pubkey)
.is_ok()
} else {
false
}
} else {
false
}
}
pub fn check_low_s(vch_sig: &[u8]) -> bool {
/* Zcash, unlike Bitcoin, has always enforced strict DER signatures. */
if let Ok(sig) = ecdsa::Signature::from_der(vch_sig) {
let mut check = sig;
check.normalize_s();
sig == check
} else {
false
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -3,27 +3,32 @@
#![doc(html_logo_url = "https://www.zfnd.org/images/zebra-icon.png")]
#![doc(html_root_url = "https://docs.rs/zcash_script/0.3.0")]
#![allow(unsafe_code)]
#[macro_use]
extern crate enum_primitive;
mod cxx;
pub use cxx::*;
mod external;
mod interpreter;
pub use interpreter::{HashType, VerificationFlags};
mod script;
pub mod script_error;
mod zcash_script;
pub use zcash_script::*;
use std::os::raw::{c_int, c_uint, c_void};
use zcash_primitives::transaction::TxVersion;
use tracing::warn;
pub use cxx::*;
pub use interpreter::{HashType, SighashCalculator, SignedOutputs, VerificationFlags};
pub use zcash_script::*;
/// A tag to indicate that the C++ implementation of zcash_script should be used.
pub enum Cxx {}
pub enum CxxInterpreter {}
impl From<zcash_script_error_t> for Error {
#[allow(non_upper_case_globals)]
fn from(err_code: zcash_script_error_t) -> Error {
fn from(err_code: zcash_script_error_t) -> Self {
match err_code {
zcash_script_error_t_zcash_script_ERR_OK => Error::Ok,
zcash_script_error_t_zcash_script_ERR_OK => Error::Ok(None),
zcash_script_error_t_zcash_script_ERR_VERIFY_SCRIPT => Error::VerifyScript,
unknown => Error::Unknown(unknown.into()),
}
@ -46,10 +51,11 @@ extern "C" fn sighash_callback(
// function.
let script_code_vec =
unsafe { std::slice::from_raw_parts(script_code, checked_script_code_len) };
// SAFETY: `ctx` is a valid `(SighashCalculator, TxVersion)` constructed in `verify_callback`
// SAFETY: `ctx` is a valid `SighashCalculator` constructed in `verify_callback`
// which forwards it to the `CallbackTransactionSignatureChecker`.
let (callback, tx_version) = unsafe { *(ctx as *const (SighashCalculator, TxVersion)) };
if let Some(sighash) = HashType::from_bits(hash_type, tx_version)
let callback = unsafe { *(ctx as *const SighashCalculator) };
// We dont need to handle strictness here, because … something
if let Some(sighash) = HashType::from_bits(hash_type, false)
.ok()
.and_then(|ht| callback(script_code_vec, ht))
{
@ -61,7 +67,7 @@ extern "C" fn sighash_callback(
}
/// This steals a bit of the wrapper code from zebra_script, to provide the API that they want.
impl ZcashScript for Cxx {
impl ZcashScript for CxxInterpreter {
fn verify_callback(
sighash: SighashCalculator,
lock_time: i64,
@ -69,14 +75,13 @@ impl ZcashScript for Cxx {
script_pub_key: &[u8],
signature_script: &[u8],
flags: VerificationFlags,
tx_version: TxVersion,
) -> 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, tx_version) as *const (SighashCalculator, TxVersion)) as *const c_void,
(&sighash as *const SighashCalculator) as *const c_void,
Some(sighash_callback),
lock_time,
if is_final { 1 } else { 0 },
@ -115,10 +120,131 @@ impl ZcashScript for Cxx {
}
}
/// Runs both the C++ and Rust implementations `ZcashScript::legacy_sigop_count_script` and returns
/// both results. This is more useful for testing than the impl that logs a warning if the results
/// differ and always returns the C++ result.
fn check_legacy_sigop_count_script<T: ZcashScript, U: ZcashScript>(
script: &[u8],
) -> (Result<u32, Error>, Result<u32, Error>) {
(
T::legacy_sigop_count_script(script),
U::legacy_sigop_count_script(script),
)
}
/// Runs two implementations of `ZcashScript::verify_callback` with the same arguments and returns
/// both results. This is more useful for testing than the impl that logs a warning if the results
/// differ and always returns the `T` result.
pub fn check_verify_callback<T: ZcashScript, U: ZcashScript>(
sighash: SighashCalculator,
lock_time: i64,
is_final: bool,
script_pub_key: &[u8],
script_sig: &[u8],
flags: VerificationFlags,
) -> (Result<(), Error>, Result<(), Error>) {
(
T::verify_callback(
sighash,
lock_time,
is_final,
script_pub_key,
script_sig,
flags,
),
U::verify_callback(
sighash,
lock_time,
is_final,
script_pub_key,
script_sig,
flags,
),
)
}
/// Convert errors that dont exist in the C++ code into the cases that do.
pub fn normalize_error(err: Error) -> Error {
match err {
Error::Ok(Some(_)) => Error::Ok(None),
_ => err,
}
}
/// A tag to indicate that both the C++ and Rust implementations of zcash_script should be used,
/// with their results compared.
pub enum CxxRustComparisonInterpreter {}
/// This implementation is functionally equivalent to the `T` impl, but it also runs a second (`U`)
/// impl and logs a warning if they disagree.
impl ZcashScript for CxxRustComparisonInterpreter {
fn legacy_sigop_count_script(script: &[u8]) -> Result<u32, Error> {
let (cxx, rust) =
check_legacy_sigop_count_script::<CxxInterpreter, RustInterpreter>(script);
if rust != cxx {
warn!(
"The Rust Zcash Script interpreter had a different sigop count ({:?}) from the C++ one ({:?}).",
rust,
cxx)
};
cxx
}
fn verify_callback(
sighash: SighashCalculator,
lock_time: i64,
is_final: bool,
script_pub_key: &[u8],
script_sig: &[u8],
flags: VerificationFlags,
) -> Result<(), Error> {
let (cxx, rust) = check_verify_callback::<CxxInterpreter, RustInterpreter>(
sighash,
lock_time,
is_final,
script_pub_key,
script_sig,
flags,
);
if rust.map_err(normalize_error) != cxx {
// probably want to distinguish between
// - C++ succeeding when Rust fails (bad),
// - Rust succeeding when C++ fals (worse), and
// - differing error codes (maybe not bad).
warn!(
"The Rust Zcash Script interpreter had a different result ({:?}) from the C++ one ({:?}).",
rust,
cxx)
};
cxx
}
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use super::*;
/// Ensures that flags represent a supported state. This avoids crashes in the C++ code, which
/// break various tests.
pub fn repair_flags(flags: VerificationFlags) -> VerificationFlags {
// TODO: The C++ implementation fails an assert (interpreter.cpp:1097) if `CleanStack` is
// set without `P2SH`.
if flags.contains(VerificationFlags::CleanStack) {
flags & VerificationFlags::P2SH
} else {
flags
}
}
/// A `usize` one larger than the longest allowed script, for testing bounds.
pub const OVERFLOW_SCRIPT_SIZE: usize = script::MAX_SCRIPT_SIZE + 1;
}
#[cfg(test)]
mod tests {
pub use super::*;
use super::{testing::*, *};
use hex::FromHex;
use proptest::prelude::*;
lazy_static::lazy_static! {
pub static ref SCRIPT_PUBKEY: Vec<u8> = <Vec<u8>>::from_hex("a914c117756dcbe144a12a7c33a77cfa81aa5aeeb38187").unwrap();
@ -130,7 +256,7 @@ mod tests {
.unwrap()
.as_slice()
.first_chunk::<32>()
.map(|hash| *hash)
.copied()
}
fn invalid_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> {
@ -138,7 +264,7 @@ mod tests {
.unwrap()
.as_slice()
.first_chunk::<32>()
.map(|hash| *hash)
.copied()
}
fn missing_sighash(_script_code: &[u8], _hash_type: HashType) -> Option<[u8; 32]> {
@ -153,17 +279,17 @@ mod tests {
let script_sig = &SCRIPT_SIG;
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
let ret = Cxx::verify_callback(
let ret = check_verify_callback::<CxxInterpreter, RustInterpreter>(
&sighash,
n_lock_time,
is_final,
script_pub_key,
script_sig,
flags,
TxVersion::Sapling,
);
assert!(ret.is_ok());
assert_eq!(ret.0, ret.1.map_err(normalize_error));
assert!(ret.0.is_ok());
}
#[test]
@ -174,17 +300,21 @@ mod tests {
let script_sig = &SCRIPT_SIG;
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
let ret = Cxx::verify_callback(
let ret = check_verify_callback::<CxxInterpreter, RustInterpreter>(
&invalid_sighash,
n_lock_time,
is_final,
script_pub_key,
script_sig,
flags,
TxVersion::Sapling,
);
assert_eq!(ret, Err(Error::Ok));
assert_eq!(ret.0, ret.1.map_err(normalize_error));
// Checks the Rust result, because we have more information on the Rust side.
assert_eq!(
ret.1,
Err(Error::Ok(Some(script_error::ScriptError::EvalFalse)))
);
}
#[test]
@ -195,16 +325,72 @@ mod tests {
let script_sig = &SCRIPT_SIG;
let flags = VerificationFlags::P2SH | VerificationFlags::CHECKLOCKTIMEVERIFY;
let ret = Cxx::verify_callback(
let ret = check_verify_callback::<CxxInterpreter, RustInterpreter>(
&missing_sighash,
n_lock_time,
is_final,
script_pub_key,
script_sig,
flags,
TxVersion::Sapling,
);
assert_eq!(ret, Err(Error::Ok));
assert_eq!(ret.0, ret.1.map_err(normalize_error));
// Checks the Rust result, because we have more information on the Rust side.
assert_eq!(
ret.1,
Err(Error::Ok(Some(script_error::ScriptError::EvalFalse)))
);
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 20_000, .. ProptestConfig::default()
})]
/// This test is very shallow, because we have only `()` for success and most errors have
/// been collapsed to `Error::Ok`. A deeper comparison, requires changes to the C++ code.
#[test]
fn test_arbitrary_scripts(
lock_time in prop::num::i64::ANY,
is_final in prop::bool::ANY,
pub_key in prop::collection::vec(0..=0xffu8, 0..=OVERFLOW_SCRIPT_SIZE),
sig in prop::collection::vec(0..=0xffu8, 1..=OVERFLOW_SCRIPT_SIZE),
flags in prop::bits::u32::masked(VerificationFlags::all().bits()),
) {
let ret = check_verify_callback::<CxxInterpreter, RustInterpreter>(
&missing_sighash,
lock_time,
is_final,
&pub_key[..],
&sig[..],
repair_flags(VerificationFlags::from_bits_truncate(flags)),
);
prop_assert_eq!(ret.0, ret.1.map_err(normalize_error),
"original Rust result: {:?}", ret.1);
}
/// Similar to `test_arbitrary_scripts`, but ensures the `sig` only contains pushes.
#[test]
fn test_restricted_sig_scripts(
lock_time in prop::num::i64::ANY,
is_final in prop::bool::ANY,
pub_key in prop::collection::vec(0..=0xffu8, 0..=OVERFLOW_SCRIPT_SIZE),
sig in prop::collection::vec(0..=0x60u8, 0..=OVERFLOW_SCRIPT_SIZE),
flags in prop::bits::u32::masked(
// Dont waste test cases on whether or not `SigPushOnly` is set.
(VerificationFlags::all() - VerificationFlags::SigPushOnly).bits()),
) {
let ret = check_verify_callback::<CxxInterpreter, RustInterpreter>(
&missing_sighash,
lock_time,
is_final,
&pub_key[..],
&sig[..],
repair_flags(VerificationFlags::from_bits_truncate(flags))
| VerificationFlags::SigPushOnly,
);
prop_assert_eq!(ret.0, ret.1.map_err(normalize_error),
"original Rust result: {:?}", ret.1);
}
}
}

642
src/script.rs Normal file
View File

@ -0,0 +1,642 @@
#![allow(non_camel_case_types)]
use std::{
num::TryFromIntError,
ops::{Add, Neg, Sub},
};
use enum_primitive::FromPrimitive;
use super::script_error::*;
pub const MAX_SCRIPT_ELEMENT_SIZE: usize = 520; // bytes
/// Maximum script length in bytes
pub const MAX_SCRIPT_SIZE: usize = 10000;
// Threshold for lock_time: below this value it is interpreted as block number,
// otherwise as UNIX timestamp.
pub const LOCKTIME_THRESHOLD: ScriptNum = ScriptNum(500000000); // Tue Nov 5 00:53:20 1985 UTC
/** Script opcodes */
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum Opcode {
PushValue(PushValue),
Operation(Operation),
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
#[repr(u8)]
pub enum PushValue {
// push value
OP_0 = 0x00,
PushdataBytelength(u8),
OP_PUSHDATA1 = 0x4c,
OP_PUSHDATA2 = 0x4d,
OP_PUSHDATA4 = 0x4e,
OP_1NEGATE = 0x4f,
OP_RESERVED = 0x50,
OP_1 = 0x51,
OP_2 = 0x52,
OP_3 = 0x53,
OP_4 = 0x54,
OP_5 = 0x55,
OP_6 = 0x56,
OP_7 = 0x57,
OP_8 = 0x58,
OP_9 = 0x59,
OP_10 = 0x5a,
OP_11 = 0x5b,
OP_12 = 0x5c,
OP_13 = 0x5d,
OP_14 = 0x5e,
OP_15 = 0x5f,
OP_16 = 0x60,
}
use PushValue::*;
enum_from_primitive! {
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
#[repr(u8)]
pub enum Operation {
// control
OP_NOP = 0x61,
OP_VER = 0x62,
OP_IF = 0x63,
OP_NOTIF = 0x64,
OP_VERIF = 0x65,
OP_VERNOTIF = 0x66,
OP_ELSE = 0x67,
OP_ENDIF = 0x68,
OP_VERIFY = 0x69,
OP_RETURN = 0x6a,
// stack ops
OP_TOALTSTACK = 0x6b,
OP_FROMALTSTACK = 0x6c,
OP_2DROP = 0x6d,
OP_2DUP = 0x6e,
OP_3DUP = 0x6f,
OP_2OVER = 0x70,
OP_2ROT = 0x71,
OP_2SWAP = 0x72,
OP_IFDUP = 0x73,
OP_DEPTH = 0x74,
OP_DROP = 0x75,
OP_DUP = 0x76,
OP_NIP = 0x77,
OP_OVER = 0x78,
OP_PICK = 0x79,
OP_ROLL = 0x7a,
OP_ROT = 0x7b,
OP_SWAP = 0x7c,
OP_TUCK = 0x7d,
// splice ops
OP_CAT = 0x7e,
OP_SUBSTR = 0x7f,
OP_LEFT = 0x80,
OP_RIGHT = 0x81,
OP_SIZE = 0x82,
// bit logic
OP_INVERT = 0x83,
OP_AND = 0x84,
OP_OR = 0x85,
OP_XOR = 0x86,
OP_EQUAL = 0x87,
OP_EQUALVERIFY = 0x88,
OP_RESERVED1 = 0x89,
OP_RESERVED2 = 0x8a,
// numeric
OP_1ADD = 0x8b,
OP_1SUB = 0x8c,
OP_2MUL = 0x8d,
OP_2DIV = 0x8e,
OP_NEGATE = 0x8f,
OP_ABS = 0x90,
OP_NOT = 0x91,
OP_0NOTEQUAL = 0x92,
OP_ADD = 0x93,
OP_SUB = 0x94,
OP_MUL = 0x95,
OP_DIV = 0x96,
OP_MOD = 0x97,
OP_LSHIFT = 0x98,
OP_RSHIFT = 0x99,
OP_BOOLAND = 0x9a,
OP_BOOLOR = 0x9b,
OP_NUMEQUAL = 0x9c,
OP_NUMEQUALVERIFY = 0x9d,
OP_NUMNOTEQUAL = 0x9e,
OP_LESSTHAN = 0x9f,
OP_GREATERTHAN = 0xa0,
OP_LESSTHANOREQUAL = 0xa1,
OP_GREATERTHANOREQUAL = 0xa2,
OP_MIN = 0xa3,
OP_MAX = 0xa4,
OP_WITHIN = 0xa5,
// crypto
OP_RIPEMD160 = 0xa6,
OP_SHA1 = 0xa7,
OP_SHA256 = 0xa8,
OP_HASH160 = 0xa9,
OP_HASH256 = 0xaa,
OP_CODESEPARATOR = 0xab,
OP_CHECKSIG = 0xac,
OP_CHECKSIGVERIFY = 0xad,
OP_CHECKMULTISIG = 0xae,
OP_CHECKMULTISIGVERIFY = 0xaf,
// expansion
OP_NOP1 = 0xb0,
OP_NOP2 = 0xb1,
OP_NOP3 = 0xb2,
OP_NOP4 = 0xb3,
OP_NOP5 = 0xb4,
OP_NOP6 = 0xb5,
OP_NOP7 = 0xb6,
OP_NOP8 = 0xb7,
OP_NOP9 = 0xb8,
OP_NOP10 = 0xb9,
OP_INVALIDOPCODE = 0xff,
}
}
use Operation::*;
pub const OP_CHECKLOCKTIMEVERIFY: Operation = OP_NOP2;
impl From<Opcode> for u8 {
fn from(value: Opcode) -> Self {
match value {
Opcode::PushValue(pv) => pv.into(),
Opcode::Operation(op) => op.into(),
}
}
}
impl From<u8> for Opcode {
fn from(value: u8) -> Self {
Operation::from_u8(value).map_or(
PushValue::try_from(value)
.map_or(Opcode::Operation(OP_INVALIDOPCODE), Opcode::PushValue),
Opcode::Operation,
)
}
}
impl From<PushValue> for u8 {
fn from(value: PushValue) -> Self {
match value {
OP_0 => 0x00,
PushdataBytelength(byte) => byte,
OP_PUSHDATA1 => 0x4c,
OP_PUSHDATA2 => 0x4d,
OP_PUSHDATA4 => 0x4e,
OP_1NEGATE => 0x4f,
OP_RESERVED => 0x50,
OP_1 => 0x51,
OP_2 => 0x52,
OP_3 => 0x53,
OP_4 => 0x54,
OP_5 => 0x55,
OP_6 => 0x56,
OP_7 => 0x57,
OP_8 => 0x58,
OP_9 => 0x59,
OP_10 => 0x5a,
OP_11 => 0x5b,
OP_12 => 0x5c,
OP_13 => 0x5d,
OP_14 => 0x5e,
OP_15 => 0x5f,
OP_16 => 0x60,
}
}
}
impl TryFrom<u8> for PushValue {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x00 => Ok(OP_0),
0x4c => Ok(OP_PUSHDATA1),
0x4d => Ok(OP_PUSHDATA2),
0x4e => Ok(OP_PUSHDATA4),
0x4f => Ok(OP_1NEGATE),
0x50 => Ok(OP_RESERVED),
0x51 => Ok(OP_1),
0x52 => Ok(OP_2),
0x53 => Ok(OP_3),
0x54 => Ok(OP_4),
0x55 => Ok(OP_5),
0x56 => Ok(OP_6),
0x57 => Ok(OP_7),
0x58 => Ok(OP_8),
0x59 => Ok(OP_9),
0x5a => Ok(OP_10),
0x5b => Ok(OP_11),
0x5c => Ok(OP_12),
0x5d => Ok(OP_13),
0x5e => Ok(OP_14),
0x5f => Ok(OP_15),
0x60 => Ok(OP_16),
_ => {
if value <= 0x60 {
Ok(PushdataBytelength(value))
} else {
Err(())
}
}
}
}
}
impl From<Operation> for u8 {
fn from(value: Operation) -> Self {
// This is how you get the discriminant, but using `as` everywhere is too much code smell
value as u8
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct ScriptNum(i64);
impl ScriptNum {
const DEFAULT_MAX_NUM_SIZE: usize = 4;
pub fn new(
vch: &Vec<u8>,
require_minimal: bool,
max_num_size: Option<usize>,
) -> Result<Self, ScriptNumError> {
let max_num_size = max_num_size.unwrap_or(Self::DEFAULT_MAX_NUM_SIZE);
if vch.len() > max_num_size {
return Err(ScriptNumError::Overflow {
max_num_size,
actual: vch.len(),
});
}
if require_minimal && !vch.is_empty() {
// Check that the number is encoded with the minimum possible
// number of bytes.
//
// If the most-significant-byte - excluding the sign bit - is zero
// then we're not minimal. Note how this test also rejects the
// negative-zero encoding, 0x80.
if (vch.last().unwrap_or_else(|| unreachable!()) & 0x7F) == 0 {
// One exception: if there's more than one byte and the most
// significant bit of the second-most-significant-byte is set
// it would conflict with the sign bit. An example of this case
// is +-255, which encode to 0xff00 and 0xff80 respectively.
// (big-endian).
if vch.len() <= 1 {
return Err(ScriptNumError::NegativeZero);
} else if (vch[vch.len() - 2] & 0x80) == 0 {
return Err(ScriptNumError::NonMinimalEncoding);
}
}
}
Self::set_vch(vch).map(ScriptNum)
}
pub fn getint(&self) -> i32 {
if self.0 > i32::MAX.into() {
i32::MAX
} else if self.0 < i32::MIN.into() {
i32::MIN
} else {
self.0.try_into().unwrap()
}
}
pub fn getvch(&self) -> Vec<u8> {
Self::serialize(&self.0)
}
pub fn serialize(value: &i64) -> Vec<u8> {
if *value == 0 {
return Vec::new();
}
if *value == i64::MIN {
// The code below is buggy, and produces the "wrong" result for
// INT64_MIN. To avoid undefined behavior while attempting to
// negate a value of INT64_MIN, we intentionally return the result
// that the code below would produce on an x86_64 system.
return vec![0, 0, 0, 0, 0, 0, 0, 128, 128];
}
let mut result = Vec::new();
let neg = *value < 0;
let mut absvalue = value.abs();
while absvalue != 0 {
result.push(
(absvalue & 0xff)
.try_into()
.unwrap_or_else(|_| unreachable!()),
);
absvalue >>= 8;
}
// - If the most significant byte is >= 0x80 and the value is positive, push a
// new zero-byte to make the significant byte < 0x80 again.
// - If the most significant byte is >= 0x80 and the value is negative, push a
// new 0x80 byte that will be popped off when converting to an integral.
// - If the most significant byte is < 0x80 and the value is negative, add
// 0x80 to it, since it will be subtracted and interpreted as a negative when
// converting to an integral.
if result.last().map_or(true, |last| last & 0x80 != 0) {
result.push(if neg { 0x80 } else { 0 });
} else if neg {
if let Some(last) = result.last_mut() {
*last |= 0x80;
}
}
result
}
fn set_vch(vch: &Vec<u8>) -> Result<i64, ScriptNumError> {
match vch.last() {
None => Ok(0),
Some(vch_back) => {
if *vch == vec![0, 0, 0, 0, 0, 0, 0, 128, 128] {
// On an x86_64 system, the code below would actually decode the buggy
// INT64_MIN encoding correctly. However in this case, it would be
// performing left shifts of a signed type by 64, which has undefined
// behavior.
return Ok(i64::MIN);
};
// Guard against undefined behavior. INT64_MIN is the only allowed 9-byte encoding.
if vch.len() > 8 {
return Err(ScriptNumError::Overflow {
max_num_size: 8,
actual: vch.len(),
});
};
let mut result: i64 = 0;
for (i, vch_i) in vch.iter().enumerate() {
result |= i64::from(*vch_i) << (8 * i);
}
// If the input vector's most significant byte is 0x80, remove it from
// the result's msb and return a negative.
if vch_back & 0x80 != 0 {
return Ok(-(result & !(0x80 << (8 * (vch.len() - 1)))));
};
Ok(result)
}
}
}
}
impl From<i64> for ScriptNum {
fn from(value: i64) -> Self {
ScriptNum(value)
}
}
impl From<i32> for ScriptNum {
fn from(value: i32) -> Self {
ScriptNum(value.into())
}
}
impl From<u8> for ScriptNum {
fn from(value: u8) -> Self {
ScriptNum(value.into())
}
}
/// TODO: This instance will be obsolete if we convert bool directly to a `Vec<u8>`, which is also
/// more efficient.
impl From<bool> for ScriptNum {
fn from(value: bool) -> Self {
ScriptNum(value.into())
}
}
impl TryFrom<usize> for ScriptNum {
type Error = TryFromIntError;
fn try_from(value: usize) -> Result<Self, Self::Error> {
value.try_into().map(ScriptNum)
}
}
impl TryFrom<ScriptNum> for u16 {
type Error = TryFromIntError;
fn try_from(value: ScriptNum) -> Result<Self, Self::Error> {
value.getint().try_into()
}
}
impl TryFrom<ScriptNum> for u8 {
type Error = TryFromIntError;
fn try_from(value: ScriptNum) -> Result<Self, Self::Error> {
value.getint().try_into()
}
}
impl Add for ScriptNum {
type Output = Self;
fn add(self, other: Self) -> Self {
let rhs = other.0;
assert!(
rhs == 0
|| (rhs > 0 && self.0 <= i64::MAX - rhs)
|| (rhs < 0 && self.0 >= i64::MIN - rhs)
);
Self(self.0 + rhs)
}
}
impl Sub for ScriptNum {
type Output = Self;
fn sub(self, other: Self) -> Self {
let rhs = other.0;
assert!(
rhs == 0
|| (rhs > 0 && self.0 >= i64::MIN + rhs)
|| (rhs < 0 && self.0 <= i64::MAX + rhs)
);
Self(self.0 - rhs)
}
}
impl Neg for ScriptNum {
type Output = Self;
fn neg(self) -> Self {
assert!(self.0 != i64::MIN);
Self(-self.0)
}
}
/** Serialized script, used inside transaction inputs and outputs */
#[derive(Clone, Debug)]
pub struct Script<'a>(pub &'a [u8]);
impl Script<'_> {
pub fn get_op(script: &mut &[u8]) -> Result<Opcode, ScriptError> {
Self::get_op2(script, &mut vec![])
}
pub fn get_op2(script: &mut &[u8], buffer: &mut Vec<u8>) -> Result<Opcode, ScriptError> {
if script.is_empty() {
return Err(ScriptError::ReadError {
expected_bytes: 1,
available_bytes: 0,
});
}
// Empty the provided buffer, if any
buffer.truncate(0);
let leading_byte = Opcode::from(script[0]);
*script = &script[1..];
Ok(match leading_byte {
Opcode::PushValue(pv) => match pv {
OP_PUSHDATA1 | OP_PUSHDATA2 | OP_PUSHDATA4 => {
let read_le = |script: &mut &[u8], needed_bytes: usize| {
if script.len() < needed_bytes {
Err(ScriptError::ReadError {
expected_bytes: needed_bytes,
available_bytes: script.len(),
})
} else {
let mut size = 0;
for i in (0..needed_bytes).rev() {
size <<= 8;
size |= usize::from(script[i]);
}
*script = &script[needed_bytes..];
Ok(size)
}
};
let size = match pv {
OP_PUSHDATA1 => read_le(script, 1),
OP_PUSHDATA2 => read_le(script, 2),
OP_PUSHDATA4 => read_le(script, 4),
_ => unreachable!(),
}?;
if script.len() < size {
return Err(ScriptError::ReadError {
expected_bytes: size,
available_bytes: script.len(),
});
}
buffer.extend(&script[0..size]);
*script = &script[size..];
leading_byte
}
// OP_0/OP_FALSE doesn't actually push a constant 0 onto the stack but
// pushes an empty array. (Thus we leave the buffer truncated to 0 length)
OP_0 => leading_byte,
PushdataBytelength(size_byte) => {
let size = size_byte.into();
if script.len() < size {
return Err(ScriptError::ReadError {
expected_bytes: size,
available_bytes: script.len(),
});
}
buffer.extend(&script[0..size]);
*script = &script[size..];
leading_byte
}
_ => leading_byte,
},
_ => leading_byte,
})
}
/** Encode/decode small integers: */
pub fn decode_op_n(opcode: PushValue) -> u32 {
if opcode == OP_0 {
return 0;
}
assert!(opcode >= OP_1 && opcode <= OP_16);
(u8::from(opcode) - (u8::from(OP_1) - 1)).into()
}
/// Pre-version-0.6, Bitcoin always counted CHECKMULTISIGs
/// as 20 sigops. With pay-to-script-hash, that changed:
/// CHECKMULTISIGs serialized in script_sigs are
/// counted more accurately, assuming they are of the form
/// ... OP_N CHECKMULTISIG ...
pub fn get_sig_op_count(&self, accurate: bool) -> u32 {
let mut n = 0;
let mut pc = self.0;
let mut last_opcode = Opcode::Operation(OP_INVALIDOPCODE);
while !pc.is_empty() {
let opcode = match Self::get_op(&mut pc) {
Ok(o) => o,
Err(_) => break,
};
if let Opcode::Operation(op) = opcode {
if op == OP_CHECKSIG || op == OP_CHECKSIGVERIFY {
n += 1;
} else if op == OP_CHECKMULTISIG || op == OP_CHECKMULTISIGVERIFY {
match last_opcode {
Opcode::PushValue(pv) => {
if accurate && pv >= OP_1 && pv <= OP_16 {
n += Self::decode_op_n(pv);
} else {
n += 20
}
}
_ => n += 20,
}
}
}
last_opcode = opcode;
}
n
}
/// Returns true iff this script is P2SH.
pub fn is_pay_to_script_hash(&self) -> bool {
self.0.len() == 23
&& self.0[0] == OP_HASH160.into()
&& self.0[1] == 0x14
&& self.0[22] == OP_EQUAL.into()
}
/// Called by `IsStandardTx` and P2SH/BIP62 VerifyScript (which makes it consensus-critical).
pub fn is_push_only(&self) -> bool {
let mut pc = self.0;
while !pc.is_empty() {
if let Ok(Opcode::PushValue(_)) = Self::get_op(&mut pc) {
} else {
return false;
}
}
true
}
}

68
src/script_error.rs Normal file
View File

@ -0,0 +1,68 @@
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum ScriptNumError {
NegativeZero,
NonMinimalEncoding,
Overflow { max_num_size: usize, actual: usize },
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[repr(i32)]
pub enum ScriptError {
// Ok = 0,
UnknownError = 1,
EvalFalse,
OpReturn,
// Max sizes
ScriptSize,
PushSize,
OpCount,
StackSize,
SigCount,
PubKeyCount,
// Failed verify operations
Verify,
EqualVerify,
CheckMultisigVerify,
CheckSigVerify,
NumEqualVerify,
// Logical/Format/Canonical errors
BadOpcode,
DisabledOpcode,
InvalidStackOperation,
InvalidAltstackOperation,
UnbalancedConditional,
// OP_CHECKLOCKTIMEVERIFY
NegativeLockTime,
UnsatisfiedLockTime,
// BIP62
SigHashType,
SigDER,
MinimalData,
SigPushOnly,
// SigHighS,
SigNullDummy = 27,
PubKeyType,
CleanStack,
// softfork safeness
DiscourageUpgradableNOPs,
ReadError {
expected_bytes: usize,
available_bytes: usize,
},
/// Corresponds to the `scriptnum_error` exception in C++.
ScriptNumError(ScriptNumError),
}
impl From<ScriptNumError> for ScriptError {
fn from(value: ScriptNumError) -> Self {
ScriptError::ScriptNumError(value)
}
}

View File

@ -1,8 +1,8 @@
use std::num::TryFromIntError;
use zcash_primitives::transaction::TxVersion;
use super::interpreter::*;
use super::script::*;
use super::script_error::*;
/// This maps to `zcash_script_error_t`, but most of those cases arent used any more. This only
/// replicates the still-used cases, and then an `Unknown` bucket for anything else that might
@ -10,7 +10,10 @@ use super::interpreter::*;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Error {
/// Any failure that results in the script being invalid.
Ok,
///
/// __NB__: This is in `Option` because this type is used by both the C++ and Rust
/// implementations, but the C++ impl doesnt yet expose the original error.
Ok(Option<ScriptError>),
/// An exception was caught.
VerifyScript,
/// The script size cant fit in a `u32`, as required by the C++ code.
@ -22,22 +25,6 @@ pub enum Error {
Unknown(i64),
}
/// All signature hashes are 32 bytes, since they are either:
/// - a SHA-256 output (for v1 or v2 transactions).
/// - a BLAKE2b-256 output (for v3 and above transactions).
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 doesnt 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 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 {
@ -57,16 +44,49 @@ pub trait ZcashScript {
///
/// Note that script verification failure is indicated by `Err(Error::Ok)`.
fn verify_callback(
sighash: SighashCalculator,
n_lock_time: i64,
sighash_callback: SighashCalculator,
lock_time: i64,
is_final: bool,
script_pub_key: &[u8],
script_sig: &[u8],
flags: VerificationFlags,
tx_version: TxVersion,
) -> 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>;
}
/// A tag to indicate that the Rust implementation of zcash_script should be used.
pub enum RustInterpreter {}
impl ZcashScript for RustInterpreter {
/// 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> {
let cscript = Script(script);
Ok(cscript.get_sig_op_count(false))
}
fn verify_callback(
sighash: SighashCalculator,
lock_time: i64,
is_final: bool,
script_pub_key: &[u8],
script_sig: &[u8],
flags: VerificationFlags,
) -> Result<(), Error> {
let lock_time_num = lock_time.into();
verify_script(
&Script(script_sig),
&Script(script_pub_key),
flags,
&CallbackTransactionSignatureChecker {
sighash,
lock_time: &lock_time_num,
is_final,
},
)
.map_err(|e| Error::Ok(Some(e)))
}
}