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:
Conrado Gouvea 2021-11-11 19:18:37 -03:00 committed by GitHub
parent a7299aa7f7
commit 6570ebeeb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 4530 additions and 5 deletions

3
Cargo.lock generated
View File

@ -4471,6 +4471,7 @@ dependencies = [
"itertools 0.10.1", "itertools 0.10.1",
"jubjub 0.7.0", "jubjub 0.7.0",
"lazy_static", "lazy_static",
"orchard",
"proptest", "proptest",
"proptest-derive", "proptest-derive",
"rand 0.8.4", "rand 0.8.4",
@ -4489,6 +4490,7 @@ dependencies = [
"uint", "uint",
"x25519-dalek", "x25519-dalek",
"zcash_history", "zcash_history",
"zcash_note_encryption",
"zcash_primitives", "zcash_primitives",
"zebra-test", "zebra-test",
] ]
@ -4509,6 +4511,7 @@ dependencies = [
"displaydoc", "displaydoc",
"futures 0.3.17", "futures 0.3.17",
"futures-util", "futures-util",
"halo2",
"jubjub 0.7.0", "jubjub 0.7.0",
"lazy_static", "lazy_static",
"metrics", "metrics",

View File

@ -33,6 +33,7 @@ hex = "0.4"
incrementalmerkletree = "0.1.0" incrementalmerkletree = "0.1.0"
jubjub = "0.7.0" jubjub = "0.7.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
orchard = { git = "https://github.com/zcash/orchard.git", rev = "2c8241f25b943aa05203eacf9905db117c69bd29" }
rand_core = "0.6" rand_core = "0.6"
ripemd160 = "0.9" ripemd160 = "0.9"
secp256k1 = { version = "0.20.3", features = ["serde"] } secp256k1 = { version = "0.20.3", features = ["serde"] }
@ -45,6 +46,7 @@ uint = "0.9.1"
x25519-dalek = { version = "1.1", features = ["serde"] } x25519-dalek = { version = "1.1", features = ["serde"] }
zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "53d0a51d33a421cb76d3e3124d1e4c1c9036068e" } zcash_history = { git = "https://github.com/zcash/librustzcash.git", rev = "53d0a51d33a421cb76d3e3124d1e4c1c9036068e" }
zcash_primitives = { 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 = { version = "0.10", optional = true }
proptest-derive = { version = "0.3.0", optional = true } proptest-derive = { version = "0.3.0", optional = true }

View File

@ -20,5 +20,5 @@ pub use action::Action;
pub use address::Address; pub use address::Address;
pub use commitment::{CommitmentRandomness, NoteCommitment, ValueCommitment}; pub use commitment::{CommitmentRandomness, NoteCommitment, ValueCommitment};
pub use keys::Diversifier; pub use keys::Diversifier;
pub use note::{EncryptedNote, Note, Nullifier}; pub use note::{EncryptedNote, Note, Nullifier, WrappedNoteKey};
pub use shielded_data::{AuthorizedAction, Flags, ShieldedData}; pub use shielded_data::{AuthorizedAction, Flags, ShieldedData};

View File

@ -15,4 +15,5 @@ pub use x25519_dalek as x25519;
pub use proofs::{Bctv14Proof, Groth16Proof, Halo2Proof, ZkSnarkProof}; pub use proofs::{Bctv14Proof, Groth16Proof, Halo2Proof, ZkSnarkProof};
pub mod zcash_history; pub mod zcash_history;
pub mod zcash_note_encryption;
pub(crate) mod zcash_primitives; pub(crate) mod zcash_primitives;

View File

@ -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
}

View File

@ -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, trans: &Transaction,
network_upgrade: NetworkUpgrade, network_upgrade: NetworkUpgrade,
) -> Result<zcash_primitives::transaction::Transaction, io::Error> { ) -> Result<zcash_primitives::transaction::Transaction, io::Error> {

View File

@ -42,6 +42,7 @@ proptest-derive = { version = "0.3.0", optional = true }
[dev-dependencies] [dev-dependencies]
color-eyre = "0.5.11" color-eyre = "0.5.11"
halo2 = "=0.1.0-beta.1"
proptest = "0.10" proptest = "0.10"
proptest-derive = "0.3.0" proptest-derive = "0.3.0"
rand07 = { package = "rand", version = "0.7" } rand07 = { package = "rand", version = "0.7" }

View File

@ -169,6 +169,12 @@ where
check::time_is_valid_at(&block.header, now, &height, &hash) check::time_is_valid_at(&block.header, now, &height, &hash)
.map_err(VerifyBlockError::Time)?; .map_err(VerifyBlockError::Time)?;
check::coinbase_is_first(&block)?; 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)?; check::subsidy_is_valid(&block, network)?;
let mut async_checks = FuturesUnordered::new(); let mut async_checks = FuturesUnordered::new();

View File

@ -50,6 +50,9 @@ pub enum TransactionError {
#[error("coinbase transaction MUST NOT have the EnableSpendsOrchard flag set")] #[error("coinbase transaction MUST NOT have the EnableSpendsOrchard flag set")]
CoinbaseHasEnableSpendsOrchard, 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")] #[error("coinbase inputs MUST NOT exist in mempool")]
CoinbaseInMempool, CoinbaseInMempool,

View File

@ -34,7 +34,7 @@ use zebra_state as zs;
use crate::{error::TransactionError, primitives, script, BoxError}; use crate::{error::TransactionError, primitives, script, BoxError};
mod check; pub mod check;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;

View File

@ -9,6 +9,7 @@ use zebra_chain::{
block::Height, block::Height,
orchard::Flags, orchard::Flags,
parameters::{Network, NetworkUpgrade}, parameters::{Network, NetworkUpgrade},
primitives::zcash_note_encryption,
sapling::{Output, PerSpendAnchor, Spend}, sapling::{Output, PerSpendAnchor, Spend},
transaction::Transaction, transaction::Transaction,
}; };
@ -208,3 +209,48 @@ where
Ok(()) 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(())
}

View File

@ -1,14 +1,16 @@
use std::{collections::HashMap, convert::TryFrom, convert::TryInto, sync::Arc}; use std::{collections::HashMap, convert::TryFrom, convert::TryInto, sync::Arc};
use halo2::{arithmetic::FieldExt, pasta::pallas};
use tower::{service_fn, ServiceExt}; use tower::{service_fn, ServiceExt};
use zebra_chain::{ use zebra_chain::{
amount::{Amount, NonNegative}, amount::{Amount, NonNegative},
block, orchard, block::{self, Block, Height},
orchard::{self, AuthorizedAction, EncryptedNote, WrappedNoteKey},
parameters::{Network, NetworkUpgrade}, parameters::{Network, NetworkUpgrade},
primitives::{ed25519, x25519, Groth16Proof}, primitives::{ed25519, x25519, Groth16Proof},
sapling, sapling,
serialization::ZcashDeserialize, serialization::{ZcashDeserialize, ZcashDeserializeInto},
sprout, sprout,
transaction::{ transaction::{
arbitrary::{ arbitrary::{
@ -1542,3 +1544,173 @@ fn add_to_sprout_pool_after_nu() {
Ok(()) 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)
);
}
}

View File

@ -4,7 +4,10 @@ use hex::FromHex;
use lazy_static::lazy_static; use lazy_static::lazy_static;
mod block; mod block;
mod orchard_note_encryption;
pub use block::*; pub use block::*;
pub use orchard_note_encryption::*;
/// A testnet transaction test vector /// A testnet transaction test vector
/// ///

File diff suppressed because it is too large Load Diff