Skip to content
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

Refactor Sprout Join Split validation by transaction verifier #2371

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions zebra-consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ wagyu-zcash-parameters = "0.2.0"

[dev-dependencies]
color-eyre = "0.5.11"
rand07 = { package = "rand", version = "0.7" }
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
spandoc = "0.2"
tokio = { version = "0.3.6", features = ["full"] }
tracing-error = "0.1.2"
Expand Down
71 changes: 40 additions & 31 deletions zebra-consensus/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,6 @@ where
let mut spend_verifier = primitives::groth16::SPEND_VERIFIER.clone();
let mut output_verifier = primitives::groth16::OUTPUT_VERIFIER.clone();

let mut ed25519_verifier = primitives::ed25519::VERIFIER.clone();
let mut redjubjub_verifier = primitives::redjubjub::VERIFIER.clone();

// A set of asynchronous checks which must all succeed.
Expand All @@ -255,36 +254,10 @@ where

let shielded_sighash = tx.sighash(upgrade, HashType::ALL, None);

if let Some(joinsplit_data) = joinsplit_data {
// XXX create a method on JoinSplitData
// that prepares groth16::Items with the correct proofs
// and proof inputs, handling interstitial treestates
// correctly.

// Then, pass those items to self.joinsplit to verify them.

// Consensus rule: The joinSplitSig MUST represent a
// valid signature, under joinSplitPubKey, of the
// sighash.
//
// Queue the validation of the JoinSplit signature while
// adding the resulting future to our collection of
// async checks that (at a minimum) must pass for the
// transaction to verify.
//
// https://zips.z.cash/protocol/protocol.pdf#sproutnonmalleability
// https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
let rsp = ed25519_verifier.ready_and().await?.call(
(
joinsplit_data.pub_key,
joinsplit_data.sig,
&shielded_sighash,
)
.into(),
);

async_checks.push(rsp.boxed());
}
async_checks.extend(Self::verify_sprout_shielded_data(
joinsplit_data,
&shielded_sighash,
));

if let Some(sapling_shielded_data) = sapling_shielded_data {
for spend in sapling_shielded_data.spends_per_anchor() {
Expand Down Expand Up @@ -489,6 +462,42 @@ where
}
}

/// Verifies a transaction's Sprout shielded join split data.
fn verify_sprout_shielded_data(
joinsplit_data: &Option<transaction::JoinSplitData<Groth16Proof>>,
shielded_sighash: &blake2b_simd::Hash,
) -> AsyncChecks {
let checks = AsyncChecks::new();

if let Some(joinsplit_data) = joinsplit_data {
// XXX create a method on JoinSplitData
// that prepares groth16::Items with the correct proofs
// and proof inputs, handling interstitial treestates
// correctly.

// Then, pass those items to self.joinsplit to verify them.

// Consensus rule: The joinSplitSig MUST represent a
// valid signature, under joinSplitPubKey, of the
// sighash.
//
// Queue the validation of the JoinSplit signature while
// adding the resulting future to our collection of
// async checks that (at a minimum) must pass for the
// transaction to verify.
//
// https://zips.z.cash/protocol/protocol.pdf#sproutnonmalleability
// https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
let ed25519_verifier = primitives::ed25519::VERIFIER.clone();
let ed25519_item =
(joinsplit_data.pub_key, joinsplit_data.sig, shielded_sighash).into();

checks.push(ed25519_verifier.oneshot(ed25519_item).boxed());
}

checks
}

/// Await a set of checks that should all succeed.
///
/// If any of the checks fail, this method immediately returns the error and cancels all other
Expand Down
140 changes: 138 additions & 2 deletions zebra-consensus/src/transaction/tests.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
use std::{collections::HashMap, convert::TryFrom, sync::Arc};
use std::{collections::HashMap, convert::TryFrom, convert::TryInto, sync::Arc};

use tower::{service_fn, ServiceExt};

use zebra_chain::{
amount::Amount,
block, orchard,
parameters::{Network, NetworkUpgrade},
primitives::{ed25519, x25519, Groth16Proof},
serialization::ZcashDeserialize,
sprout,
transaction::{
arbitrary::{fake_v5_transactions_for_network, insert_fake_orchard_shielded_data},
Hash, LockTime, Transaction,
Hash, HashType, JoinSplitData, LockTime, Transaction,
},
transparent,
};
Expand Down Expand Up @@ -451,6 +454,86 @@ async fn v5_transaction_with_transparent_transfer_is_rejected_by_the_script() {
);
}

/// Tests transactions with Sprout transfers.
///
/// This is actually two tests:
/// - Test if signed V4 transaction with a dummy [`sprout::JoinSplit`] is accepted.
/// - Test if an unsigned V4 transaction with a dummy [`sprout::JoinSplit`] is rejected.
///
/// The first test verifies if the transaction verifier correctly accepts a signed transaction. The
/// second test verifies if the transaction verifier correctly rejects the transaction because of
/// the invalid signature.
///
/// These tests are grouped together because of a limitation to test shared [`tower_batch::Batch`]
/// services. Such services spawn a Tokio task in the runtime, and `#[tokio::test]` can create a
/// separate runtime for each test. This means that the worker task is created for one test and
/// destroyed before the other gets a chance to use it. (We'll fix this in #2390.)
#[tokio::test]
async fn v4_with_sprout_transfers() {
let network = Network::Mainnet;
let network_upgrade = NetworkUpgrade::Canopy;

let canopy_activation_height = network_upgrade
.activation_height(network)
.expect("Canopy activation height is not set");

let transaction_block_height =
(canopy_activation_height + 10).expect("Canopy activation height is too large");

// Initialize the verifier
let state_service =
service_fn(|_| async { unreachable!("State service should not be called") });
let script_verifier = script::Verifier::new(state_service);
let verifier = Verifier::new(network, script_verifier);

for should_sign in [true, false] {
// Create a fake Sprout join split
let (joinsplit_data, signing_key) = mock_sprout_join_split_data();

let mut transaction = Transaction::V4 {
inputs: vec![],
outputs: vec![],
lock_time: LockTime::Height(block::Height(0)),
expiry_height: (transaction_block_height + 1).expect("expiry height is too large"),
joinsplit_data: Some(joinsplit_data),
sapling_shielded_data: None,
};

let expected_result = if should_sign {
// Sign the transaction
let sighash = transaction.sighash(network_upgrade, HashType::ALL, None);

match &mut transaction {
Transaction::V4 {
joinsplit_data: Some(joinsplit_data),
..
} => joinsplit_data.sig = signing_key.sign(sighash.as_bytes()),
_ => unreachable!("Mock transaction was created incorrectly"),
}

Ok(transaction.hash())
} else {
// TODO: Fix error downcast
// Err(TransactionError::Ed25519(ed25519::Error::InvalidSignature))
Err(TransactionError::InternalDowncastError(
"downcast to redjubjub::Error failed, original error: InvalidSignature".to_string(),
))
};

// Test the transaction verifier
let result = verifier
.clone()
.oneshot(Request::Block {
transaction: Arc::new(transaction),
known_utxos: Arc::new(HashMap::new()),
height: transaction_block_height,
})
.await;

assert_eq!(result, expected_result);
}
}

// Utility functions

/// Create a mock transparent transfer to be included in a transaction.
Expand Down Expand Up @@ -524,3 +607,56 @@ fn mock_transparent_transfer(

(input, output, known_utxos)
}

/// Create a mock [`sprout::JoinSplit`] and include it in a [`transaction::JoinSplitData`].
///
/// This creates a dummy join split. By itself it is invalid, but it is useful for including in a
/// transaction to check the signatures.
///
/// The [`transaction::JoinSplitData`] with the dummy [`sprout::JoinSplit`] is returned together
/// with the [`ed25519::SigningKey`] that can be used to create a signature to later add to the
/// returned join split data.
fn mock_sprout_join_split_data() -> (JoinSplitData<Groth16Proof>, ed25519::SigningKey) {
// Prepare dummy inputs for the join split
let zero_amount = 0_i32
.try_into()
.expect("Invalid JoinSplit transparent input");
let anchor = sprout::tree::Root::default();
let nullifier = sprout::note::Nullifier([0u8; 32]);
let commitment = sprout::commitment::NoteCommitment::from([0u8; 32]);
let ephemeral_key =
x25519::PublicKey::from(&x25519::EphemeralSecret::new(rand07::thread_rng()));
let random_seed = [0u8; 32];
let mac = sprout::note::Mac::zcash_deserialize(&[0u8; 32][..])
.expect("Failure to deserialize dummy MAC");
let zkproof = Groth16Proof([0u8; 192]);
let encrypted_note = sprout::note::EncryptedNote([0u8; 601]);

// Create an dummy join split
let joinsplit = sprout::JoinSplit {
vpub_old: zero_amount,
vpub_new: zero_amount,
anchor,
nullifiers: [nullifier; 2],
commitments: [commitment; 2],
ephemeral_key,
random_seed,
vmacs: [mac.clone(), mac],
zkproof,
enc_ciphertexts: [encrypted_note; 2],
};

// Create a usable signing key
let signing_key = ed25519::SigningKey::new(rand::thread_rng());
let verification_key = ed25519::VerificationKey::from(&signing_key);

// Populate join split data with the dummy join split.
let joinsplit_data = JoinSplitData {
first: joinsplit,
rest: vec![],
pub_key: verification_key.into(),
sig: [0u8; 64].into(),
};

(joinsplit_data, signing_key)
}