ZIP 212: validate Sapling and Orchard output of coinbase transactions (#3029)
* Part of ZIP 212: validate Sapling and Orchard output of coinbase transactions * Add Orchard test vector * Revert accidentally deleted link * Apply suggestions from code review Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com> * Use height from loop * Apply suggestions from code review Co-authored-by: Deirdre Connolly <deirdre@zfnd.org> * Fix formatting Co-authored-by: Alfredo Garcia <oxarbitrage@gmail.com> Co-authored-by: Deirdre Connolly <deirdre@zfnd.org>
This commit is contained in:
parent
a7299aa7f7
commit
6570ebeeb8
|
@ -4471,6 +4471,7 @@ dependencies = [
|
|||
"itertools 0.10.1",
|
||||
"jubjub 0.7.0",
|
||||
"lazy_static",
|
||||
"orchard",
|
||||
"proptest",
|
||||
"proptest-derive",
|
||||
"rand 0.8.4",
|
||||
|
@ -4489,6 +4490,7 @@ dependencies = [
|
|||
"uint",
|
||||
"x25519-dalek",
|
||||
"zcash_history",
|
||||
"zcash_note_encryption",
|
||||
"zcash_primitives",
|
||||
"zebra-test",
|
||||
]
|
||||
|
@ -4509,6 +4511,7 @@ dependencies = [
|
|||
"displaydoc",
|
||||
"futures 0.3.17",
|
||||
"futures-util",
|
||||
"halo2",
|
||||
"jubjub 0.7.0",
|
||||
"lazy_static",
|
||||
"metrics",
|
||||
|
|
|
@ -33,6 +33,7 @@ hex = "0.4"
|
|||
incrementalmerkletree = "0.1.0"
|
||||
jubjub = "0.7.0"
|
||||
lazy_static = "1.4.0"
|
||||
orchard = { git = "https://github.com/zcash/orchard.git", rev = "2c8241f25b943aa05203eacf9905db117c69bd29" }
|
||||
rand_core = "0.6"
|
||||
ripemd160 = "0.9"
|
||||
secp256k1 = { version = "0.20.3", features = ["serde"] }
|
||||
|
@ -45,6 +46,7 @@ uint = "0.9.1"
|
|||
x25519-dalek = { version = "1.1", features = ["serde"] }
|
||||
zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "53d0a51d33a421cb76d3e3124d1e4c1c9036068e" }
|
||||
zcash_primitives = { git = "https://github.com/zcash/librustzcash.git", rev = "53d0a51d33a421cb76d3e3124d1e4c1c9036068e" }
|
||||
zcash_note_encryption = { git = "https://github.com/zcash/librustzcash.git", rev = "53d0a51d33a421cb76d3e3124d1e4c1c9036068e" }
|
||||
|
||||
proptest = { version = "0.10", optional = true }
|
||||
proptest-derive = { version = "0.3.0", optional = true }
|
||||
|
|
|
@ -20,5 +20,5 @@ pub use action::Action;
|
|||
pub use address::Address;
|
||||
pub use commitment::{CommitmentRandomness, NoteCommitment, ValueCommitment};
|
||||
pub use keys::Diversifier;
|
||||
pub use note::{EncryptedNote, Note, Nullifier};
|
||||
pub use note::{EncryptedNote, Note, Nullifier, WrappedNoteKey};
|
||||
pub use shielded_data::{AuthorizedAction, Flags, ShieldedData};
|
||||
|
|
|
@ -15,4 +15,5 @@ pub use x25519_dalek as x25519;
|
|||
pub use proofs::{Bctv14Proof, Groth16Proof, Halo2Proof, ZkSnarkProof};
|
||||
|
||||
pub mod zcash_history;
|
||||
pub mod zcash_note_encryption;
|
||||
pub(crate) mod zcash_primitives;
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
//! Contains code that interfaces with the zcash_note_encryption crate from
|
||||
//! librustzcash.
|
||||
//!
|
||||
use crate::{
|
||||
block::Height,
|
||||
parameters::{Network, NetworkUpgrade},
|
||||
primitives::zcash_primitives::convert_tx_to_librustzcash,
|
||||
transaction::Transaction,
|
||||
};
|
||||
|
||||
/// Returns true if all Sapling or Orchard outputs, if any, decrypt successfully with
|
||||
/// an all-zeroes outgoing viewing key.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If passed a network/height without matching consensus branch ID (pre-Overwinter),
|
||||
/// 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)
|
||||
.expect("zcash_primitives and Zebra transaction formats must be compatible");
|
||||
|
||||
let alt_height = height.0.into();
|
||||
let null_sapling_ovk = zcash_primitives::sapling::keys::OutgoingViewingKey([0u8; 32]);
|
||||
|
||||
if let Some(bundle) = alt_tx.sapling_bundle() {
|
||||
for output in bundle.shielded_outputs.iter() {
|
||||
let recovery = match network {
|
||||
Network::Mainnet => {
|
||||
zcash_primitives::sapling::note_encryption::try_sapling_output_recovery(
|
||||
&zcash_primitives::consensus::MAIN_NETWORK,
|
||||
alt_height,
|
||||
&null_sapling_ovk,
|
||||
output,
|
||||
)
|
||||
}
|
||||
Network::Testnet => {
|
||||
zcash_primitives::sapling::note_encryption::try_sapling_output_recovery(
|
||||
&zcash_primitives::consensus::TEST_NETWORK,
|
||||
alt_height,
|
||||
&null_sapling_ovk,
|
||||
output,
|
||||
)
|
||||
}
|
||||
};
|
||||
if recovery.is_none() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(bundle) = alt_tx.orchard_bundle() {
|
||||
for act in bundle.actions() {
|
||||
if zcash_note_encryption::try_output_recovery_with_ovk(
|
||||
&orchard::note_encryption::OrchardDomain::for_action(act),
|
||||
&orchard::keys::OutgoingViewingKey::from([0u8; 32]),
|
||||
act,
|
||||
act.cv_net(),
|
||||
&act.encrypted_note().out_ciphertext,
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
|
@ -39,7 +39,7 @@ impl TryFrom<&Transaction> for zcash_primitives::transaction::Transaction {
|
|||
}
|
||||
}
|
||||
|
||||
fn convert_tx_to_librustzcash(
|
||||
pub(crate) fn convert_tx_to_librustzcash(
|
||||
trans: &Transaction,
|
||||
network_upgrade: NetworkUpgrade,
|
||||
) -> Result<zcash_primitives::transaction::Transaction, io::Error> {
|
||||
|
|
|
@ -42,6 +42,7 @@ proptest-derive = { version = "0.3.0", optional = true }
|
|||
|
||||
[dev-dependencies]
|
||||
color-eyre = "0.5.11"
|
||||
halo2 = "=0.1.0-beta.1"
|
||||
proptest = "0.10"
|
||||
proptest-derive = "0.3.0"
|
||||
rand07 = { package = "rand", version = "0.7" }
|
||||
|
|
|
@ -169,6 +169,12 @@ where
|
|||
check::time_is_valid_at(&block.header, now, &height, &hash)
|
||||
.map_err(VerifyBlockError::Time)?;
|
||||
check::coinbase_is_first(&block)?;
|
||||
let coinbase_tx = block
|
||||
.transactions
|
||||
.get(0)
|
||||
.expect("must have coinbase transaction");
|
||||
// Check compatibility with ZIP-212 shielded Sapling and Orchard coinbase output decryption
|
||||
tx::check::coinbase_outputs_are_decryptable(coinbase_tx, network, height)?;
|
||||
check::subsidy_is_valid(&block, network)?;
|
||||
|
||||
let mut async_checks = FuturesUnordered::new();
|
||||
|
|
|
@ -50,6 +50,9 @@ pub enum TransactionError {
|
|||
#[error("coinbase transaction MUST NOT have the EnableSpendsOrchard flag set")]
|
||||
CoinbaseHasEnableSpendsOrchard,
|
||||
|
||||
#[error("coinbase transaction Sapling or Orchard outputs MUST be decryptable with an all-zero outgoing viewing key")]
|
||||
CoinbaseOutputsNotDecryptable,
|
||||
|
||||
#[error("coinbase inputs MUST NOT exist in mempool")]
|
||||
CoinbaseInMempool,
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ use zebra_state as zs;
|
|||
|
||||
use crate::{error::TransactionError, primitives, script, BoxError};
|
||||
|
||||
mod check;
|
||||
pub mod check;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ use zebra_chain::{
|
|||
block::Height,
|
||||
orchard::Flags,
|
||||
parameters::{Network, NetworkUpgrade},
|
||||
primitives::zcash_note_encryption,
|
||||
sapling::{Output, PerSpendAnchor, Spend},
|
||||
transaction::Transaction,
|
||||
};
|
||||
|
@ -208,3 +209,48 @@ where
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks compatibility with [ZIP-212] shielded Sapling and Orchard coinbase output decryption
|
||||
///
|
||||
/// Pre-Heartwood: returns `Ok`.
|
||||
/// Heartwood-onward: returns `Ok` if all Sapling or Orchard outputs, if any, decrypt successfully with
|
||||
/// an all-zeroes outgoing viewing key. Returns `Err` otherwise.
|
||||
///
|
||||
/// This is used to validate coinbase transactions:
|
||||
///
|
||||
/// > [Heartwood onward] All Sapling and Orchard outputs in coinbase transactions MUST decrypt to a note
|
||||
/// > plaintext, i.e. the procedure in § 4.19.3 ‘Decryption using a Full Viewing Key ( Sapling and Orchard )’ on p. 67
|
||||
/// > does not return ⊥, using a sequence of 32 zero bytes as the outgoing viewing key. (This implies that before
|
||||
/// > Canopy activation, Sapling outputs of a coinbase transaction MUST have note plaintext lead byte equal to
|
||||
/// > 0x01.)
|
||||
///
|
||||
/// > [Canopy onward] Any Sapling or Orchard output of a coinbase transaction decrypted to a note plaintext
|
||||
/// > according to the preceding rule MUST have note plaintext lead byte equal to 0x02. (This applies even during
|
||||
/// > the "grace period" specified in [ZIP-212].)
|
||||
///
|
||||
/// [3.10]: https://zips.z.cash/protocol/protocol.pdf#coinbasetransactions
|
||||
/// [ZIP-212]: https://zips.z.cash/zip-0212#consensus-rule-change-for-coinbase-transactions
|
||||
///
|
||||
/// TODO: Currently, a 0x01 lead byte is allowed in the "grace period" mentioned since we're
|
||||
/// using `librustzcash` to implement this and it doesn't currently allow changing that behavior.
|
||||
/// https://github.com/ZcashFoundation/zebra/issues/3027
|
||||
pub fn coinbase_outputs_are_decryptable(
|
||||
transaction: &Transaction,
|
||||
network: Network,
|
||||
height: Height,
|
||||
) -> Result<(), TransactionError> {
|
||||
// The consensus rule only applies to Heartwood onward.
|
||||
if height
|
||||
< NetworkUpgrade::Heartwood
|
||||
.activation_height(network)
|
||||
.expect("Heartwood height is known")
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !zcash_note_encryption::decrypts_successfully(transaction, network, height) {
|
||||
return Err(TransactionError::CoinbaseOutputsNotDecryptable);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
use std::{collections::HashMap, convert::TryFrom, convert::TryInto, sync::Arc};
|
||||
|
||||
use halo2::{arithmetic::FieldExt, pasta::pallas};
|
||||
use tower::{service_fn, ServiceExt};
|
||||
|
||||
use zebra_chain::{
|
||||
amount::{Amount, NonNegative},
|
||||
block, orchard,
|
||||
block::{self, Block, Height},
|
||||
orchard::{self, AuthorizedAction, EncryptedNote, WrappedNoteKey},
|
||||
parameters::{Network, NetworkUpgrade},
|
||||
primitives::{ed25519, x25519, Groth16Proof},
|
||||
sapling,
|
||||
serialization::ZcashDeserialize,
|
||||
serialization::{ZcashDeserialize, ZcashDeserializeInto},
|
||||
sprout,
|
||||
transaction::{
|
||||
arbitrary::{
|
||||
|
@ -1542,3 +1544,173 @@ fn add_to_sprout_pool_after_nu() {
|
|||
Ok(())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coinbase_outputs_are_decryptable_for_historical_blocks() -> Result<(), Report> {
|
||||
zebra_test::init();
|
||||
|
||||
coinbase_outputs_are_decryptable_for_historical_blocks_for_network(Network::Mainnet)?;
|
||||
coinbase_outputs_are_decryptable_for_historical_blocks_for_network(Network::Testnet)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn coinbase_outputs_are_decryptable_for_historical_blocks_for_network(
|
||||
network: Network,
|
||||
) -> Result<(), Report> {
|
||||
let block_iter = match network {
|
||||
Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.iter(),
|
||||
Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.iter(),
|
||||
};
|
||||
|
||||
let mut tested_coinbase_txs = 0;
|
||||
let mut tested_non_coinbase_txs = 0;
|
||||
|
||||
for (height, block) in block_iter {
|
||||
let block = block
|
||||
.zcash_deserialize_into::<Block>()
|
||||
.expect("block is structurally valid");
|
||||
let height = Height(*height);
|
||||
let heartwood_onward = height
|
||||
>= NetworkUpgrade::Heartwood
|
||||
.activation_height(network)
|
||||
.unwrap();
|
||||
let coinbase_tx = block
|
||||
.transactions
|
||||
.get(0)
|
||||
.expect("must have coinbase transaction");
|
||||
|
||||
// Check if the coinbase outputs are decryptable with an all-zero key.
|
||||
if heartwood_onward
|
||||
&& (coinbase_tx.sapling_outputs().count() > 0
|
||||
|| coinbase_tx.orchard_actions().count() > 0)
|
||||
{
|
||||
// We are only truly decrypting something if it's Heartwood-onward
|
||||
// and there are relevant outputs.
|
||||
tested_coinbase_txs += 1;
|
||||
}
|
||||
check::coinbase_outputs_are_decryptable(coinbase_tx, network, height)
|
||||
.expect("coinbase outputs must be decryptable with an all-zero key");
|
||||
|
||||
// For remaining transactions, check if existing outputs are NOT decryptable
|
||||
// with an all-zero key, if applicable.
|
||||
for tx in block.transactions.iter().skip(1) {
|
||||
let has_outputs = tx.sapling_outputs().count() > 0 || tx.orchard_actions().count() > 0;
|
||||
if has_outputs && heartwood_onward {
|
||||
tested_non_coinbase_txs += 1;
|
||||
check::coinbase_outputs_are_decryptable(tx, network, height).expect_err(
|
||||
"decrypting a non-coinbase output with an all-zero key should fail",
|
||||
);
|
||||
} else {
|
||||
check::coinbase_outputs_are_decryptable(tx, network, height)
|
||||
.expect("a transaction without outputs, or pre-Heartwood, must be considered 'decryptable'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(tested_coinbase_txs > 0, "ensure it was actually tested");
|
||||
assert!(tested_non_coinbase_txs > 0, "ensure it was actually tested");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Given an Orchard action as a base, fill fields related to note encryption
|
||||
/// from the given test vector and returned the modified action.
|
||||
fn fill_action_with_note_encryption_test_vector(
|
||||
action: &orchard::Action,
|
||||
v: &zebra_test::vectors::TestVector,
|
||||
) -> orchard::Action {
|
||||
let mut action = action.clone();
|
||||
action.cv = v.cv_net.try_into().expect("test vector must be valid");
|
||||
action.cm_x = pallas::Base::from_bytes(&v.cmx).unwrap();
|
||||
action.nullifier = v.rho.try_into().expect("test vector must be valid");
|
||||
action.ephemeral_key = v
|
||||
.ephemeral_key
|
||||
.try_into()
|
||||
.expect("test vector must be valid");
|
||||
action.out_ciphertext = WrappedNoteKey(v.c_out);
|
||||
action.enc_ciphertext = EncryptedNote(v.c_enc);
|
||||
action
|
||||
}
|
||||
|
||||
/// Test if shielded coinbase outputs are decryptable with an all-zero outgoing
|
||||
/// viewing key.
|
||||
#[test]
|
||||
fn coinbase_outputs_are_decryptable_for_fake_v5_blocks() {
|
||||
let network = Network::Testnet;
|
||||
|
||||
for v in zebra_test::vectors::ORCHARD_NOTE_ENCRYPTION_ZERO_VECTOR.iter() {
|
||||
// Find a transaction with no inputs or outputs to use as base
|
||||
let mut transaction =
|
||||
fake_v5_transactions_for_network(network, zebra_test::vectors::TESTNET_BLOCKS.iter())
|
||||
.rev()
|
||||
.find(|transaction| {
|
||||
transaction.inputs().is_empty()
|
||||
&& transaction.outputs().is_empty()
|
||||
&& transaction.sapling_spends_per_anchor().next().is_none()
|
||||
&& transaction.sapling_outputs().next().is_none()
|
||||
&& transaction.joinsplit_count() == 0
|
||||
})
|
||||
.expect("At least one fake V5 transaction with no inputs and no outputs");
|
||||
|
||||
let shielded_data = insert_fake_orchard_shielded_data(&mut transaction);
|
||||
shielded_data.flags = orchard::Flags::ENABLE_SPENDS | orchard::Flags::ENABLE_OUTPUTS;
|
||||
|
||||
let action =
|
||||
fill_action_with_note_encryption_test_vector(&shielded_data.actions[0].action, v);
|
||||
let sig = shielded_data.actions[0].spend_auth_sig;
|
||||
shielded_data.actions = vec![AuthorizedAction::from_parts(action, sig)]
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
check::coinbase_outputs_are_decryptable(
|
||||
&transaction,
|
||||
network,
|
||||
NetworkUpgrade::Nu5.activation_height(network).unwrap(),
|
||||
),
|
||||
Ok(())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test if random shielded outputs are NOT decryptable with an all-zero outgoing
|
||||
/// viewing key.
|
||||
#[test]
|
||||
fn shielded_outputs_are_not_decryptable_for_fake_v5_blocks() {
|
||||
let network = Network::Testnet;
|
||||
|
||||
for v in zebra_test::vectors::ORCHARD_NOTE_ENCRYPTION_VECTOR.iter() {
|
||||
// Find a transaction with no inputs or outputs to use as base
|
||||
let mut transaction =
|
||||
fake_v5_transactions_for_network(network, zebra_test::vectors::TESTNET_BLOCKS.iter())
|
||||
.rev()
|
||||
.find(|transaction| {
|
||||
transaction.inputs().is_empty()
|
||||
&& transaction.outputs().is_empty()
|
||||
&& transaction.sapling_spends_per_anchor().next().is_none()
|
||||
&& transaction.sapling_outputs().next().is_none()
|
||||
&& transaction.joinsplit_count() == 0
|
||||
})
|
||||
.expect("At least one fake V5 transaction with no inputs and no outputs");
|
||||
|
||||
let shielded_data = insert_fake_orchard_shielded_data(&mut transaction);
|
||||
shielded_data.flags = orchard::Flags::ENABLE_SPENDS | orchard::Flags::ENABLE_OUTPUTS;
|
||||
|
||||
let action =
|
||||
fill_action_with_note_encryption_test_vector(&shielded_data.actions[0].action, v);
|
||||
let sig = shielded_data.actions[0].spend_auth_sig;
|
||||
shielded_data.actions = vec![AuthorizedAction::from_parts(action, sig)]
|
||||
.try_into()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
check::coinbase_outputs_are_decryptable(
|
||||
&transaction,
|
||||
network,
|
||||
NetworkUpgrade::Nu5.activation_height(network).unwrap(),
|
||||
),
|
||||
Err(TransactionError::CoinbaseOutputsNotDecryptable)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,10 @@ use hex::FromHex;
|
|||
use lazy_static::lazy_static;
|
||||
|
||||
mod block;
|
||||
mod orchard_note_encryption;
|
||||
|
||||
pub use block::*;
|
||||
pub use orchard_note_encryption::*;
|
||||
|
||||
/// A testnet transaction test vector
|
||||
///
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue