From f306a0a78a388c3bbf4e7cb081592b19b99a8010 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 7 Mar 2024 22:13:47 +0000 Subject: [PATCH 1/7] zcash_client_sqlite: Make internal testing functions module-private This ensures we only access them through `TestState` in future. --- zcash_client_sqlite/src/testing.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 2a13e468d..a49694982 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -767,7 +767,7 @@ pub(crate) enum AddressType { /// Create a fake CompactBlock at the given height, containing a single output paying /// an address. Returns the CompactBlock and the nullifier for the new note. -pub(crate) fn fake_compact_block( +fn fake_compact_block( params: &P, height: BlockHeight, prev_hash: BlockHash, @@ -827,7 +827,7 @@ pub(crate) fn fake_compact_block( } /// Create a fake CompactBlock at the given height containing only the given transaction. -pub(crate) fn fake_compact_block_from_tx( +fn fake_compact_block_from_tx( height: BlockHeight, prev_hash: BlockHash, tx_index: usize, @@ -870,7 +870,7 @@ pub(crate) fn fake_compact_block_from_tx( /// Create a fake CompactBlock at the given height, spending a single note from the /// given address. #[allow(clippy::too_many_arguments)] -pub(crate) fn fake_compact_block_spending( +fn fake_compact_block_spending( params: &P, height: BlockHeight, prev_hash: BlockHash, @@ -945,7 +945,7 @@ pub(crate) fn fake_compact_block_spending( fake_compact_block_from_compact_tx(ctx, height, prev_hash, initial_sapling_tree_size, 0) } -pub(crate) fn fake_compact_block_from_compact_tx( +fn fake_compact_block_from_compact_tx( ctx: CompactTx, height: BlockHeight, prev_hash: BlockHash, @@ -1090,7 +1090,7 @@ pub(crate) fn input_selector( // Checks that a protobuf proposal serialized from the provided proposal value correctly parses to // the same proposal value. -pub(crate) fn check_proposal_serialization_roundtrip( +fn check_proposal_serialization_roundtrip( db_data: &WalletDb, proposal: &Proposal, ) { From bc0b1aed076475bffa04983dee03e275db2504d4 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 7 Mar 2024 22:52:11 +0000 Subject: [PATCH 2/7] De-duplicate compact Sapling output creation in `TestState` --- zcash_client_sqlite/src/testing.rs | 153 ++++++++++++----------------- 1 file changed, 63 insertions(+), 90 deletions(-) diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index a49694982..dbd9871e5 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -7,7 +7,7 @@ use std::fs::File; use nonempty::NonEmpty; use prost::Message; -use rand_core::{OsRng, RngCore}; +use rand_core::{CryptoRng, OsRng, RngCore}; use rusqlite::{params, Connection}; use secrecy::{Secret, SecretVec}; use tempfile::NamedTempFile; @@ -18,7 +18,6 @@ use tempfile::TempDir; use sapling::{ note_encryption::{sapling_note_encryption, SaplingDomain}, util::generate_random_rseed, - value::NoteValue, zip32::DiversifiableFullViewingKey, Note, Nullifier, PaymentAddress, }; @@ -765,6 +764,49 @@ pub(crate) enum AddressType { Internal, } +/// Creates a `CompactSaplingOutput` at the given height paying the given recipient. +/// +/// Returns the `CompactSaplingOutput` and the new note. +fn compact_sapling_output( + params: &P, + height: BlockHeight, + recipient: sapling::PaymentAddress, + value: NonNegativeAmount, + ovk: sapling::keys::OutgoingViewingKey, + rng: &mut R, +) -> (CompactSaplingOutput, sapling::Note) { + let rseed = generate_random_rseed(zip212_enforcement(params, height), rng); + let note = Note::from_parts( + recipient, + sapling::value::NoteValue::from_raw(value.into_u64()), + rseed, + ); + let encryptor = + sapling_note_encryption(Some(ovk), note.clone(), *MemoBytes::empty().as_array(), rng); + let cmu = note.cmu().to_bytes().to_vec(); + let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec(); + let enc_ciphertext = encryptor.encrypt_note_plaintext(); + + ( + CompactSaplingOutput { + cmu, + ephemeral_key, + ciphertext: enc_ciphertext.as_ref()[..52].to_vec(), + }, + note, + ) +} + +/// Creates a fake `CompactTx` with a random transaction ID and no spends or outputs. +fn fake_compact_tx(rng: &mut R) -> CompactTx { + let mut ctx = CompactTx::default(); + let mut txid = vec![0; 32]; + rng.fill_bytes(&mut txid); + ctx.hash = txid; + + ctx +} + /// Create a fake CompactBlock at the given height, containing a single output paying /// an address. Returns the CompactBlock and the nullifier for the new note. fn fake_compact_block( @@ -784,45 +826,14 @@ fn fake_compact_block( // Create a fake Note for the account let mut rng = OsRng; - let rseed = generate_random_rseed(zip212_enforcement(params, height), &mut rng); - let note = Note::from_parts(to, NoteValue::from_raw(value.into_u64()), rseed); - let encryptor = sapling_note_encryption( - Some(dfvk.fvk().ovk), - note.clone(), - *MemoBytes::empty().as_array(), - &mut rng, - ); - let cmu = note.cmu().to_bytes().to_vec(); - let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec(); - let enc_ciphertext = encryptor.encrypt_note_plaintext(); + let (cout, note) = compact_sapling_output(params, height, to, value, dfvk.fvk().ovk, &mut rng); // Create a fake CompactBlock containing the note - let cout = CompactSaplingOutput { - cmu, - ephemeral_key, - ciphertext: enc_ciphertext.as_ref()[..52].to_vec(), - }; - let mut ctx = CompactTx::default(); - let mut txid = vec![0; 32]; - rng.fill_bytes(&mut txid); - ctx.hash = txid; + let mut ctx = fake_compact_tx(&mut rng); ctx.outputs.push(cout); - let mut cb = CompactBlock { - hash: { - let mut hash = vec![0; 32]; - rng.fill_bytes(&mut hash); - hash - }, - height: height.into(), - ..Default::default() - }; - cb.prev_hash.extend_from_slice(&prev_hash.0); - cb.vtx.push(ctx); - cb.chain_metadata = Some(compact::ChainMetadata { - sapling_commitment_tree_size: initial_sapling_tree_size - + cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::(), - ..Default::default() - }); + + let cb = + fake_compact_block_from_compact_tx(ctx, height, prev_hash, initial_sapling_tree_size, 0); (cb, note.nf(&dfvk.fvk().vk.nk, 0)) } @@ -880,67 +891,29 @@ fn fake_compact_block_spending( value: NonNegativeAmount, initial_sapling_tree_size: u32, ) -> CompactBlock { - let zip212_enforcement = zip212_enforcement(params, height); let mut rng = OsRng; - let rseed = generate_random_rseed(zip212_enforcement, &mut rng); + let mut ctx = fake_compact_tx(&mut rng); - // Create a fake CompactBlock containing the note + // Create a fake spend let cspend = CompactSaplingSpend { nf: nf.to_vec() }; - let mut ctx = CompactTx::default(); - let mut txid = vec![0; 32]; - rng.fill_bytes(&mut txid); - ctx.hash = txid; ctx.spends.push(cspend); // Create a fake Note for the payment - ctx.outputs.push({ - let note = Note::from_parts( - to, - sapling::value::NoteValue::from_raw(value.into_u64()), - rseed, - ); - let encryptor = sapling_note_encryption( - Some(dfvk.fvk().ovk), - note.clone(), - *MemoBytes::empty().as_array(), - &mut rng, - ); - let cmu = note.cmu().to_bytes().to_vec(); - let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec(); - let enc_ciphertext = encryptor.encrypt_note_plaintext(); - - CompactSaplingOutput { - cmu, - ephemeral_key, - ciphertext: enc_ciphertext.as_ref()[..52].to_vec(), - } - }); + ctx.outputs + .push(compact_sapling_output(params, height, to, value, dfvk.fvk().ovk, &mut rng).0); // Create a fake Note for the change - ctx.outputs.push({ - let change_addr = dfvk.default_address().1; - let rseed = generate_random_rseed(zip212_enforcement, &mut rng); - let note = Note::from_parts( - change_addr, - NoteValue::from_raw((in_value - value).unwrap().into_u64()), - rseed, - ); - let encryptor = sapling_note_encryption( - Some(dfvk.fvk().ovk), - note.clone(), - *MemoBytes::empty().as_array(), + ctx.outputs.push( + compact_sapling_output( + params, + height, + dfvk.default_address().1, + (in_value - value).unwrap(), + dfvk.fvk().ovk, &mut rng, - ); - let cmu = note.cmu().to_bytes().to_vec(); - let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec(); - let enc_ciphertext = encryptor.encrypt_note_plaintext(); - - CompactSaplingOutput { - cmu, - ephemeral_key, - ciphertext: enc_ciphertext.as_ref()[..52].to_vec(), - } - }); + ) + .0, + ); fake_compact_block_from_compact_tx(ctx, height, prev_hash, initial_sapling_tree_size, 0) } From 291a6f579971d340da0eea1714c1f6ee6d17aaa9 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 7 Mar 2024 23:25:31 +0000 Subject: [PATCH 3/7] Use correct change address in `TestState::generate_next_block_spending` --- zcash_client_sqlite/src/testing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index dbd9871e5..63b598abe 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -907,7 +907,7 @@ fn fake_compact_block_spending( compact_sapling_output( params, height, - dfvk.default_address().1, + dfvk.change_address().1, (in_value - value).unwrap(), dfvk.fvk().ovk, &mut rng, From 0a41d659106a8c21129fd3d6699a89bc85c57957 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 7 Mar 2024 23:59:12 +0000 Subject: [PATCH 4/7] Generalise `TestState::generate_*` methods over a `TestFvk` trait --- zcash_client_sqlite/src/testing.rs | 141 ++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 43 deletions(-) diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 63b598abe..fa063fe3e 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -186,12 +186,12 @@ where /// Creates a fake block at the expected next height containing a single output of the /// given value, and inserts it into the cache. - pub(crate) fn generate_next_block( + pub(crate) fn generate_next_block( &mut self, - dfvk: &DiversifiableFullViewingKey, + fvk: &Fvk, req: AddressType, value: NonNegativeAmount, - ) -> (BlockHeight, Cache::InsertResult, Nullifier) { + ) -> (BlockHeight, Cache::InsertResult, Fvk::Nullifier) { let (height, prev_hash, initial_sapling_tree_size) = self .latest_cached_block .map(|(prev_height, prev_hash, end_size)| (prev_height + 1, prev_hash, end_size)) @@ -200,7 +200,7 @@ where let (res, nf) = self.generate_block_at( height, prev_hash, - dfvk, + fvk, req, value, initial_sapling_tree_size, @@ -214,20 +214,20 @@ where /// /// This generated block will be treated as the latest block, and subsequent calls to /// [`Self::generate_next_block`] will build on it. - pub(crate) fn generate_block_at( + pub(crate) fn generate_block_at( &mut self, height: BlockHeight, prev_hash: BlockHash, - dfvk: &DiversifiableFullViewingKey, + fvk: &Fvk, req: AddressType, value: NonNegativeAmount, initial_sapling_tree_size: u32, - ) -> (Cache::InsertResult, Nullifier) { + ) -> (Cache::InsertResult, Fvk::Nullifier) { let (cb, nf) = fake_compact_block( &self.network(), height, prev_hash, - dfvk, + fvk, req, value, initial_sapling_tree_size, @@ -246,10 +246,10 @@ where /// Creates a fake block at the expected next height spending the given note, and /// inserts it into the cache. - pub(crate) fn generate_next_block_spending( + pub(crate) fn generate_next_block_spending( &mut self, - dfvk: &DiversifiableFullViewingKey, - note: (Nullifier, NonNegativeAmount), + fvk: &Fvk, + note: (Fvk::Nullifier, NonNegativeAmount), to: PaymentAddress, value: NonNegativeAmount, ) -> (BlockHeight, Cache::InsertResult) { @@ -263,7 +263,7 @@ where height, prev_hash, note, - dfvk, + fvk, to, value, initial_sapling_tree_size, @@ -757,6 +757,64 @@ impl TestState { } } +/// Trait used by tests that require a full viewing key. +pub(crate) trait TestFvk { + type Nullifier; + + fn sapling_ovk(&self) -> Option; + + fn add_compact_spend(&self, ctx: &mut CompactTx, nf: Self::Nullifier); + + fn add_compact_output( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + req: AddressType, + value: NonNegativeAmount, + initial_sapling_tree_size: u32, + rng: &mut R, + ) -> Self::Nullifier; +} + +impl TestFvk for DiversifiableFullViewingKey { + type Nullifier = Nullifier; + + fn sapling_ovk(&self) -> Option { + Some(self.fvk().ovk) + } + + fn add_compact_spend(&self, ctx: &mut CompactTx, nf: Self::Nullifier) { + let cspend = CompactSaplingSpend { nf: nf.to_vec() }; + ctx.spends.push(cspend); + } + + fn add_compact_output( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + req: AddressType, + value: NonNegativeAmount, + initial_sapling_tree_size: u32, + rng: &mut R, + ) -> Self::Nullifier { + let recipient = match req { + AddressType::DefaultExternal => self.default_address().1, + AddressType::DiversifiedExternal(idx) => self.find_address(idx).unwrap().1, + AddressType::Internal => self.change_address().1, + }; + + let position = initial_sapling_tree_size + ctx.outputs.len() as u32; + + let (cout, note) = + compact_sapling_output(params, height, recipient, value, self.sapling_ovk(), rng); + ctx.outputs.push(cout); + + note.nf(&self.fvk().vk.nk, position as u64) + } +} + #[allow(dead_code)] pub(crate) enum AddressType { DefaultExternal, @@ -772,7 +830,7 @@ fn compact_sapling_output( height: BlockHeight, recipient: sapling::PaymentAddress, value: NonNegativeAmount, - ovk: sapling::keys::OutgoingViewingKey, + ovk: Option, rng: &mut R, ) -> (CompactSaplingOutput, sapling::Note) { let rseed = generate_random_rseed(zip212_enforcement(params, height), rng); @@ -781,8 +839,7 @@ fn compact_sapling_output( sapling::value::NoteValue::from_raw(value.into_u64()), rseed, ); - let encryptor = - sapling_note_encryption(Some(ovk), note.clone(), *MemoBytes::empty().as_array(), rng); + let encryptor = sapling_note_encryption(ovk, note.clone(), *MemoBytes::empty().as_array(), rng); let cmu = note.cmu().to_bytes().to_vec(); let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec(); let enc_ciphertext = encryptor.encrypt_note_plaintext(); @@ -809,32 +866,33 @@ fn fake_compact_tx(rng: &mut R) -> CompactTx { /// Create a fake CompactBlock at the given height, containing a single output paying /// an address. Returns the CompactBlock and the nullifier for the new note. -fn fake_compact_block( +fn fake_compact_block( params: &P, height: BlockHeight, prev_hash: BlockHash, - dfvk: &DiversifiableFullViewingKey, + fvk: &Fvk, req: AddressType, value: NonNegativeAmount, initial_sapling_tree_size: u32, -) -> (CompactBlock, Nullifier) { - let to = match req { - AddressType::DefaultExternal => dfvk.default_address().1, - AddressType::DiversifiedExternal(idx) => dfvk.find_address(idx).unwrap().1, - AddressType::Internal => dfvk.change_address().1, - }; - +) -> (CompactBlock, Fvk::Nullifier) { // Create a fake Note for the account let mut rng = OsRng; - let (cout, note) = compact_sapling_output(params, height, to, value, dfvk.fvk().ovk, &mut rng); // Create a fake CompactBlock containing the note let mut ctx = fake_compact_tx(&mut rng); - ctx.outputs.push(cout); + let nf = fvk.add_compact_output( + &mut ctx, + params, + height, + req, + value, + initial_sapling_tree_size, + &mut rng, + ); let cb = fake_compact_block_from_compact_tx(ctx, height, prev_hash, initial_sapling_tree_size, 0); - (cb, note.nf(&dfvk.fvk().vk.nk, 0)) + (cb, nf) } /// Create a fake CompactBlock at the given height containing only the given transaction. @@ -881,12 +939,12 @@ fn fake_compact_block_from_tx( /// Create a fake CompactBlock at the given height, spending a single note from the /// given address. #[allow(clippy::too_many_arguments)] -fn fake_compact_block_spending( +fn fake_compact_block_spending( params: &P, height: BlockHeight, prev_hash: BlockHash, - (nf, in_value): (Nullifier, NonNegativeAmount), - dfvk: &DiversifiableFullViewingKey, + (nf, in_value): (Fvk::Nullifier, NonNegativeAmount), + fvk: &Fvk, to: PaymentAddress, value: NonNegativeAmount, initial_sapling_tree_size: u32, @@ -895,24 +953,21 @@ fn fake_compact_block_spending( let mut ctx = fake_compact_tx(&mut rng); // Create a fake spend - let cspend = CompactSaplingSpend { nf: nf.to_vec() }; - ctx.spends.push(cspend); + fvk.add_compact_spend(&mut ctx, nf); // Create a fake Note for the payment ctx.outputs - .push(compact_sapling_output(params, height, to, value, dfvk.fvk().ovk, &mut rng).0); + .push(compact_sapling_output(params, height, to, value, fvk.sapling_ovk(), &mut rng).0); // Create a fake Note for the change - ctx.outputs.push( - compact_sapling_output( - params, - height, - dfvk.change_address().1, - (in_value - value).unwrap(), - dfvk.fvk().ovk, - &mut rng, - ) - .0, + fvk.add_compact_output( + &mut ctx, + params, + height, + AddressType::Internal, + (in_value - value).unwrap(), + initial_sapling_tree_size, + &mut rng, ); fake_compact_block_from_compact_tx(ctx, height, prev_hash, initial_sapling_tree_size, 0) From 6c6080c99c721062a20fdc76a862a7a8c8517394 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 8 Mar 2024 01:00:48 +0000 Subject: [PATCH 5/7] `impl TestFvk for orchard::keys::FullViewingKey` --- Cargo.lock | 1 + Cargo.toml | 1 + zcash_client_sqlite/Cargo.toml | 1 + zcash_client_sqlite/src/testing.rs | 162 +++++++++++++++++++++++++++-- 4 files changed, 158 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3610f810..42183af20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3066,6 +3066,7 @@ dependencies = [ "maybe-rayon", "nonempty", "orchard", + "pasta_curves", "proptest", "prost", "rand_core", diff --git a/Cargo.toml b/Cargo.toml index 4ef3e1306..3ae744f87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ sapling = { package = "sapling-crypto", version = "0.1.1" } # - Orchard nonempty = "0.7" orchard = { version = "0.7.1", default-features = false } +pasta_curves = "0.5" # - Transparent hdwallet = "0.4" diff --git a/zcash_client_sqlite/Cargo.toml b/zcash_client_sqlite/Cargo.toml index 2cad806c9..7a581258c 100644 --- a/zcash_client_sqlite/Cargo.toml +++ b/zcash_client_sqlite/Cargo.toml @@ -70,6 +70,7 @@ maybe-rayon.workspace = true [dev-dependencies] assert_matches.workspace = true incrementalmerkletree = { workspace = true, features = ["test-dependencies"] } +pasta_curves.workspace = true shardtree = { workspace = true, features = ["legacy-api", "test-dependencies"] } nonempty.workspace = true proptest.workspace = true diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index fa063fe3e..bd4816cc0 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -72,6 +72,14 @@ use crate::{ use super::BlockDb; +#[cfg(feature = "orchard")] +use { + group::ff::{Field, PrimeField}, + orchard::note_encryption::{OrchardDomain, OrchardNoteEncryption}, + pasta_curves::pallas, + zcash_client_backend::proto::compact_formats::CompactOrchardAction, +}; + #[cfg(feature = "transparent-inputs")] use { zcash_client_backend::data_api::wallet::{ @@ -763,9 +771,18 @@ pub(crate) trait TestFvk { fn sapling_ovk(&self) -> Option; - fn add_compact_spend(&self, ctx: &mut CompactTx, nf: Self::Nullifier); + #[cfg(feature = "orchard")] + fn orchard_ovk(&self, scope: zip32::Scope) -> Option; - fn add_compact_output( + fn add_spend( + &self, + ctx: &mut CompactTx, + nf: Self::Nullifier, + rng: &mut R, + ); + + #[allow(clippy::too_many_arguments)] + fn add_output( &self, ctx: &mut CompactTx, params: &P, @@ -784,12 +801,22 @@ impl TestFvk for DiversifiableFullViewingKey { Some(self.fvk().ovk) } - fn add_compact_spend(&self, ctx: &mut CompactTx, nf: Self::Nullifier) { + #[cfg(feature = "orchard")] + fn orchard_ovk(&self, _: zip32::Scope) -> Option { + None + } + + fn add_spend( + &self, + ctx: &mut CompactTx, + nf: Self::Nullifier, + _: &mut R, + ) { let cspend = CompactSaplingSpend { nf: nf.to_vec() }; ctx.spends.push(cspend); } - fn add_compact_output( + fn add_output( &self, ctx: &mut CompactTx, params: &P, @@ -815,6 +842,79 @@ impl TestFvk for DiversifiableFullViewingKey { } } +#[cfg(feature = "orchard")] +impl TestFvk for orchard::keys::FullViewingKey { + type Nullifier = orchard::note::Nullifier; + + fn sapling_ovk(&self) -> Option { + None + } + + fn orchard_ovk(&self, scope: zip32::Scope) -> Option { + Some(self.to_ovk(scope)) + } + + fn add_spend( + &self, + ctx: &mut CompactTx, + nf: Self::Nullifier, + rng: &mut R, + ) { + // Generate a dummy recipient. + let recipient = loop { + let mut bytes = [0; 32]; + rng.fill_bytes(&mut bytes); + let sk = orchard::keys::SpendingKey::from_bytes(bytes); + if sk.is_some().into() { + break orchard::keys::FullViewingKey::from(&sk.unwrap()) + .address_at(0u32, zip32::Scope::External); + } + }; + + let (cact, _) = compact_orchard_action( + nf, + recipient, + NonNegativeAmount::ZERO, + self.orchard_ovk(zip32::Scope::Internal), + rng, + ); + ctx.actions.push(cact); + } + + fn add_output( + &self, + ctx: &mut CompactTx, + _: &P, + _: BlockHeight, + req: AddressType, + value: NonNegativeAmount, + _: u32, + mut rng: &mut R, + ) -> Self::Nullifier { + // Generate a dummy nullifier + let nullifier = + orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr()) + .unwrap(); + + let (j, scope) = match req { + AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External), + AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External), + AddressType::Internal => (0u32.into(), zip32::Scope::Internal), + }; + + let (cact, note) = compact_orchard_action( + nullifier, + self.address_at(j, scope), + value, + self.orchard_ovk(scope), + rng, + ); + ctx.actions.push(cact); + + note.nullifier(self) + } +} + #[allow(dead_code)] pub(crate) enum AddressType { DefaultExternal, @@ -854,6 +954,54 @@ fn compact_sapling_output( ) } +/// Creates a `CompactOrchardAction` at the given height paying the given recipient. +/// +/// Returns the `CompactOrchardAction` and the new note. +#[cfg(feature = "orchard")] +fn compact_orchard_action( + nullifier: orchard::note::Nullifier, + recipient: orchard::Address, + value: NonNegativeAmount, + ovk: Option, + rng: &mut R, +) -> (CompactOrchardAction, orchard::Note) { + let nf = nullifier.to_bytes().to_vec(); + + let rseed = { + loop { + let mut bytes = [0; 32]; + rng.fill_bytes(&mut bytes); + let rseed = orchard::note::RandomSeed::from_bytes(bytes, &nullifier); + if rseed.is_some().into() { + break rseed.unwrap(); + } + } + }; + let note = orchard::Note::from_parts( + recipient, + orchard::value::NoteValue::from_raw(value.into_u64()), + nullifier, + rseed, + ) + .unwrap(); + let encryptor = OrchardNoteEncryption::new(ovk, note, *MemoBytes::empty().as_array()); + let cmx = orchard::note::ExtractedNoteCommitment::from(note.commitment()) + .to_bytes() + .to_vec(); + let ephemeral_key = OrchardDomain::epk_bytes(encryptor.epk()).0.to_vec(); + let enc_ciphertext = encryptor.encrypt_note_plaintext(); + + ( + CompactOrchardAction { + nullifier: nf, + cmx, + ephemeral_key, + ciphertext: enc_ciphertext.as_ref()[..52].to_vec(), + }, + note, + ) +} + /// Creates a fake `CompactTx` with a random transaction ID and no spends or outputs. fn fake_compact_tx(rng: &mut R) -> CompactTx { let mut ctx = CompactTx::default(); @@ -880,7 +1028,7 @@ fn fake_compact_block( // Create a fake CompactBlock containing the note let mut ctx = fake_compact_tx(&mut rng); - let nf = fvk.add_compact_output( + let nf = fvk.add_output( &mut ctx, params, height, @@ -953,14 +1101,14 @@ fn fake_compact_block_spending( let mut ctx = fake_compact_tx(&mut rng); // Create a fake spend - fvk.add_compact_spend(&mut ctx, nf); + fvk.add_spend(&mut ctx, nf, &mut rng); // Create a fake Note for the payment ctx.outputs .push(compact_sapling_output(params, height, to, value, fvk.sapling_ovk(), &mut rng).0); // Create a fake Note for the change - fvk.add_compact_output( + fvk.add_output( &mut ctx, params, height, From cc90c4c4059a6d122e54160474d6a0b01758ac0f Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 8 Mar 2024 01:08:50 +0000 Subject: [PATCH 6/7] Avoid excessive Orchard dummies in `TestState::generate_next_block_spending` --- zcash_client_sqlite/src/testing.rs | 70 ++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index bd4816cc0..3717faecd 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -792,6 +792,30 @@ pub(crate) trait TestFvk { initial_sapling_tree_size: u32, rng: &mut R, ) -> Self::Nullifier; + + #[allow(clippy::too_many_arguments)] + fn add_logical_action( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + nf: Self::Nullifier, + req: AddressType, + value: NonNegativeAmount, + initial_sapling_tree_size: u32, + rng: &mut R, + ) -> Self::Nullifier { + self.add_spend(ctx, nf, rng); + self.add_output( + ctx, + params, + height, + req, + value, + initial_sapling_tree_size, + rng, + ) + } } impl TestFvk for DiversifiableFullViewingKey { @@ -913,6 +937,36 @@ impl TestFvk for orchard::keys::FullViewingKey { note.nullifier(self) } + + // Override so we can merge the spend and output into a single action. + fn add_logical_action( + &self, + ctx: &mut CompactTx, + _: &P, + _: BlockHeight, + nf: Self::Nullifier, + req: AddressType, + value: NonNegativeAmount, + _: u32, + rng: &mut R, + ) -> Self::Nullifier { + let (j, scope) = match req { + AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External), + AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External), + AddressType::Internal => (0u32.into(), zip32::Scope::Internal), + }; + + let (cact, note) = compact_orchard_action( + nf, + self.address_at(j, scope), + value, + self.orchard_ovk(scope), + rng, + ); + ctx.actions.push(cact); + + note.nullifier(self) + } } #[allow(dead_code)] @@ -1100,24 +1154,22 @@ fn fake_compact_block_spending( let mut rng = OsRng; let mut ctx = fake_compact_tx(&mut rng); - // Create a fake spend - fvk.add_spend(&mut ctx, nf, &mut rng); - - // Create a fake Note for the payment - ctx.outputs - .push(compact_sapling_output(params, height, to, value, fvk.sapling_ovk(), &mut rng).0); - - // Create a fake Note for the change - fvk.add_output( + // Create a fake spend and a fake Note for the change + fvk.add_logical_action( &mut ctx, params, height, + nf, AddressType::Internal, (in_value - value).unwrap(), initial_sapling_tree_size, &mut rng, ); + // Create a fake Note for the payment + ctx.outputs + .push(compact_sapling_output(params, height, to, value, fvk.sapling_ovk(), &mut rng).0); + fake_compact_block_from_compact_tx(ctx, height, prev_hash, initial_sapling_tree_size, 0) } From c8d03f69efbc5574ba81a2e60f531a845f0cf016 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 8 Mar 2024 13:40:54 +0000 Subject: [PATCH 7/7] Extend `TestState::generate_next_block_spending` with Orchard support --- zcash_client_sqlite/src/testing.rs | 69 +++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 3717faecd..a37ff5c2c 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -19,7 +19,7 @@ use sapling::{ note_encryption::{sapling_note_encryption, SaplingDomain}, util::generate_random_rseed, zip32::DiversifiableFullViewingKey, - Note, Nullifier, PaymentAddress, + Note, Nullifier, }; #[allow(deprecated)] use zcash_client_backend::{ @@ -258,7 +258,7 @@ where &mut self, fvk: &Fvk, note: (Fvk::Nullifier, NonNegativeAmount), - to: PaymentAddress, + to: impl Into
, value: NonNegativeAmount, ) -> (BlockHeight, Cache::InsertResult) { let (height, prev_hash, initial_sapling_tree_size) = self @@ -272,7 +272,7 @@ where prev_hash, note, fvk, - to, + to.into(), value, initial_sapling_tree_size, ); @@ -1147,7 +1147,7 @@ fn fake_compact_block_spending( prev_hash: BlockHash, (nf, in_value): (Fvk::Nullifier, NonNegativeAmount), fvk: &Fvk, - to: PaymentAddress, + to: Address, value: NonNegativeAmount, initial_sapling_tree_size: u32, ) -> CompactBlock { @@ -1167,8 +1167,65 @@ fn fake_compact_block_spending( ); // Create a fake Note for the payment - ctx.outputs - .push(compact_sapling_output(params, height, to, value, fvk.sapling_ovk(), &mut rng).0); + match to { + Address::Sapling(recipient) => ctx.outputs.push( + compact_sapling_output( + params, + height, + recipient, + value, + fvk.sapling_ovk(), + &mut rng, + ) + .0, + ), + Address::Transparent(_) => panic!("transparent addresses not supported in compact blocks"), + Address::Unified(ua) => { + // This is annoying to implement, because the protocol-aware UA type has no + // concept of ZIP 316 preference order. + let mut done = false; + + #[cfg(feature = "orchard")] + if let Some(recipient) = ua.orchard() { + // Generate a dummy nullifier + let nullifier = + orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr()) + .unwrap(); + + ctx.actions.push( + compact_orchard_action( + nullifier, + *recipient, + value, + fvk.orchard_ovk(zip32::Scope::External), + &mut rng, + ) + .0, + ); + done = true; + } + + if !done { + if let Some(recipient) = ua.sapling() { + ctx.outputs.push( + compact_sapling_output( + params, + height, + *recipient, + value, + fvk.sapling_ovk(), + &mut rng, + ) + .0, + ); + done = true; + } + } + if !done { + panic!("No supported shielded receiver to send funds to"); + } + } + } fake_compact_block_from_compact_tx(ctx, height, prev_hash, initial_sapling_tree_size, 0) }