Skip to content

Commit

Permalink
Refactor Sprout Join Split validation by transaction verifier (#2371)
Browse files Browse the repository at this point in the history
* Refactor to create `verify_sprout_shielded_data`

Move the join split verification code into a new
`verify_sprout_shielded_data` helper method that returns an
`AsyncChecks` set.

* Test if signed V4 tx. join splits are accepted

Create a fake V4 transaction with a dummy join split, and sign it
appropriately. Check if the transaction verifier accepts the
transaction.

* Test if unsigned V4 tx. joinsplit data is rejected

Create a fake V4 transaction with a dummy join split. Do NOT sign this
transaction's join split data, and check that the verifier rejects the
transaction.

* Join tests to share Tokio runtime

Otherwise one of the tests might fail incorrectly because of a
limitation in the test environment. `Batch` services spawn a task in the
Tokio runtime, but separate tests can have separate runtimes, so sharing
a `Batch` service can lead to the worker task only being available for
one of the tests.
  • Loading branch information
jvff authored Jun 25, 2021
1 parent df7075e commit fdeb6d5
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 33 deletions.
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 @@ -35,6 +35,7 @@ wagyu-zcash-parameters = "0.2.0"

[dev-dependencies]
color-eyre = "0.5.11"
rand07 = { package = "rand", version = "0.7" }
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 @@ -250,7 +250,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 @@ -270,36 +269,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 @@ -498,6 +471,42 @@ where
Ok(script_checks)
}

/// 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)
}

0 comments on commit fdeb6d5

Please sign in to comment.