zebra_script: update to new zcash_script callback API (#8566)

* zebra_script: change to use new zcash_script callback API

* add precomputation

* cleanups

* use released zcash_script

* fix clippy error

* Apply suggestions from code review

Co-authored-by: Marek <mail@marek.onl>

* improve docs

* use tuple instead of two Option params

---------

Co-authored-by: Marek <mail@marek.onl>
This commit is contained in:
Conrado Gouvea 2024-06-19 09:48:12 -03:00 committed by GitHub
parent c6f575319d
commit 3650442ad8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 422 additions and 420 deletions

View File

@ -823,16 +823,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "color-eyre"
version = "0.6.3"
@ -1078,47 +1068,6 @@ dependencies = [
"syn 2.0.66",
]
[[package]]
name = "cxx"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "048948e14bc2c2652ec606c8e3bb913407f0187288fb351a0b2d972beaf12070"
dependencies = [
"cc",
"cxxbridge-flags",
"cxxbridge-macro",
"link-cplusplus",
]
[[package]]
name = "cxx-gen"
version = "0.7.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "383ecb9f96a536a1c7a2a61c5786f583da84f9240da149d78d005a4413c9a71e"
dependencies = [
"codespan-reporting",
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af40b0467c68d3d9fb7550ef984edc8ad47252f703ef0f1f2d1052e0e4af8793"
[[package]]
name = "cxxbridge-macro"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7743446286141c9f6d4497c493c01234eb848e14d2e20866ae9811eae0630cb9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "darling"
version = "0.13.4"
@ -2472,15 +2421,6 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "link-cplusplus"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9"
dependencies = [
"cc",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@ -2565,17 +2505,6 @@ dependencies = [
"nonempty",
]
[[package]]
name = "metrics"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde3af1a009ed76a778cb84fdef9e7dbbdf5775ae3e4cc1f434a6a307f6f76c5"
dependencies = [
"ahash",
"metrics-macros",
"portable-atomic",
]
[[package]]
name = "metrics"
version = "0.22.3"
@ -2598,7 +2527,7 @@ dependencies = [
"hyper-util",
"indexmap 2.2.6",
"ipnet",
"metrics 0.22.3",
"metrics",
"metrics-util",
"quanta",
"thiserror",
@ -2606,17 +2535,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "metrics-macros"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b4faf00617defe497754acde3024865bc143d44a86799b24e191ecff91354f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "metrics-util"
version = "0.16.3"
@ -2626,7 +2544,7 @@ dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
"hashbrown 0.14.5",
"metrics 0.22.3",
"metrics",
"num_cpus",
"quanta",
"sketches-ddsketch",
@ -5940,41 +5858,12 @@ dependencies = [
[[package]]
name = "zcash_script"
version = "0.1.16"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3febfe5f2abdab3597d17c8f71cc0071902d82f8433aa329abe52461eabaa9c4"
checksum = "2122a042c77d529d3c60b899e74705eda39ae96a8a992460caeb06afa76990a2"
dependencies = [
"bellman",
"bindgen",
"blake2b_simd",
"blake2s_simd",
"bls12_381",
"bridgetree",
"byteorder",
"cc",
"crossbeam-channel",
"cxx",
"cxx-gen",
"group",
"incrementalmerkletree",
"jubjub",
"libc",
"memuse",
"metrics 0.21.1",
"orchard 0.7.1",
"rand 0.8.5",
"rand_core 0.6.4",
"rayon",
"redjubjub",
"sapling-crypto",
"subtle",
"syn 1.0.109",
"tracing",
"zcash_address",
"zcash_encoding",
"zcash_note_encryption",
"zcash_primitives 0.14.0",
"zcash_proofs",
]
[[package]]
@ -6065,7 +5954,7 @@ dependencies = [
"howudoin",
"jubjub",
"lazy_static",
"metrics 0.22.3",
"metrics",
"num-integer",
"once_cell",
"orchard 0.7.1",
@ -6133,7 +6022,7 @@ dependencies = [
"indexmap 2.2.6",
"itertools 0.13.0",
"lazy_static",
"metrics 0.22.3",
"metrics",
"num-integer",
"ordered-map",
"pin-project",
@ -6271,7 +6160,7 @@ dependencies = [
"itertools 0.13.0",
"jubjub",
"lazy_static",
"metrics 0.22.3",
"metrics",
"mset",
"once_cell",
"proptest",
@ -6377,7 +6266,7 @@ dependencies = [
"jsonrpc-core",
"lazy_static",
"log",
"metrics 0.22.3",
"metrics",
"metrics-exporter-prometheus",
"num-integer",
"once_cell",

View File

@ -140,7 +140,7 @@ const FAKE_TESTNET_ACTIVATION_HEIGHTS: &[(block::Height, NetworkUpgrade)] = &[
/// The Consensus Branch Id, used to bind transactions and blocks to a
/// particular network upgrade.
#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct ConsensusBranchId(u32);
pub struct ConsensusBranchId(pub(crate) u32);
impl ConsensusBranchId {
/// Return the hash bytes in big-endian byte-order suitable for printing out byte by byte.

View File

@ -17,7 +17,12 @@ use crate::{
/// since `librustzcash` won't be able to parse it.
pub fn decrypts_successfully(transaction: &Transaction, network: &Network, height: Height) -> bool {
let network_upgrade = NetworkUpgrade::current(network, height);
let alt_tx = convert_tx_to_librustzcash(transaction, network_upgrade)
let alt_tx = convert_tx_to_librustzcash(
transaction,
network_upgrade
.branch_id()
.expect("should have a branch ID"),
)
.expect("zcash_primitives and Zebra transaction formats must be compatible");
let null_sapling_ovk = sapling::keys::OutgoingViewingKey([0u8; 32]);

View File

@ -3,11 +3,11 @@
use std::{io, ops::Deref};
use zcash_primitives::transaction as zp_tx;
use zcash_primitives::transaction::{self as zp_tx, TxDigests};
use crate::{
amount::{Amount, NonNegative},
parameters::{Network, NetworkUpgrade},
parameters::{ConsensusBranchId, Network},
serialization::ZcashSerialize,
transaction::{AuthDigest, HashType, SigHash, Transaction},
transparent::{self, Script},
@ -127,6 +127,7 @@ impl zp_tx::components::orchard::MapAuth<orchard::bundle::Authorized, orchard::b
}
}
#[derive(Debug)]
struct PrecomputedAuth<'a> {
_phantom: std::marker::PhantomData<&'a ()>,
}
@ -148,6 +149,7 @@ impl TryFrom<&Transaction> for zp_tx::Transaction {
///
/// If the transaction is not V5. (Currently there is no need for this
/// conversion for other versions.)
#[allow(clippy::unwrap_in_result)]
fn try_from(trans: &Transaction) -> Result<Self, Self::Error> {
let network_upgrade = match trans {
Transaction::V5 {
@ -159,19 +161,19 @@ impl TryFrom<&Transaction> for zp_tx::Transaction {
| Transaction::V4 { .. } => panic!("Zebra only uses librustzcash for V5 transactions"),
};
convert_tx_to_librustzcash(trans, *network_upgrade)
convert_tx_to_librustzcash(
trans,
network_upgrade.branch_id().expect("V5 txs have branch IDs"),
)
}
}
pub(crate) fn convert_tx_to_librustzcash(
trans: &Transaction,
network_upgrade: NetworkUpgrade,
branch_id: ConsensusBranchId,
) -> Result<zp_tx::Transaction, io::Error> {
let serialized_tx = trans.zcash_serialize_to_vec()?;
let branch_id: u32 = network_upgrade
.branch_id()
.expect("Network upgrade must have a Branch ID")
.into();
let branch_id: u32 = branch_id.into();
// We've already parsed this transaction, so its network upgrade must be valid.
let branch_id: zcash_primitives::consensus::BranchId = branch_id
.try_into()
@ -230,37 +232,76 @@ impl From<Script> for zcash_primitives::legacy::Script {
}
}
/// Precomputed data used for sighash or txid computation.
#[derive(Debug)]
pub(crate) struct PrecomputedTxData<'a> {
tx_data: zp_tx::TransactionData<PrecomputedAuth<'a>>,
txid_parts: TxDigests<blake2b_simd::Hash>,
all_previous_outputs: &'a [transparent::Output],
}
impl<'a> PrecomputedTxData<'a> {
/// Compute data used for sighash or txid computation.
///
/// # Inputs
///
/// - `tx`: the relevant transaction
/// - `branch_id`: the branch ID of the transaction
/// - `all_previous_outputs` the transparent Output matching each
/// transparent input in the transaction.
pub(crate) fn new(
tx: &'a Transaction,
branch_id: ConsensusBranchId,
all_previous_outputs: &'a [transparent::Output],
) -> PrecomputedTxData<'a> {
let alt_tx = convert_tx_to_librustzcash(tx, branch_id)
.expect("zcash_primitives and Zebra transaction formats must be compatible");
let txid_parts = alt_tx.deref().digest(zp_tx::txid::TxIdDigester);
let f_transparent = MapTransparent {
auth: TransparentAuth {
all_prev_outputs: all_previous_outputs,
},
};
let tx_data: zp_tx::TransactionData<PrecomputedAuth> = alt_tx
.into_data()
.map_authorization(f_transparent, IdentityMap, IdentityMap);
PrecomputedTxData {
tx_data,
txid_parts,
all_previous_outputs,
}
}
}
/// Compute a signature hash using librustzcash.
///
/// # Inputs
///
/// - `transaction`: the transaction whose signature hash to compute.
/// - `precomputed_tx_data`: precomputed data for the transaction whose
/// signature hash is being computed.
/// - `hash_type`: the type of hash (SIGHASH) being used.
/// - `network_upgrade`: the network upgrade of the block containing the transaction.
/// - `input`: information about the transparent input for which this signature
/// hash is being computed, if any. A tuple with the matching output of the
/// previous transaction, the input itself, and the index of the input in
/// the transaction.
/// - `input_index_script_code`: a tuple with the index of the transparent Input
/// for which we are producing a sighash and the respective script code being
/// validated, or None if it's a shielded input.
pub(crate) fn sighash(
trans: &Transaction,
precomputed_tx_data: &PrecomputedTxData,
hash_type: HashType,
network_upgrade: NetworkUpgrade,
all_previous_outputs: &[transparent::Output],
input_index: Option<usize>,
input_index_script_code: Option<(usize, Vec<u8>)>,
) -> SigHash {
let alt_tx = convert_tx_to_librustzcash(trans, network_upgrade)
.expect("zcash_primitives and Zebra transaction formats must be compatible");
let script: zcash_primitives::legacy::Script;
let signable_input = match input_index {
Some(input_index) => {
let output = all_previous_outputs[input_index].clone();
script = output.lock_script.into();
let lock_script: zcash_primitives::legacy::Script;
let unlock_script: zcash_primitives::legacy::Script;
let signable_input = match input_index_script_code {
Some((input_index, script_code)) => {
let output = &precomputed_tx_data.all_previous_outputs[input_index];
lock_script = output.lock_script.clone().into();
unlock_script = zcash_primitives::legacy::Script(script_code);
zp_tx::sighash::SignableInput::Transparent {
hash_type: hash_type.bits() as _,
index: input_index,
script_code: &script,
script_pubkey: &script,
script_code: &unlock_script,
script_pubkey: &lock_script,
value: output
.value
.try_into()
@ -270,18 +311,14 @@ pub(crate) fn sighash(
None => zp_tx::sighash::SignableInput::Shielded,
};
let txid_parts = alt_tx.deref().digest(zp_tx::txid::TxIdDigester);
let f_transparent = MapTransparent {
auth: TransparentAuth {
all_prev_outputs: all_previous_outputs,
},
};
let txdata: zp_tx::TransactionData<PrecomputedAuth> =
alt_tx
.into_data()
.map_authorization(f_transparent, IdentityMap, IdentityMap);
SigHash(*zp_tx::sighash::signature_hash(&txdata, &signable_input, &txid_parts).as_ref())
SigHash(
*zp_tx::sighash::signature_hash(
&precomputed_tx_data.tx_data,
&signable_input,
&precomputed_tx_data.txid_parts,
)
.as_ref(),
)
}
/// Compute the authorizing data commitment of this transaction as specified

View File

@ -33,7 +33,7 @@ pub use serialize::{
SerializedTransaction, MIN_TRANSPARENT_TX_SIZE, MIN_TRANSPARENT_TX_V4_SIZE,
MIN_TRANSPARENT_TX_V5_SIZE,
};
pub use sighash::{HashType, SigHash};
pub use sighash::{HashType, SigHash, SigHasher};
pub use unmined::{
zip317, UnminedTx, UnminedTxId, VerifiedUnminedTx, MEMPOOL_TRANSACTION_COST_THRESHOLD,
};
@ -41,9 +41,11 @@ pub use unmined::{
use crate::{
amount::{Amount, Error as AmountError, NegativeAllowed, NonNegative},
block, orchard,
parameters::NetworkUpgrade,
parameters::{ConsensusBranchId, NetworkUpgrade},
primitives::{ed25519, Bctv14Proof, Groth16Proof},
sapling, sprout,
sapling,
serialization::ZcashSerialize,
sprout,
transparent::{
self, outputs_from_utxos,
CoinbaseSpendRestriction::{self, *},
@ -192,14 +194,20 @@ impl Transaction {
UnminedTxId::from(self)
}
/// Calculate the sighash for the current transaction
/// Calculate the sighash for the current transaction.
///
/// If you need to compute multiple sighashes for the same transactions,
/// it's more efficient to use [`Transaction::sighasher()`].
///
/// # Details
///
/// The `input` argument indicates the transparent Input for which we are
/// producing a sighash. It is comprised of the index identifying the
/// transparent::Input within the transaction and the transparent::Output
/// representing the UTXO being spent by that input.
/// `all_previous_outputs` represents the UTXOs being spent by each input
/// in the transaction.
///
/// The `input_index_script_code` tuple indicates the index of the
/// transparent Input for which we are producing a sighash and the
/// respective script code being validated, or None if it's a shielded
/// input.
///
/// # Panics
///
@ -209,19 +217,22 @@ impl Transaction {
/// - if the input index is out of bounds for self.inputs()
pub fn sighash(
&self,
network_upgrade: NetworkUpgrade,
branch_id: ConsensusBranchId,
hash_type: sighash::HashType,
all_previous_outputs: &[transparent::Output],
input: Option<usize>,
input_index_script_code: Option<(usize, Vec<u8>)>,
) -> SigHash {
sighash::SigHasher::new(
self,
hash_type,
network_upgrade,
all_previous_outputs,
input,
)
.sighash()
sighash::SigHasher::new(self, branch_id, all_previous_outputs)
.sighash(hash_type, input_index_script_code)
}
/// Return a [`SigHasher`] for this transaction.
pub fn sighasher<'a>(
&'a self,
branch_id: ConsensusBranchId,
all_previous_outputs: &'a [transparent::Output],
) -> sighash::SigHasher {
sighash::SigHasher::new(self, branch_id, all_previous_outputs)
}
/// Compute the authorizing data commitment of this transaction as specified
@ -371,6 +382,26 @@ impl Transaction {
}
}
/// Get the raw lock time value.
pub fn raw_lock_time(&self) -> u32 {
let lock_time = match self {
Transaction::V1 { lock_time, .. }
| Transaction::V2 { lock_time, .. }
| Transaction::V3 { lock_time, .. }
| Transaction::V4 { lock_time, .. }
| Transaction::V5 { lock_time, .. } => *lock_time,
};
let mut lock_time_bytes = Vec::new();
lock_time
.zcash_serialize(&mut lock_time_bytes)
.expect("lock_time should serialize");
u32::from_le_bytes(
lock_time_bytes
.try_into()
.expect("should serialize as 4 bytes"),
)
}
/// Returns `true` if this transaction's `lock_time` is a [`LockTime::Time`].
/// Returns `false` if it is a [`LockTime::Height`] (locked or unlocked), is unlocked,
/// or if the transparent input sequence numbers have disabled lock times.

View File

@ -2,11 +2,10 @@
use super::Transaction;
use crate::{parameters::NetworkUpgrade, transparent};
use crate::parameters::ConsensusBranchId;
use crate::transparent;
use crate::primitives::zcash_primitives::sighash;
static ZIP143_EXPLANATION: &str = "Invalid transaction version: after Overwinter activation transaction versions 1 and 2 are rejected";
use crate::primitives::zcash_primitives::{sighash, PrecomputedTxData};
bitflags::bitflags! {
/// The different SigHash types, as defined in <https://zips.z.cash/zip-0143>
@ -40,50 +39,46 @@ impl AsRef<[u8]> for SigHash {
}
}
pub(super) struct SigHasher<'a> {
trans: &'a Transaction,
hash_type: HashType,
network_upgrade: NetworkUpgrade,
all_previous_outputs: &'a [transparent::Output],
input_index: Option<usize>,
/// A SigHasher context which stores precomputed data that is reused
/// between sighash computations for the same transaction.
pub struct SigHasher<'a> {
precomputed_tx_data: PrecomputedTxData<'a>,
}
impl<'a> SigHasher<'a> {
/// Create a new SigHasher for the given transaction.
pub fn new(
trans: &'a Transaction,
hash_type: HashType,
network_upgrade: NetworkUpgrade,
branch_id: ConsensusBranchId,
all_previous_outputs: &'a [transparent::Output],
input_index: Option<usize>,
) -> Self {
let precomputed_tx_data = PrecomputedTxData::new(trans, branch_id, all_previous_outputs);
SigHasher {
trans,
hash_type,
network_upgrade,
all_previous_outputs,
input_index,
precomputed_tx_data,
}
}
pub(super) fn sighash(self) -> SigHash {
use NetworkUpgrade::*;
match self.network_upgrade {
Genesis | BeforeOverwinter => unreachable!("{}", ZIP143_EXPLANATION),
Overwinter | Sapling | Blossom | Heartwood | Canopy | Nu5 => {
self.hash_sighash_librustzcash()
}
}
}
/// Compute a signature hash using librustzcash.
fn hash_sighash_librustzcash(&self) -> SigHash {
/// Calculate the sighash for the current transaction.
///
/// # Details
///
/// The `input_index_script_code` tuple indicates the index of the
/// transparent Input for which we are producing a sighash and the
/// respective script code being validated, or None if it's a shielded
/// input.
///
/// # Panics
///
/// - if the input index is out of bounds for self.inputs()
pub fn sighash(
&self,
hash_type: HashType,
input_index_script_code: Option<(usize, Vec<u8>)>,
) -> SigHash {
sighash(
self.trans,
self.hash_type,
self.network_upgrade,
self.all_previous_outputs,
self.input_index,
&self.precomputed_tx_data,
hash_type,
input_index_script_code,
)
}
}

View File

@ -7,6 +7,7 @@ use lazy_static::lazy_static;
use crate::{
block::{Block, Height, MAX_BLOCK_BYTES},
parameters::Network,
primitives::zcash_primitives::PrecomputedTxData,
serialization::{SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize},
transaction::{sighash::SigHasher, txid::TxIdBuilder},
transparent::Script,
@ -607,13 +608,11 @@ fn test_vec143_1() -> Result<()> {
let hasher = SigHasher::new(
&transaction,
HashType::ALL,
NetworkUpgrade::Overwinter,
NetworkUpgrade::Overwinter.branch_id().unwrap(),
&[],
None,
);
let hash = hasher.sighash();
let hash = hasher.sighash(HashType::ALL, None);
let expected = "a1f1a4e5cd9bd522322d661edd2af1bf2a7019cfab94ece18f4ba935b0a19073";
let result = hex::encode(hash);
let span = tracing::span!(
@ -637,18 +636,22 @@ fn test_vec143_2() -> Result<()> {
let value = hex::decode("2f6e04963b4c0100")?.zcash_deserialize_into::<Amount<_>>()?;
let lock_script = Script::new(&hex::decode("53")?);
let input_ind = 1;
let output = transparent::Output { value, lock_script };
let output = transparent::Output {
value,
lock_script: lock_script.clone(),
};
let all_previous_outputs = mock_pre_v5_output_list(output, input_ind);
let hasher = SigHasher::new(
&transaction,
HashType::SINGLE,
NetworkUpgrade::Overwinter,
NetworkUpgrade::Overwinter.branch_id().unwrap(),
&all_previous_outputs,
Some(input_ind),
);
let hash = hasher.sighash();
let hash = hasher.sighash(
HashType::SINGLE,
Some((input_ind, lock_script.as_raw_bytes().to_vec())),
);
let expected = "23652e76cb13b85a0e3363bb5fca061fa791c40c533eccee899364e6e60bb4f7";
let result: &[u8] = hash.as_ref();
let result = hex::encode(result);
@ -672,13 +675,11 @@ fn test_vec243_1() -> Result<()> {
let hasher = SigHasher::new(
&transaction,
HashType::ALL,
NetworkUpgrade::Sapling,
NetworkUpgrade::Sapling.branch_id().unwrap(),
&[],
None,
);
let hash = hasher.sighash();
let hash = hasher.sighash(HashType::ALL, None);
let expected = "63d18534de5f2d1c9e169b73f9c783718adbef5c8a7d55b5e7a37affa1dd3ff3";
let result = hex::encode(hash);
let span = tracing::span!(
@ -690,13 +691,13 @@ fn test_vec243_1() -> Result<()> {
let _guard = span.enter();
assert_eq!(expected, result);
let alt_sighash = crate::primitives::zcash_primitives::sighash(
let precomputed_tx_data = PrecomputedTxData::new(
&transaction,
HashType::ALL,
NetworkUpgrade::Sapling,
NetworkUpgrade::Sapling.branch_id().unwrap(),
&[],
None,
);
let alt_sighash =
crate::primitives::zcash_primitives::sighash(&precomputed_tx_data, HashType::ALL, None);
let result = hex::encode(alt_sighash);
assert_eq!(expected, result);
@ -712,18 +713,22 @@ fn test_vec243_2() -> Result<()> {
let value = hex::decode("adedf02996510200")?.zcash_deserialize_into::<Amount<_>>()?;
let lock_script = Script::new(&[]);
let input_ind = 1;
let output = transparent::Output { value, lock_script };
let output = transparent::Output {
value,
lock_script: lock_script.clone(),
};
let all_previous_outputs = mock_pre_v5_output_list(output, input_ind);
let hasher = SigHasher::new(
&transaction,
HashType::NONE,
NetworkUpgrade::Sapling,
NetworkUpgrade::Sapling.branch_id().unwrap(),
&all_previous_outputs,
Some(input_ind),
);
let hash = hasher.sighash();
let hash = hasher.sighash(
HashType::NONE,
Some((input_ind, lock_script.as_raw_bytes().to_vec())),
);
let expected = "bbe6d84f57c56b29b914c694baaccb891297e961de3eb46c68e3c89c47b1a1db";
let result = hex::encode(hash);
let span = tracing::span!(
@ -736,16 +741,22 @@ fn test_vec243_2() -> Result<()> {
assert_eq!(expected, result);
let lock_script = Script::new(&[]);
let prevout = transparent::Output { value, lock_script };
let prevout = transparent::Output {
value,
lock_script: lock_script.clone(),
};
let index = input_ind;
let all_previous_outputs = mock_pre_v5_output_list(prevout, input_ind);
let alt_sighash = crate::primitives::zcash_primitives::sighash(
let precomputed_tx_data = PrecomputedTxData::new(
&transaction,
HashType::NONE,
NetworkUpgrade::Sapling,
NetworkUpgrade::Sapling.branch_id().unwrap(),
&all_previous_outputs,
Some(index),
);
let alt_sighash = crate::primitives::zcash_primitives::sighash(
&precomputed_tx_data,
HashType::NONE,
Some((index, lock_script.as_raw_bytes().to_vec())),
);
let result = hex::encode(alt_sighash);
assert_eq!(expected, result);
@ -764,17 +775,21 @@ fn test_vec243_3() -> Result<()> {
"76a914507173527b4c3318a2aecd793bf1cfed705950cf88ac",
)?);
let input_ind = 0;
let all_previous_outputs = vec![transparent::Output { value, lock_script }];
let all_previous_outputs = vec![transparent::Output {
value,
lock_script: lock_script.clone(),
}];
let hasher = SigHasher::new(
&transaction,
HashType::ALL,
NetworkUpgrade::Sapling,
NetworkUpgrade::Sapling.branch_id().unwrap(),
&all_previous_outputs,
Some(input_ind),
);
let hash = hasher.sighash();
let hash = hasher.sighash(
HashType::ALL,
Some((input_ind, lock_script.as_raw_bytes().to_vec())),
);
let expected = "f3148f80dfab5e573d5edfe7a850f5fd39234f80b5429d3a57edcc11e34c585b";
let result = hex::encode(hash);
let span = tracing::span!(
@ -789,15 +804,22 @@ fn test_vec243_3() -> Result<()> {
let lock_script = Script::new(&hex::decode(
"76a914507173527b4c3318a2aecd793bf1cfed705950cf88ac",
)?);
let prevout = transparent::Output { value, lock_script };
let prevout = transparent::Output {
value,
lock_script: lock_script.clone(),
};
let index = input_ind;
let alt_sighash = crate::primitives::zcash_primitives::sighash(
let all_previous_outputs = &[prevout];
let precomputed_tx_data = PrecomputedTxData::new(
&transaction,
NetworkUpgrade::Sapling.branch_id().unwrap(),
all_previous_outputs,
);
let alt_sighash = crate::primitives::zcash_primitives::sighash(
&precomputed_tx_data,
HashType::ALL,
NetworkUpgrade::Sapling,
&[prevout],
Some(index),
Some((index, lock_script.as_raw_bytes().to_vec())),
);
let result = hex::encode(alt_sighash);
assert_eq!(expected, result);
@ -821,19 +843,21 @@ fn zip143_sighash() -> Result<()> {
),
None => (None, None),
};
let all_previous_outputs: Vec<_> = match output {
let all_previous_outputs: Vec<_> = match output.clone() {
Some(output) => mock_pre_v5_output_list(output, input_index.unwrap()),
None => vec![],
};
let result = hex::encode(
transaction.sighash(
NetworkUpgrade::from_branch_id(test.consensus_branch_id)
.expect("must be a valid branch ID"),
let result = hex::encode(transaction.sighash(
ConsensusBranchId(test.consensus_branch_id),
HashType::from_bits(test.hash_type).expect("must be a valid HashType"),
&all_previous_outputs,
input_index.map(|input_index| {
(
input_index,
),
);
output.unwrap().lock_script.as_raw_bytes().to_vec(),
)
}),
));
let expected = hex::encode(test.sighash);
assert_eq!(expected, result, "test #{i}: sighash does not match");
}
@ -857,19 +881,21 @@ fn zip243_sighash() -> Result<()> {
),
None => (None, None),
};
let all_previous_outputs: Vec<_> = match output {
let all_previous_outputs: Vec<_> = match output.clone() {
Some(output) => mock_pre_v5_output_list(output, input_index.unwrap()),
None => vec![],
};
let result = hex::encode(
transaction.sighash(
NetworkUpgrade::from_branch_id(test.consensus_branch_id)
.expect("must be a valid branch ID"),
let result = hex::encode(transaction.sighash(
ConsensusBranchId(test.consensus_branch_id),
HashType::from_bits(test.hash_type).expect("must be a valid HashType"),
&all_previous_outputs,
input_index.map(|input_index| {
(
input_index,
),
);
output.unwrap().lock_script.as_raw_bytes().to_vec(),
)
}),
));
let expected = hex::encode(test.sighash);
assert_eq!(expected, result, "test #{i}: sighash does not match");
}
@ -895,7 +921,7 @@ fn zip244_sighash() -> Result<()> {
.collect();
let result = hex::encode(transaction.sighash(
NetworkUpgrade::Nu5,
NetworkUpgrade::Nu5.branch_id().unwrap(),
HashType::ALL,
&all_previous_outputs,
None,
@ -904,12 +930,15 @@ fn zip244_sighash() -> Result<()> {
assert_eq!(expected, result, "test #{i}: sighash does not match");
if let Some(sighash_all) = test.sighash_all {
let result = hex::encode(transaction.sighash(
NetworkUpgrade::Nu5,
let result = hex::encode(
transaction.sighash(
NetworkUpgrade::Nu5.branch_id().unwrap(),
HashType::ALL,
&all_previous_outputs,
test.transparent_input.map(|idx| idx as _),
));
test.transparent_input
.map(|idx| (idx as _, test.script_pubkeys[idx as usize].clone())),
),
);
let expected = hex::encode(sighash_all);
assert_eq!(expected, result, "test #{i}: sighash does not match");
}
@ -944,7 +973,8 @@ fn binding_signatures_for_network(network: Network) {
..
} => {
if let Some(sapling_shielded_data) = sapling_shielded_data {
let shielded_sighash = tx.sighash(upgrade, HashType::ALL, &[], None);
let shielded_sighash =
tx.sighash(upgrade.branch_id().unwrap(), HashType::ALL, &[], None);
let bvk = redjubjub::VerificationKey::try_from(
sapling_shielded_data.binding_verification_key(),
@ -963,7 +993,8 @@ fn binding_signatures_for_network(network: Network) {
..
} => {
if let Some(sapling_shielded_data) = sapling_shielded_data {
let shielded_sighash = tx.sighash(upgrade, HashType::ALL, &[], None);
let shielded_sighash =
tx.sighash(upgrade.branch_id().unwrap(), HashType::ALL, &[], None);
let bvk = redjubjub::VerificationKey::try_from(
sapling_shielded_data.binding_verification_key(),

View File

@ -613,6 +613,7 @@ where
/// - the prepared `cached_ffi_transaction` used by the script verifier
/// - the Sprout `joinsplit_data` shielded data in the transaction
/// - the `sapling_shielded_data` in the transaction
#[allow(clippy::unwrap_in_result)]
fn verify_v4_transaction(
request: &Request,
network: &Network,
@ -627,7 +628,9 @@ where
Self::verify_v4_transaction_network_upgrade(&tx, upgrade)?;
let shielded_sighash = tx.sighash(
upgrade,
upgrade
.branch_id()
.expect("Overwinter-onwards must have branch ID, and we checkpoint on Canopy"),
HashType::ALL,
cached_ffi_transaction.all_previous_outputs(),
None,
@ -705,6 +708,7 @@ where
/// - the prepared `cached_ffi_transaction` used by the script verifier
/// - the sapling shielded data of the transaction, if any
/// - the orchard shielded data of the transaction, if any
#[allow(clippy::unwrap_in_result)]
fn verify_v5_transaction(
request: &Request,
network: &Network,
@ -719,7 +723,9 @@ where
Self::verify_v5_transaction_network_upgrade(&transaction, upgrade)?;
let shielded_sighash = transaction.sighash(
upgrade,
upgrade
.branch_id()
.expect("Overwinter-onwards must have branch ID, and we checkpoint on Canopy"),
HashType::ALL,
cached_ffi_transaction.all_previous_outputs(),
None,

View File

@ -1429,7 +1429,12 @@ fn v4_transaction_with_conflicting_sprout_nullifier_inside_joinsplit_is_rejected
};
// Sign the transaction
let sighash = transaction.sighash(network_upgrade, HashType::ALL, &[], None);
let sighash = transaction.sighash(
network_upgrade.branch_id().expect("must have branch ID"),
HashType::ALL,
&[],
None,
);
match &mut transaction {
Transaction::V4 {
@ -1500,7 +1505,12 @@ fn v4_transaction_with_conflicting_sprout_nullifier_across_joinsplits_is_rejecte
};
// Sign the transaction
let sighash = transaction.sighash(network_upgrade, HashType::ALL, &[], None);
let sighash = transaction.sighash(
network_upgrade.branch_id().expect("must have branch ID"),
HashType::ALL,
&[],
None,
);
match &mut transaction {
Transaction::V4 {

View File

@ -15,8 +15,7 @@ keywords = ["zebra", "zcash"]
categories = ["api-bindings", "cryptography::cryptocurrencies"]
[dependencies]
zcash_script = "0.1.15"
zcash_script = "0.2.0"
zebra-chain = { path = "../zebra-chain", version = "1.0.0-beta.37" }
thiserror = "1.0.61"

View File

@ -6,7 +6,10 @@
#![allow(unsafe_code)]
use core::fmt;
use std::sync::Arc;
use std::{
ffi::{c_int, c_uint, c_void},
sync::Arc,
};
use thiserror::Error;
@ -18,7 +21,8 @@ use zcash_script::{
};
use zebra_chain::{
parameters::ConsensusBranchId, serialization::ZcashSerialize, transaction::Transaction,
parameters::ConsensusBranchId,
transaction::{HashType, SigHasher, Transaction},
transparent,
};
@ -38,6 +42,9 @@ pub enum Error {
/// tx has an invalid size
#[non_exhaustive]
TxSizeMismatch,
/// tx is a coinbase transaction and should not be verified
#[non_exhaustive]
TxCoinbase,
/// unknown error from zcash_script: {0}
#[non_exhaustive]
Unknown(zcash_script_error_t),
@ -50,6 +57,9 @@ impl fmt::Display for Error {
Error::TxDeserialize => "could not deserialize tx".to_owned(),
Error::TxIndex => "input index out of bounds".to_owned(),
Error::TxSizeMismatch => "tx has an invalid size".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}"),
})
}
@ -74,23 +84,47 @@ impl From<zcash_script_error_t> for Error {
pub struct CachedFfiTransaction {
/// The deserialized Zebra transaction.
///
/// This field is private so that `transaction`, `all_previous_outputs`, and `precomputed` always match.
/// This field is private so that `transaction`, and `all_previous_outputs` always match.
transaction: Arc<Transaction>,
/// The outputs from previous transactions that match each input in the transaction
/// being verified.
///
/// SAFETY: this field must be private,
/// and `CachedFfiTransaction::new` must be the only method that modifies it,
/// so that it is [`Send`], [`Sync`], consistent with `transaction` and `precomputed`.
all_previous_outputs: Vec<transparent::Output>,
}
/// The deserialized `zcash_script` transaction, as a C++ object.
///
/// SAFETY: this field must be private,
/// and `CachedFfiTransaction::new` must be the only method that modifies it,
/// so that it is [`Send`], [`Sync`], valid, and not NULL.
precomputed: *mut std::ffi::c_void,
/// A sighash context used for the zcash_script sighash callback.
struct SigHashContext<'a> {
/// The index of the input being verified.
input_index: usize,
/// The SigHasher for the transaction being verified.
sighasher: SigHasher<'a>,
}
/// 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 SigHashContext because it is always passed to
// `zcash_script_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 SigHashContext;
let script_code_vec =
std::slice::from_raw_parts(script_code, script_code_len as usize).to_vec();
let sighash = (*ctx).sighasher.sighash(
HashType::from_bits_truncate(hash_type as u32),
Some(((*ctx).input_index, script_code_vec)),
);
// Sanity check; must always be true.
assert_eq!(sighash_out_len, sighash.0.len() as c_uint);
std::ptr::copy_nonoverlapping(sighash.0.as_ptr(), sighash_out, sighash.0.len());
}
}
impl CachedFfiTransaction {
@ -101,52 +135,9 @@ impl CachedFfiTransaction {
transaction: Arc<Transaction>,
all_previous_outputs: Vec<transparent::Output>,
) -> Self {
let tx_to_serialized = transaction
.zcash_serialize_to_vec()
.expect("serialization into a vec is infallible");
let tx_to_serialized_ptr = tx_to_serialized.as_ptr();
let tx_to_serialized_len = tx_to_serialized
.len()
.try_into()
.expect("serialized transaction lengths are much less than u32::MAX");
let mut err = 0;
let all_previous_outputs_serialized = all_previous_outputs
.zcash_serialize_to_vec()
.expect("serialization into a vec is infallible");
let all_previous_outputs_serialized_ptr = all_previous_outputs_serialized.as_ptr();
let all_previous_outputs_serialized_len: u32 = all_previous_outputs_serialized
.len()
.try_into()
.expect("serialized transaction lengths are much less than u32::MAX");
// SAFETY:
// the `tx_to_*` fields are created from a valid Rust `Vec`
// the `all_previous_outputs_*` fields are created from a valid Rust `Vec`
let precomputed = unsafe {
zcash_script::zcash_script_new_precomputed_tx_v5(
tx_to_serialized_ptr,
tx_to_serialized_len,
all_previous_outputs_serialized_ptr,
all_previous_outputs_serialized_len,
&mut err,
)
};
// SAFETY: the safety of other methods depends on `precomputed` being valid and not NULL.
assert!(
!precomputed.is_null(),
"zcash_script_new_precomputed_tx returned {} ({})",
err,
Error::from(err)
);
Self {
transaction,
all_previous_outputs,
// SAFETY: `precomputed` must not be modified after initialisation,
// so that it is `Send` and `Sync`.
precomputed,
}
}
@ -171,7 +162,10 @@ impl CachedFfiTransaction {
.get(input_index)
.ok_or(Error::TxIndex)?
.clone();
let transparent::Output { value, lock_script } = previous_output;
let transparent::Output {
value: _,
lock_script,
} = previous_output;
let script_pub_key: &[u8] = lock_script.as_raw_bytes();
// This conversion is useful on some platforms, but not others.
@ -180,11 +174,6 @@ impl CachedFfiTransaction {
.try_into()
.expect("transaction indexes are much less than c_uint::MAX");
let script_ptr = script_pub_key.as_ptr();
let script_len = script_pub_key.len();
let amount = value.into();
let flags = zcash_script::zcash_script_SCRIPT_FLAGS_VERIFY_P2SH
| zcash_script::zcash_script_SCRIPT_FLAGS_VERIFY_CHECKLOCKTIMEVERIFY;
// This conversion is useful on some platforms, but not others.
@ -193,23 +182,38 @@ impl CachedFfiTransaction {
.try_into()
.expect("zcash_script_SCRIPT_FLAGS_VERIFY_* enum values fit in a c_uint");
let consensus_branch_id = branch_id.into();
let mut err = 0;
let lock_time = self.transaction.raw_lock_time() as i64;
let is_final = if self.transaction.inputs()[input_index].sequence() == u32::MAX {
1
} else {
0
};
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)?,
};
// SAFETY: `CachedFfiTransaction::new` makes sure `self.precomputed` is not NULL.
// The `script_*` fields are created from a valid Rust `slice`.
let ctx = Box::new(SigHashContext {
input_index: n_in,
sighasher: SigHasher::new(&self.transaction, branch_id, &self.all_previous_outputs),
});
// SAFETY: The `script_*` fields are created from a valid Rust `slice`.
let ret = unsafe {
zcash_script::zcash_script_verify_precomputed(
self.precomputed,
n_in,
script_ptr,
script_len
.try_into()
.expect("script lengths are much less than u32::MAX"),
amount,
zcash_script::zcash_script_verify_callback(
(&*ctx as *const SigHashContext) 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,
consensus_branch_id,
&mut err,
)
};
@ -225,64 +229,40 @@ impl CachedFfiTransaction {
/// transparent inputs and outputs of this transaction.
#[allow(clippy::unwrap_in_result)]
pub fn legacy_sigop_count(&self) -> Result<u64, Error> {
let mut err = 0;
let mut count: u64 = 0;
// SAFETY: `CachedFfiTransaction::new` makes sure `self.precomputed` is not NULL.
for input in self.transaction.inputs() {
count += match input {
transparent::Input::PrevOut {
outpoint: _,
unlock_script,
sequence: _,
} => {
let script = unlock_script.as_raw_bytes();
// SAFETY: `script` is created from a valid Rust `slice`.
unsafe {
zcash_script::zcash_script_legacy_sigop_count_script(
script.as_ptr(),
script.len() as u32,
)
}
}
transparent::Input::Coinbase { .. } => 0,
} as u64;
}
for output in self.transaction.outputs() {
let script = output.lock_script.as_raw_bytes();
// SAFETY: `script` is created from a valid Rust `slice`.
let ret = unsafe {
zcash_script::zcash_script_legacy_sigop_count_precomputed(self.precomputed, &mut err)
zcash_script::zcash_script_legacy_sigop_count_script(
script.as_ptr(),
script.len() as u32,
)
};
if err == zcash_script_error_t_zcash_script_ERR_OK {
let ret = ret.into();
Ok(ret)
} else {
Err(Error::from(err))
count += ret as u64;
}
}
}
// # SAFETY
//
// ## Justification
//
// `CachedFfiTransaction` is not `Send` and `Sync` by default because of the
// `*mut c_void` it contains. This is because raw pointers could allow the same
// data to be mutated from different threads if copied.
//
// CachedFFiTransaction needs to be Send and Sync to be stored within a `Box<dyn
// Future + Send + Sync + static>`. In `zebra_consensus/src/transaction.rs`, an
// async block owns a `CachedFfiTransaction`, and holds it across an await
// point, while the transaction verifier is spawning all of the script verifier
// futures. The service readiness check requires this await between each task
// spawn. Each `script` future needs a copy of the
// `Arc<CachedFfiTransaction>` so that it can simultaneously verify inputs
// without cloning the c++ allocated type unnecessarily.
//
// ## Explanation
//
// It is safe for us to mark this as `Send` and `Sync` because the data pointed
// to by `precomputed` is never modified after it is constructed and points to
// heap memory with a stable memory location. The function
// `zcash_script::zcash_script_verify_precomputed` only reads from the
// precomputed context while verifying inputs, which makes it safe to treat this
// pointer like a shared reference (given that is how it is used).
//
// The function `zcash_script:zcash_script_legacy_sigop_count_precomputed` only reads
// from the precomputed context. Currently, these reads happen after all the concurrent
// async checks have finished.
//
// Since we're manually marking it as `Send` and `Sync`, we must ensure that
// other fields in the struct are also `Send` and `Sync`. This applies to
// `all_previous_outputs`, which are both.
//
// TODO: create a wrapper for `precomputed` and only make it implement Send/Sync (#3436)
unsafe impl Send for CachedFfiTransaction {}
unsafe impl Sync for CachedFfiTransaction {}
impl Drop for CachedFfiTransaction {
fn drop(&mut self) {
// SAFETY: `CachedFfiTransaction::new` makes sure `self.precomputed` is not NULL.
unsafe { zcash_script::zcash_script_free_precomputed_tx(self.precomputed) };
Ok(count)
}
}
@ -292,8 +272,9 @@ mod tests {
use std::sync::Arc;
use zebra_chain::{
parameters::{ConsensusBranchId, NetworkUpgrade::*},
serialization::ZcashDeserializeInto,
transparent,
serialization::{ZcashDeserialize, ZcashDeserializeInto},
transaction::Transaction,
transparent::{self, Output},
};
use zebra_test::prelude::*;
@ -480,4 +461,22 @@ mod tests {
Ok(())
}
#[test]
fn p2sh() -> Result<()> {
let _init_guard = zebra_test::init();
// real tx with txid 51ded0b026f1ff56639447760bcd673b9f4e44a8afbf3af1dbaa6ca1fd241bea
let serialized_tx = "0400008085202f8901c21354bf2305e474ad695382e68efc06e2f8b83c512496f615d153c2e00e688b00000000fdfd0000483045022100d2ab3e6258fe244fa442cfb38f6cef9ac9a18c54e70b2f508e83fa87e20d040502200eead947521de943831d07a350e45af8e36c2166984a8636f0a8811ff03ed09401473044022013e15d865010c257eef133064ef69a780b4bc7ebe6eda367504e806614f940c3022062fdbc8c2d049f91db2042d6c9771de6f1ef0b3b1fea76c1ab5542e44ed29ed8014c69522103b2cc71d23eb30020a4893982a1e2d352da0d20ee657fa02901c432758909ed8f21029d1e9a9354c0d2aee9ffd0f0cea6c39bbf98c4066cf143115ba2279d0ba7dabe2103e32096b63fd57f3308149d238dcbb24d8d28aad95c0e4e74e3e5e6a11b61bcc453aeffffffff0250954903000000001976a914a5a4e1797dac40e8ce66045d1a44c4a63d12142988acccf41c590000000017a9141c973c68b2acc6d6688eff9c7a9dd122ac1346ab8786c72400000000000000000000000000000000";
let serialized_output = "4065675c0000000017a914c117756dcbe144a12a7c33a77cfa81aa5aeeb38187";
let tx = Transaction::zcash_deserialize(&hex::decode(serialized_tx).unwrap().to_vec()[..])
.unwrap();
let previous_output =
Output::zcash_deserialize(&hex::decode(serialized_output).unwrap().to_vec()[..])
.unwrap();
let verifier = super::CachedFfiTransaction::new(Arc::new(tx), vec![previous_output]);
verifier.is_valid(Nu5.branch_id().unwrap(), 0)?;
Ok(())
}
}