diff --git a/Cargo.lock b/Cargo.lock index 6fdbefe341c..b0908dc2463 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5798,6 +5798,7 @@ name = "zebra-scan" version = "0.1.0-alpha.0" dependencies = [ "bls12_381", + "chrono", "color-eyre", "ff", "group", diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index b07a59642bd..ae7f5726358 100644 --- a/zebra-chain/src/amount.rs +++ b/zebra-chain/src/amount.rs @@ -491,7 +491,7 @@ impl Error { /// -MAX_MONEY..=MAX_MONEY, /// ); /// ``` -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] pub struct NegativeAllowed; impl Constraint for NegativeAllowed { diff --git a/zebra-chain/src/block/hash.rs b/zebra-chain/src/block/hash.rs index bf2922054fa..09a2ddc9a20 100644 --- a/zebra-chain/src/block/hash.rs +++ b/zebra-chain/src/block/hash.rs @@ -21,7 +21,7 @@ use proptest_derive::Arbitrary; /// Note: Zebra displays transaction and block hashes in big-endian byte-order, /// following the u256 convention set by Bitcoin and zcashd. #[derive(Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary, Default))] pub struct Hash(pub [u8; 32]); impl Hash { diff --git a/zebra-chain/src/block/merkle.rs b/zebra-chain/src/block/merkle.rs index 42762bbe6ca..639324f9d82 100644 --- a/zebra-chain/src/block/merkle.rs +++ b/zebra-chain/src/block/merkle.rs @@ -70,7 +70,7 @@ use proptest_derive::Arbitrary; /// /// [ZIP-244]: https://zips.z.cash/zip-0244 #[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] -#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary, Default))] pub struct Root(pub [u8; 32]); impl fmt::Debug for Root { diff --git a/zebra-chain/src/fmt.rs b/zebra-chain/src/fmt.rs index 98923446c99..708e6302121 100644 --- a/zebra-chain/src/fmt.rs +++ b/zebra-chain/src/fmt.rs @@ -163,7 +163,7 @@ where /// Wrapper to override `Debug`, redirecting it to hex-encode the type. /// The type must implement `AsRef<[u8]>`. -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)] #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] #[serde(transparent)] pub struct HexDebug>(pub T); diff --git a/zebra-chain/src/sapling.rs b/zebra-chain/src/sapling.rs index 11bfd899d9c..d9b30ba430c 100644 --- a/zebra-chain/src/sapling.rs +++ b/zebra-chain/src/sapling.rs @@ -24,7 +24,9 @@ pub mod shielded_data; pub mod spend; pub mod tree; -pub use commitment::{CommitmentRandomness, NoteCommitment, ValueCommitment}; +pub use commitment::{ + CommitmentRandomness, NotSmallOrderValueCommitment, NoteCommitment, ValueCommitment, +}; pub use keys::Diversifier; pub use note::{EncryptedNote, Note, Nullifier, WrappedNoteKey}; pub use output::{Output, OutputInTransactionV4, OutputPrefixInTransactionV5}; diff --git a/zebra-chain/src/sapling/commitment.rs b/zebra-chain/src/sapling/commitment.rs index 65c789dc6ed..1ef23081550 100644 --- a/zebra-chain/src/sapling/commitment.rs +++ b/zebra-chain/src/sapling/commitment.rs @@ -158,6 +158,7 @@ impl NoteCommitment { /// /// #[derive(Clone, Copy, Deserialize, PartialEq, Eq, Serialize)] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Default))] pub struct ValueCommitment(#[serde(with = "serde_helpers::AffinePoint")] jubjub::AffinePoint); impl<'a> std::ops::Add<&'a ValueCommitment> for ValueCommitment { @@ -302,6 +303,7 @@ lazy_static! { /// /// #[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Serialize)] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Default))] pub struct NotSmallOrderValueCommitment(ValueCommitment); impl TryFrom for NotSmallOrderValueCommitment { diff --git a/zebra-chain/src/sapling/note/ciphertexts.rs b/zebra-chain/src/sapling/note/ciphertexts.rs index 472dbfb0a44..75d25730627 100644 --- a/zebra-chain/src/sapling/note/ciphertexts.rs +++ b/zebra-chain/src/sapling/note/ciphertexts.rs @@ -12,6 +12,12 @@ use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize} #[derive(Deserialize, Serialize)] pub struct EncryptedNote(#[serde(with = "BigArray")] pub(crate) [u8; 580]); +impl From<[u8; 580]> for EncryptedNote { + fn from(byte_array: [u8; 580]) -> Self { + Self(byte_array) + } +} + impl fmt::Debug for EncryptedNote { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_tuple("EncryptedNote") @@ -59,6 +65,12 @@ impl ZcashDeserialize for EncryptedNote { #[derive(Deserialize, Serialize)] pub struct WrappedNoteKey(#[serde(with = "BigArray")] pub(crate) [u8; 80]); +impl From<[u8; 80]> for WrappedNoteKey { + fn from(byte_array: [u8; 80]) -> Self { + Self(byte_array) + } +} + impl fmt::Debug for WrappedNoteKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_tuple("WrappedNoteKey") diff --git a/zebra-chain/src/work/difficulty.rs b/zebra-chain/src/work/difficulty.rs index a15c1f13ce8..4724188060c 100644 --- a/zebra-chain/src/work/difficulty.rs +++ b/zebra-chain/src/work/difficulty.rs @@ -63,6 +63,7 @@ mod tests; /// /// [section 7.7.4]: https://zips.z.cash/protocol/protocol.pdf#nbits #[derive(Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Default))] pub struct CompactDifficulty(pub(crate) u32); /// An invalid CompactDifficulty value, for testing. diff --git a/zebra-chain/src/work/equihash.rs b/zebra-chain/src/work/equihash.rs index e8b73b1614a..f65438a5314 100644 --- a/zebra-chain/src/work/equihash.rs +++ b/zebra-chain/src/work/equihash.rs @@ -93,6 +93,13 @@ impl Clone for Solution { impl Eq for Solution {} +#[cfg(any(test, feature = "proptest-impl"))] +impl Default for Solution { + fn default() -> Self { + Self([0; SOLUTION_SIZE]) + } +} + impl ZcashSerialize for Solution { fn zcash_serialize(&self, writer: W) -> Result<(), io::Error> { zcash_serialize_bytes(&self.0.to_vec(), writer) diff --git a/zebra-scan/Cargo.toml b/zebra-scan/Cargo.toml index c4cc20f40ea..802897a22fa 100644 --- a/zebra-scan/Cargo.toml +++ b/zebra-scan/Cargo.toml @@ -35,6 +35,8 @@ zcash_primitives = "0.13.0-rc.1" zebra-chain = { path = "../zebra-chain", version = "1.0.0-beta.31" } zebra-state = { path = "../zebra-state", version = "1.0.0-beta.31", features = ["shielded-scan"] } +chrono = { version = "0.4.31", default-features = false, features = ["clock", "std", "serde"] } + [dev-dependencies] bls12_381 = "0.8.0" diff --git a/zebra-scan/src/tests.rs b/zebra-scan/src/tests.rs index 4d5273b1f11..5ad7a59971d 100644 --- a/zebra-scan/src/tests.rs +++ b/zebra-scan/src/tests.rs @@ -5,10 +5,12 @@ use std::sync::Arc; -use color_eyre::Result; +use chrono::{DateTime, Utc}; + +use color_eyre::{Report, Result}; use ff::{Field, PrimeField}; use group::GroupEncoding; -use rand::{rngs::OsRng, RngCore}; +use rand::{rngs::OsRng, thread_rng, RngCore}; use zcash_client_backend::{ encoding::decode_extended_full_viewing_key, @@ -26,16 +28,23 @@ use zcash_primitives::{ note_encryption::{sapling_note_encryption, SaplingDomain}, util::generate_random_rseed, value::NoteValue, - Note, Nullifier, SaplingIvk, + Note, Nullifier, }, - zip32::{AccountId, DiversifiableFullViewingKey, ExtendedSpendingKey}, + zip32::{DiversifiableFullViewingKey, ExtendedSpendingKey}, }; use zebra_chain::{ - block::{Block, Height}, + amount::{Amount, NegativeAllowed}, + block::{self, merkle, Block, Header, Height}, chain_tip::ChainTip, + fmt::HexDebug, parameters::Network, - serialization::ZcashDeserializeInto, + primitives::{redjubjub, Groth16Proof}, + sapling::{self, PerSpendAnchor, Spend, TransferData}, + serialization::{AtLeastOne, ZcashDeserializeInto}, + transaction::{LockTime, Transaction}, + transparent::{CoinbaseData, Input}, + work::{difficulty::CompactDifficulty, equihash::Solution}, }; use zebra_state::SaplingScannedResult; @@ -44,49 +53,40 @@ use crate::{ scan::{block_to_compact, scan_block}, }; -/// Prove that we can create fake blocks with fake notes and scan them using the -/// `zcash_client_backend::scanning::scan_block` function: -/// - Function `fake_compact_block` will generate 1 block with one pre created fake nullifier in -/// the transaction and one additional random transaction without it. -/// - Verify one relevant transaction is found in the chain when scanning for the pre created fake -/// account's nullifier. -#[test] -fn scanning_from_fake_generated_blocks() -> Result<()> { - let account = AccountId::from(12); +/// This test: +/// - Creates a viewing key and a fake block containing a Sapling output decryptable by the key. +/// - Scans the block. +/// - Checks that the result contains the txid of the tx containing the Sapling output. +#[tokio::test] +async fn scanning_from_fake_generated_blocks() -> Result<()> { let extsk = ExtendedSpendingKey::master(&[]); let dfvk: DiversifiableFullViewingKey = extsk.to_diversifiable_full_viewing_key(); - let vks: Vec<(&AccountId, &SaplingIvk)> = vec![]; let nf = Nullifier([7; 32]); - let cb = fake_compact_block( - 1u32.into(), - BlockHash([0; 32]), - nf, - &dfvk, - 1, - false, - Some(0), - ); + let (block, sapling_tree_size) = fake_block(1u32.into(), nf, &dfvk, 1, true, Some(0)); - // The fake block function will have our transaction and a random one. - assert_eq!(cb.vtx.len(), 2); + assert_eq!(block.transactions.len(), 4); - let res = zcash_client_backend::scanning::scan_block( - &zcash_primitives::consensus::MainNetwork, - cb.clone(), - &vks[..], - &[(account, nf)], - None, - ) - .unwrap(); + let res = scan_block(Network::Mainnet, &block, sapling_tree_size, &[&dfvk]).unwrap(); // The response should have one transaction relevant to the key we provided. assert_eq!(res.transactions().len(), 1); - // The transaction should be the one we provided, second one in the block. - // (random transaction is added before ours in `fake_compact_block` function) - assert_eq!(res.transactions()[0].txid, cb.vtx[1].txid()); + + // Check that the original block contains the txid in the scanning result. + assert!(block + .transactions + .iter() + .map(|tx| tx.hash().bytes_in_display_order()) + .any(|txid| &txid == res.transactions()[0].txid.as_ref())); + + // Check that the txid in the scanning result matches the third tx in the original block. + assert_eq!( + res.transactions()[0].txid.as_ref(), + &block.transactions[2].hash().bytes_in_display_order() + ); + // The block hash of the response should be the same as the one provided. - assert_eq!(res.block_hash(), cb.hash()); + assert_eq!(res.block_hash().0, block.hash().0); Ok(()) } @@ -111,7 +111,7 @@ async fn scanning_zecpages_from_populated_zebra_state() -> Result<()> { let ivk = fvk.vk.ivk(); let ivks = vec![ivk]; - let network = zebra_chain::parameters::Network::Mainnet; + let network = Network::Mainnet; // Create a continuous chain of mainnet blocks from genesis let blocks: Vec> = zebra_test::vectors::CONTINUOUS_MAINNET_BLOCKS @@ -170,13 +170,14 @@ async fn scanning_zecpages_from_populated_zebra_state() -> Result<()> { Ok(()) } -/// In this test we generate a viewing key and manually add it to the database. Also we send results to the Storage database. +/// Creates a viewing key and a fake block containing a Sapling output decryptable by the key, scans +/// the block using the key, and adds the results to the database. +/// /// The purpose of this test is to check if our database and our scanning code are compatible. #[test] #[allow(deprecated)] fn scanning_fake_blocks_store_key_and_results() -> Result<()> { // Generate a key - let account = AccountId::from(12); let extsk = ExtendedSpendingKey::master(&[]); // TODO: find out how to do it with `to_diversifiable_full_viewing_key` as `to_extended_full_viewing_key` is deprecated. let extfvk = extsk.to_extended_full_viewing_key(); @@ -197,28 +198,11 @@ fn scanning_fake_blocks_store_key_and_results() -> Result<()> { Some(&s.min_sapling_birthday_height()) ); - let vks: Vec<(&AccountId, &SaplingIvk)> = vec![]; let nf = Nullifier([7; 32]); - // Add key to fake block - let cb = fake_compact_block( - 1u32.into(), - BlockHash([0; 32]), - nf, - &dfvk, - 1, - false, - Some(0), - ); + let (block, sapling_tree_size) = fake_block(1u32.into(), nf, &dfvk, 1, true, Some(0)); - let result = zcash_client_backend::scanning::scan_block( - &zcash_primitives::consensus::MainNetwork, - cb.clone(), - &vks[..], - &[(account, nf)], - None, - ) - .unwrap(); + let result = scan_block(Network::Mainnet, &block, sapling_tree_size, &[&dfvk]).unwrap(); // The response should have one transaction relevant to the key we provided. assert_eq!(result.transactions().len(), 1); @@ -237,6 +221,81 @@ fn scanning_fake_blocks_store_key_and_results() -> Result<()> { Ok(()) } +/// Generates a fake block containing a Sapling output decryptable by `dfvk`. +/// +/// The fake block has the following transactions in this order: +/// 1. a transparent coinbase tx, +/// 2. a V4 tx containing a random Sapling output, +/// 3. a V4 tx containing a Sapling output decryptable by `dfvk`, +/// 4. depending on the value of `tx_after`, another V4 tx containing a random Sapling output. +fn fake_block( + height: BlockHeight, + nf: Nullifier, + dfvk: &DiversifiableFullViewingKey, + value: u64, + tx_after: bool, + initial_sapling_tree_size: Option, +) -> (Block, u32) { + let header = Header { + version: 4, + previous_block_hash: block::Hash::default(), + merkle_root: merkle::Root::default(), + commitment_bytes: HexDebug::default(), + time: DateTime::::default(), + difficulty_threshold: CompactDifficulty::default(), + nonce: HexDebug::default(), + solution: Solution::default(), + }; + + let block = fake_compact_block( + height, + BlockHash([0; 32]), + nf, + dfvk, + value, + tx_after, + initial_sapling_tree_size, + ); + + let mut transactions: Vec> = block + .vtx + .iter() + .map(|tx| compact_to_v4(tx).expect("A fake compact tx should be convertible to V4.")) + .map(Arc::new) + .collect(); + + let coinbase_input = Input::Coinbase { + height: Height(1), + data: CoinbaseData::new(vec![]), + sequence: u32::MAX, + }; + + let coinbase = Transaction::V4 { + inputs: vec![coinbase_input], + outputs: vec![], + lock_time: LockTime::Height(Height(1)), + expiry_height: Height(1), + joinsplit_data: None, + sapling_shielded_data: None, + }; + + transactions.insert(0, Arc::new(coinbase)); + + let sapling_tree_size = block + .chain_metadata + .as_ref() + .unwrap() + .sapling_commitment_tree_size; + + ( + Block { + header: Arc::new(header), + transactions, + }, + sapling_tree_size, + ) +} + /// Create a fake compact block with provided fake account data. // This is a copy of zcash_primitives `fake_compact_block` where the `value` argument was changed to // be a number for easier conversion: @@ -362,3 +421,85 @@ fn random_compact_tx(mut rng: impl RngCore) -> CompactTx { ctx.outputs.push(cout); ctx } + +/// Converts [`CompactTx`] to [`Transaction::V4`]. +fn compact_to_v4(tx: &CompactTx) -> Result { + let sk = redjubjub::SigningKey::::new(thread_rng()); + let vk = redjubjub::VerificationKey::from(&sk); + let dummy_rk = sapling::keys::ValidatingKey::try_from(vk) + .expect("Internally generated verification key should be convertible to a validating key."); + + let spends = tx + .spends + .iter() + .map(|spend| { + Ok(Spend { + cv: sapling::NotSmallOrderValueCommitment::default(), + per_spend_anchor: sapling::tree::Root::default(), + nullifier: sapling::Nullifier::from( + spend.nf().map_err(|_| Report::msg("Invalid nullifier."))?.0, + ), + rk: dummy_rk.clone(), + zkproof: Groth16Proof([0; 192]), + spend_auth_sig: redjubjub::Signature::::from([0; 64]), + }) + }) + .collect::>>>()?; + + let spends = AtLeastOne::>::try_from(spends)?; + + let maybe_outputs = tx + .outputs + .iter() + .map(|output| { + let mut ciphertext = output.ciphertext.clone(); + ciphertext.resize(580, 0); + let ciphertext: [u8; 580] = ciphertext + .try_into() + .map_err(|_| Report::msg("Could not convert ciphertext to `[u8; 580]`"))?; + let enc_ciphertext = sapling::EncryptedNote::from(ciphertext); + + Ok(sapling::Output { + cv: sapling::NotSmallOrderValueCommitment::default(), + cm_u: Option::from(jubjub::Fq::from_bytes( + &output + .cmu() + .map_err(|_| Report::msg("Invalid commitment."))? + .to_bytes(), + )) + .ok_or(Report::msg("Invalid commitment."))?, + ephemeral_key: sapling::keys::EphemeralPublicKey::try_from( + output + .ephemeral_key() + .map_err(|_| Report::msg("Invalid ephemeral key."))? + .0, + ) + .map_err(Report::msg)?, + enc_ciphertext, + out_ciphertext: sapling::WrappedNoteKey::from([0; 80]), + zkproof: Groth16Proof([0; 192]), + }) + }) + .collect::>>()?; + + let transfers = TransferData::SpendsAndMaybeOutputs { + shared_anchor: sapling::FieldNotPresent, + spends, + maybe_outputs, + }; + + let shielded_data = sapling::ShieldedData { + value_balance: Amount::::default(), + transfers, + binding_sig: redjubjub::Signature::::from([0; 64]), + }; + + Ok(Transaction::V4 { + inputs: vec![], + outputs: vec![], + lock_time: LockTime::Height(Height(0)), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: (Some(shielded_data)), + }) +}