Checking of Sprout anchors in non-finalized state (#3123)

* Do prelim checking of Sprout anchors in non-finalized state

Does not check intra-transaction interstitial states yet

* Populate sprout anchors to allow other state tests to pass

* Preliminary interstitial sprout note commitment tree anchor checks implementation

* Make sure only prior anchors are checked in the same transaction

* Add tests

* Refactor a comment

* Refactor rustdoc

Co-authored-by: Deirdre Connolly <deirdre@zfnd.org>

* Use the first `JoinSplit`s from mainnet

* Print debug messages

* Use correct blocks for the tests

Co-authored-by: Marek <mail@marek.onl>
Co-authored-by: Conrado Gouvea <conrado@zfnd.org>
This commit is contained in:
Deirdre Connolly 2021-12-09 11:50:26 -05:00 committed by GitHub
parent 4ce6fbccc4
commit b973b7a622
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 252 additions and 78 deletions

View File

@ -1,4 +1,4 @@
// From https://github.com/zcash/librustzcash/blob/master/zcash_primitives/src/merkle_tree.rs#L512
// From https://github.com/zcash/librustzcash/blob/master/zcash_primitives/src/merkle_tree.rs#L585
pub const HEX_EMPTY_ROOTS: [&str; 33] = [
"0100000000000000000000000000000000000000000000000000000000000000",
"817de36ab2d57feb077634bca77819c8e0bd298c04f6fed0e6a83cc1356ca155",

View File

@ -24,7 +24,6 @@ pub struct JoinSplit<P: ZkSnarkProof> {
pub vpub_old: Amount<NonNegative>,
/// A value that the JoinSplit transfer inserts into the transparent value
/// pool.
///
pub vpub_new: Amount<NonNegative>,
/// A root of the Sprout note commitment tree at some block height in the
/// past, or the root produced by a previous JoinSplit transfer in this

View File

@ -46,9 +46,9 @@ use std::{collections::HashMap, fmt, iter};
///
/// A transaction is an encoded data structure that facilitates the transfer of
/// value between two public key addresses on the Zcash ecosystem. Everything is
/// designed to ensure that transactions can created, propagated on the network,
/// validated, and finally added to the global ledger of transactions (the
/// blockchain).
/// designed to ensure that transactions can be created, propagated on the
/// network, validated, and finally added to the global ledger of transactions
/// (the blockchain).
///
/// Zcash has a number of different transaction formats. They are represented
/// internally by different enum variants. Because we checkpoint on Canopy
@ -612,6 +612,61 @@ impl Transaction {
}
}
/// Return if the transaction has any Sprout JoinSplit data.
pub fn has_sprout_joinsplit_data(&self) -> bool {
match self {
// No JoinSplits
Transaction::V1 { .. } | Transaction::V5 { .. } => false,
// JoinSplits-on-BCTV14
Transaction::V2 { joinsplit_data, .. } | Transaction::V3 { joinsplit_data, .. } => {
joinsplit_data.is_some()
}
// JoinSplits-on-Groth16
Transaction::V4 { joinsplit_data, .. } => joinsplit_data.is_some(),
}
}
/// Returns the Sprout note commitments in this transaction.
pub fn sprout_note_commitments(
&self,
) -> Box<dyn Iterator<Item = &sprout::commitment::NoteCommitment> + '_> {
match self {
// Return [`NoteCommitment`]s with [`Bctv14Proof`]s.
Transaction::V2 {
joinsplit_data: Some(joinsplit_data),
..
}
| Transaction::V3 {
joinsplit_data: Some(joinsplit_data),
..
} => Box::new(joinsplit_data.note_commitments()),
// Return [`NoteCommitment`]s with [`Groth16Proof`]s.
Transaction::V4 {
joinsplit_data: Some(joinsplit_data),
..
} => Box::new(joinsplit_data.note_commitments()),
// Return an empty iterator.
Transaction::V2 {
joinsplit_data: None,
..
}
| Transaction::V3 {
joinsplit_data: None,
..
}
| Transaction::V4 {
joinsplit_data: None,
..
}
| Transaction::V1 { .. }
| Transaction::V5 { .. } => Box::new(std::iter::empty()),
}
}
// sapling
/// Access the deduplicated [`sapling::tree::Root`]s in this transaction,
@ -741,45 +796,6 @@ impl Transaction {
}
}
/// Returns the Sprout note commitments in this transaction.
pub fn sprout_note_commitments(
&self,
) -> Box<dyn Iterator<Item = &sprout::commitment::NoteCommitment> + '_> {
match self {
// Return [`NoteCommitment`]s with [`Bctv14Proof`]s.
Transaction::V2 {
joinsplit_data: Some(joinsplit_data),
..
}
| Transaction::V3 {
joinsplit_data: Some(joinsplit_data),
..
} => Box::new(joinsplit_data.note_commitments()),
// Return [`NoteCommitment`]s with [`Groth16Proof`]s.
Transaction::V4 {
joinsplit_data: Some(joinsplit_data),
..
} => Box::new(joinsplit_data.note_commitments()),
// Return an empty iterator.
Transaction::V2 {
joinsplit_data: None,
..
}
| Transaction::V3 {
joinsplit_data: None,
..
}
| Transaction::V4 {
joinsplit_data: None,
..
}
| Transaction::V1 { .. }
| Transaction::V5 { .. } => Box::new(std::iter::empty()),
}
}
/// Returns the Sapling note commitments in this transaction, regardless of version.
pub fn sapling_note_commitments(&self) -> Box<dyn Iterator<Item = &jubjub::Fq> + '_> {
// This function returns a boxed iterator because the different
@ -1036,7 +1052,7 @@ impl Transaction {
.joinsplits_mut()
.map(|joinsplit| &mut joinsplit.vpub_old),
),
// JoinSplits with Groth Proofs
// JoinSplits with Groth16 Proofs
Transaction::V4 {
joinsplit_data: Some(joinsplit_data),
..

View File

@ -1,14 +1,19 @@
//! Checks for whether cited anchors are previously-computed note commitment
//! tree roots.
use std::collections::HashSet;
use zebra_chain::sprout;
use crate::{
service::{finalized_state::FinalizedState, non_finalized_state::Chain},
PreparedBlock, ValidateContextError,
};
/// Check that all the Sprout, Sapling, and Orchard anchors specified by
/// Check that the Sprout, Sapling, and Orchard anchors specified by
/// transactions in this block have been computed previously within the context
/// of its parent chain.
/// of its parent chain. We do not check any anchors in checkpointed blocks, which avoids
/// JoinSplits<BCTV14Proof>
///
/// Sprout anchors may refer to some earlier block's final treestate (like
/// Sapling and Orchard do exclusively) _or_ to the interstisial output
@ -40,28 +45,52 @@ pub(crate) fn anchors_refer_to_earlier_treestates(
prepared: &PreparedBlock,
) -> Result<(), ValidateContextError> {
for transaction in prepared.block.transactions.iter() {
// Sprout JoinSplits, with interstitial treestates to check as well
// Sprout JoinSplits, with interstitial treestates to check as well.
//
// The FIRST JOINSPLIT in a transaction MUST refer to the output treestate
// of a previous block.
if transaction.has_sprout_joinsplit_data() {
// > The anchor of each JoinSplit description in a transaction MUST refer to
// > either some earlier blocks final Sprout treestate, or to the interstitial
// > output treestate of any prior JoinSplit description in the same transaction.
//
// https://zips.z.cash/protocol/protocol.pdf#joinsplit
let mut interstitial_roots: HashSet<sprout::tree::Root> = HashSet::new();
// if let Some(sprout_shielded_data) = transaction.joinsplit_data {
// for joinsplit in transaction.sprout_groth16_joinsplits() {
// if !parent_chain.sprout_anchors.contains(joinsplit.anchor)
// && !finalized_state.contains_sprout_anchor(&joinsplit.anchor)
// {
// if !(joinsplit == &sprout_shielded_data.first) {
// // TODO: check interstitial treestates of the earlier JoinSplits
// // in this transaction against this anchor
// unimplemented!()
// } else {
// return Err(ValidateContextError::UnknownSproutAnchor {
// anchor: joinsplit.anchor,
// });
// }
// }
// }
// }
let mut interstitial_note_commitment_tree = parent_chain.sprout_note_commitment_tree();
for joinsplit in transaction.sprout_groth16_joinsplits() {
// Check all anchor sets, including the one for interstitial anchors.
//
// Note that [`interstitial_roots`] is always empty in the first
// iteration of the loop. This is because:
//
// > "The anchor of each JoinSplit description in a transaction
// > MUST refer to [...] to the interstitial output treestate of
// > any **prior** JoinSplit description in the same transaction."
if !parent_chain.sprout_anchors.contains(&joinsplit.anchor)
&& !finalized_state.contains_sprout_anchor(&joinsplit.anchor)
&& (!interstitial_roots.contains(&joinsplit.anchor))
{
return Err(ValidateContextError::UnknownSproutAnchor {
anchor: joinsplit.anchor,
});
}
tracing::debug!(?joinsplit.anchor, "validated sprout anchor");
// Add new anchors to the interstitial note commitment tree.
for cm in joinsplit.commitments {
interstitial_note_commitment_tree
.append(cm)
.expect("note commitment should be appendable to the tree");
}
interstitial_roots.insert(interstitial_note_commitment_tree.root());
tracing::debug!(?joinsplit.anchor, "observed sprout anchor");
}
}
// Sapling Spends
//

View File

@ -1,17 +1,133 @@
//! Tests for whether cited anchors are checked properly.
use std::{convert::TryInto, ops::Deref, sync::Arc};
use zebra_chain::{
amount::Amount,
block::{Block, Height},
primitives::Groth16Proof,
serialization::ZcashDeserializeInto,
transaction::{LockTime, Transaction},
sprout::JoinSplit,
transaction::{JoinSplitData, LockTime, Transaction},
};
use crate::{
arbitrary::Prepare,
tests::setup::{new_state_with_mainnet_genesis, transaction_v4_from_coinbase},
PreparedBlock,
};
// sapling
// Sprout
/// Check that, when primed with the first blocks that contain Sprout anchors, a
/// Sprout Spend's referenced anchor is validated.
#[test]
fn check_sprout_anchors() {
zebra_test::init();
let (mut state, _genesis) = new_state_with_mainnet_genesis();
// Bootstrap a block at height == 1.
let block_1 = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
// Bootstrap a block just before the first Sprout anchors.
let block_395 = zebra_test::vectors::BLOCK_MAINNET_395_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
// Add initial transactions to [`block_1`].
let block_1 = prepare_sprout_block(block_1, block_395);
// Validate and commit [`block_1`]. This will add an anchor referencing the
// empty note commitment tree to the state.
assert!(state.validate_and_commit(block_1).is_ok());
// Bootstrap a block at height == 2 that references the Sprout note commitment tree state
// from [`block_1`].
let block_2 = zebra_test::vectors::BLOCK_MAINNET_2_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
// Exercise Sprout anchor checking with the first shielded transactions with
// anchors.
let block_396 = zebra_test::vectors::BLOCK_MAINNET_396_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
// Add the transactions with the first anchors to [`block_2`].
let block_2 = prepare_sprout_block(block_2, block_396);
// Validate and commit [`block_2`]. This will also check the anchors.
assert_eq!(state.validate_and_commit(block_2), Ok(()));
}
fn prepare_sprout_block(mut block_to_prepare: Block, reference_block: Block) -> PreparedBlock {
// Convert the coinbase transaction to a version that the non-finalized state will accept.
block_to_prepare.transactions[0] =
transaction_v4_from_coinbase(&block_to_prepare.transactions[0]).into();
reference_block
.transactions
.into_iter()
.filter(|tx| tx.has_sprout_joinsplit_data())
.for_each(|tx| {
let joinsplit_data = match tx.deref() {
Transaction::V2 { joinsplit_data, .. } => joinsplit_data.clone(),
_ => unreachable!("These are known v2 transactions"),
};
// Change [`joinsplit_data`] so that the transaction passes the
// semantic validation. Namely, set the value balance to zero, and
// use a dummy Groth16 proof instead of a BCTV14 one.
let joinsplit_data = joinsplit_data.map(|s| {
let mut new_joinsplits: Vec<JoinSplit<Groth16Proof>> = Vec::new();
for old_joinsplit in s.joinsplits() {
new_joinsplits.push(JoinSplit {
vpub_old: Amount::zero(),
vpub_new: Amount::zero(),
anchor: old_joinsplit.anchor,
nullifiers: old_joinsplit.nullifiers,
commitments: old_joinsplit.commitments,
ephemeral_key: old_joinsplit.ephemeral_key,
random_seed: old_joinsplit.random_seed,
vmacs: old_joinsplit.vmacs.clone(),
zkproof: Groth16Proof::from([0; 192]),
enc_ciphertexts: old_joinsplit.enc_ciphertexts,
})
}
match new_joinsplits.split_first() {
None => unreachable!("the new joinsplits are never empty"),
Some((first, rest)) => JoinSplitData {
first: first.clone(),
rest: rest.to_vec(),
pub_key: s.pub_key,
sig: s.sig,
},
}
});
// Add the new adjusted transaction to [`block_to_prepare`].
block_to_prepare
.transactions
.push(Arc::new(Transaction::V4 {
inputs: Vec::new(),
outputs: Vec::new(),
lock_time: LockTime::min_lock_time(),
expiry_height: Height(0),
joinsplit_data,
sapling_shielded_data: None,
}))
});
Arc::new(block_to_prepare).prepare()
}
// Sapling
/// Check that, when primed with the first Sapling blocks, a Sapling Spend's referenced anchor is
/// validated.

View File

@ -324,6 +324,7 @@ proptest! {
// Allows anchor checks to pass
state.disk.populate_with_anchors(&block1);
state.disk.populate_with_anchors(&block2);
let mut previous_mem = state.mem.clone();

View File

@ -631,11 +631,11 @@ impl FinalizedState {
self.db.zs_contains(orchard_nullifiers, &orchard_nullifier)
}
// /// Returns `true` if the finalized state contains `sprout_anchor`.
// pub fn contains_sprout_anchor(&self, sprout_anchor: &sprout::tree::Root) -> bool {
// let sprout_anchors = self.db.cf_handle("sprout_anchors").unwrap();
// self.db.zs_contains(sprout_anchors, &sprout_anchor)
// }
/// Returns `true` if the finalized state contains `sprout_anchor`.
pub fn contains_sprout_anchor(&self, sprout_anchor: &sprout::tree::Root) -> bool {
let sprout_anchors = self.db.cf_handle("sprout_anchors").unwrap();
self.db.zs_contains(sprout_anchors, &sprout_anchor)
}
/// Returns `true` if the finalized state contains `sapling_anchor`.
pub fn contains_sapling_anchor(&self, sapling_anchor: &sapling::tree::Root) -> bool {
@ -790,15 +790,15 @@ impl FinalizedState {
pub fn populate_with_anchors(&self, block: &Block) {
let mut batch = rocksdb::WriteBatch::default();
// let sprout_anchors = self.db.cf_handle("sprout_anchors").unwrap();
let sprout_anchors = self.db.cf_handle("sprout_anchors").unwrap();
let sapling_anchors = self.db.cf_handle("sapling_anchors").unwrap();
let orchard_anchors = self.db.cf_handle("orchard_anchors").unwrap();
for transaction in block.transactions.iter() {
// Sprout
// for joinsplit in transaction.sprout_groth16_joinsplits() {
// batch.zs_insert(sprout_anchors, joinsplit.anchor, ());
// }
for joinsplit in transaction.sprout_groth16_joinsplits() {
batch.zs_insert(sprout_anchors, joinsplit.anchor, ());
}
// Sapling
for anchor in transaction.sapling_anchors() {

View File

@ -60,9 +60,9 @@ pub struct Chain {
pub(crate) history_tree: HistoryTree,
/// The Sprout anchors created by `blocks`.
pub(super) sprout_anchors: HashMultiSet<sprout::tree::Root>,
pub(crate) sprout_anchors: HashMultiSet<sprout::tree::Root>,
/// The Sprout anchors created by each block in `blocks`.
pub(super) sprout_anchors_by_height: BTreeMap<block::Height, sprout::tree::Root>,
pub(crate) sprout_anchors_by_height: BTreeMap<block::Height, sprout::tree::Root>,
/// The Sapling anchors created by `blocks`.
pub(crate) sapling_anchors: HashMultiSet<sapling::tree::Root>,
/// The Sapling anchors created by each block in `blocks`.
@ -394,6 +394,14 @@ impl Chain {
chain_value_pools: self.chain_value_pools,
}
}
/// Returns a clone of the Sprout note commitment tree for this chain.
///
/// Useful when calculating interstitial note commitment trees for each JoinSplit in a Sprout
/// shielded transaction.
pub fn sprout_note_commitment_tree(&self) -> sprout::tree::NoteCommitmentTree {
self.sprout_note_commitment_tree.clone()
}
}
/// The revert position being performed on a chain.

View File

@ -0,0 +1 @@
04000000b356199270b1cd6e96759c2a1a224c5c65364a4072d8b1e56f348b4a690100008120191929df6a6dbb5d7f61788155ac0b3f92a431efb2c2f6783fd99bebf39c0000000000000000000000000000000000000000000000000000000000000000518c1358c97d011eaa9da5470d00000000000000000000000000000000000000000000000000000ffd400500cb784fb5929563200bd38f5ad8b0751d052abcef16c3bbcef9af69318fe831e4123aa9e0cd401fce810547653ba354c39d219705ed30ef25b68d7a773c131d82ceb7f09e97a7ca03058ca4b60df665b35e70a1017c8120475b2fdaed0b62dceb31149cd4263a2fc402f07bd29f50a7c5ee6633aa9270894a05883eddc5043679a86fef6b75dad2e066e9c1d5684de00789de1258be8884490bd3eccb621c503bccb9489a8c509e0351cca30ff42f3de4b9e0f910ab5a28f4006def44066683af258c12b3e587a0a137a6f1fd106d3aa43f14495b943cce73ca91a712b6afb9fac934d750015953e1cff110dd395d786d673ec0be906397c23f459007c2f4d6d91d074d482023bebba58b70ffd0757c570b188fcd8f20b433fd2b652c6b34cb3e0221fae43b38e81e55f2dbac4f2f1676780a71a87a7dca7f9fb44a3a3ab500d719bcf9e568004fec8bbe02c2d5553501997c8bb05dd259979d53d60a70910136128c4b2b1fc62e571c702de5b93b2515a56664326ce1959b8d0db5fc554b5299c1635d963d27537e965fc9bb070b1ce54f4ad087e9c31cec127f8e4e0a1a0c4d7377c61b61bd11429ba4e984ee6321385ec09d97ae785fb24a10c355393e1555f3be1b119af65b0b9a243ceed71b7d22492734ba3da7e272f09e31dc2d7785ed86cf1ef3e974dd0d5ac4ed41756e935b4ed61bb09841f601fe54e2826a780b5e1b22d81dbadc5cd79e6d368308722d90c511d0c56075d105054f547ce7729517db1b7655c74e1b6723ded3745498f53e95e9553ef6346ceda50c316a0529b279b753cdd2aca22a48f9dbb10a2141de61c29c07b847243de6d777d153237340ea2dabe7e840ae2c297d9f731629a98568e6ebfc3e1f25d8acac94abff4b82ada46b477c083db3408f0a1925f3531fd2e81361883452a810a7963ee9901fb452010e4c86f8091cb5705d92563fc7ea85a1ea7f158637b647143a35b001f75ac724de453845cbec1666c90a1ecdde5e8c0f82dad4d2e3ca325dd1400ab955495c7aed30799e61bfb8f456f5f94ad58a441ddcc1fb05b0441c2526b5b96827e7706ee917fa98479d6635255a1ce2fbcdcf02fdeb02f1b64ba9e530e7f987a9138ff73b10179d357c32c35558e34f27eae53fb2453cc5adb7b1eb044be9f457b24975d73fd2fc7ed16c03f60b949685c31c36ae135a8fa1f354e87f1776cb08dffc10d943ac8de556f3915ed4c1226783d973a81420c3999d68eb21c181750e713a94ee2b745794cc44c81314349d1cafcd24052ee9f0ece388efff25220ce9360357e8159bd10722b9dcde35bdd55933de0245cb42769ed9806520a8a5da91b253c310437b0f5e1e7c5c31a7ce20578b6a249fdce1b1d57f60358bc81f6bc462a639a079de30d47627c246ce41fa7f29e4021cc96fb19cb78b96a1b5234036532e1341f7054c402747c5af5d6209a9c8e578d7cf486e00c71c60a10eed2889e51791b91da5d3e6d65dff12775797e8542f0a4e3b2b277d3147bbe7b07355200ebf8c56bc7d03a15d4043b75943c673625aa2e071dd4e5b312d4408f4aca22f0e2347c205e2ed9a6036de1a301475930f945f57b28a2e6b996bd40d6fe0c9897dbcbdcaf215da48e5cb9a5e258845c2bdc5ceac9175d157f17e04e6690e10967313d0c3f303517218d4fcacb620651628f7ce39c73b93da15340661b13e2e0fd65b79ce09d69b7a874fa01c878342577137bf7dfe93b920951ce05de1c3dab420edef15fb99e21bfec12f38018309a354a5e105d99aa1f7024eb7c390caf99dddef4a1407a157ad8e22629e5e51815d3a02156a871aa8470a073dd930de01a7854725b965af5a1b3581fb756644642a8fb05b6dab521b561035d8f466b7019c59670101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff04028b0100ffffffff02705c2d01000000002321027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875ac1c574b000000000017a9147d46a730d31f97b1930d3368a967c309bd4d136a8700000000

View File

@ -329,6 +329,10 @@ lazy_static! {
pub static ref BLOCK_MAINNET_202_BYTES: Vec<u8> =
<Vec<u8>>::from_hex(include_str!("block-main-0-000-202.txt").trim())
.expect("Block bytes are in valid hex representation");
// zcash-cli getblock 395 0 > block-main-0-000-395.txt
pub static ref BLOCK_MAINNET_395_BYTES: Vec<u8> =
<Vec<u8>>::from_hex(include_str!("block-main-0-000-395.txt").trim())
.expect("Block bytes are in valid hex representation");
// zcash-cli getblock 396 0 > block-main-0-000-396.txt
pub static ref BLOCK_MAINNET_396_BYTES: Vec<u8> =
<Vec<u8>>::from_hex(include_str!("block-main-0-000-396.txt").trim())