zebra_scan/
tests.rs

1//! scanning functionality.
2//!
3//! This tests belong to the proof of concept stage of the external wallet support functionality.
4
5use std::sync::Arc;
6
7use chrono::{DateTime, Utc};
8
9use color_eyre::{Report, Result};
10use ff::{Field, PrimeField};
11use group::GroupEncoding;
12use rand::{rngs::OsRng, thread_rng, RngCore};
13
14use zcash_client_backend::{
15    encoding::encode_extended_full_viewing_key,
16    proto::compact_formats::{
17        ChainMetadata, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
18    },
19};
20use zcash_note_encryption::Domain;
21use zcash_primitives::{block::BlockHash, consensus::BlockHeight, memo::MemoBytes};
22
23use ::sapling_crypto::{
24    constants::SPENDING_KEY_GENERATOR,
25    note_encryption::{sapling_note_encryption, SaplingDomain},
26    util::generate_random_rseed,
27    value::NoteValue,
28    zip32, Note, Nullifier,
29};
30
31use zebra_chain::{
32    amount::{Amount, NegativeAllowed},
33    block::{self, merkle, Block, Header, Height},
34    fmt::HexDebug,
35    parameters::Network,
36    primitives::{redjubjub, Groth16Proof},
37    sapling::{self, PerSpendAnchor, Spend, TransferData},
38    serialization::AtLeastOne,
39    transaction::{LockTime, Transaction},
40    transparent::{CoinbaseData, Input},
41    work::{difficulty::CompactDifficulty, equihash::Solution},
42};
43use zebra_state::SaplingScanningKey;
44
45#[cfg(test)]
46mod vectors;
47
48/// The extended Sapling viewing key of [ZECpages](https://zecpages.com/boardinfo)
49pub const ZECPAGES_SAPLING_VIEWING_KEY: &str = "zxviews1q0duytgcqqqqpqre26wkl45gvwwwd706xw608hucmvfalr759ejwf7qshjf5r9aa7323zulvz6plhttp5mltqcgs9t039cx2d09mgq05ts63n8u35hyv6h9nc9ctqqtue2u7cer2mqegunuulq2luhq3ywjcz35yyljewa4mgkgjzyfwh6fr6jd0dzd44ghk0nxdv2hnv4j5nxfwv24rwdmgllhe0p8568sgqt9ckt02v2kxf5ahtql6s0ltjpkckw8gtymxtxuu9gcr0swvz";
50
51/// A fake viewing key in an incorrect format.
52pub const FAKE_SAPLING_VIEWING_KEY: &str = "zxviewsfake";
53
54/// Generates `num_keys` of [`SaplingScanningKey`]s for tests for the given [`Network`].
55///
56/// The keys are seeded only from their index in the returned `Vec`, so repeated calls return same
57/// keys at a particular index.
58pub fn mock_sapling_scanning_keys(num_keys: u8, network: &Network) -> Vec<SaplingScanningKey> {
59    let mut keys: Vec<SaplingScanningKey> = vec![];
60
61    for seed in 0..num_keys {
62        keys.push(encode_extended_full_viewing_key(
63            network.sapling_efvk_hrp(),
64            &mock_sapling_efvk(&[seed]),
65        ));
66    }
67
68    keys
69}
70
71/// Generates an [`zip32::ExtendedFullViewingKey`] from `seed` for tests.
72#[allow(deprecated)]
73pub fn mock_sapling_efvk(seed: &[u8]) -> zip32::ExtendedFullViewingKey {
74    // TODO: Use `to_diversifiable_full_viewing_key` since `to_extended_full_viewing_key` is
75    // deprecated.
76    zip32::ExtendedSpendingKey::master(seed).to_extended_full_viewing_key()
77}
78
79/// Generates a fake block containing a Sapling output decryptable by `dfvk`.
80///
81/// The fake block has the following transactions in this order:
82/// 1. a transparent coinbase tx,
83/// 2. a V4 tx containing a random Sapling output,
84/// 3. a V4 tx containing a Sapling output decryptable by `dfvk`,
85/// 4. depending on the value of `tx_after`, another V4 tx containing a random Sapling output.
86pub fn fake_block(
87    height: BlockHeight,
88    nf: Nullifier,
89    dfvk: &zip32::DiversifiableFullViewingKey,
90    value: u64,
91    tx_after: bool,
92    initial_sapling_tree_size: Option<u32>,
93) -> (Block, u32) {
94    let header = Header {
95        version: 4,
96        previous_block_hash: block::Hash::default(),
97        merkle_root: merkle::Root::default(),
98        commitment_bytes: HexDebug::default(),
99        time: DateTime::<Utc>::default(),
100        difficulty_threshold: CompactDifficulty::default(),
101        nonce: HexDebug::default(),
102        solution: Solution::default(),
103    };
104
105    let block = fake_compact_block(
106        height,
107        BlockHash([0; 32]),
108        nf,
109        dfvk,
110        value,
111        tx_after,
112        initial_sapling_tree_size,
113    );
114
115    let mut transactions: Vec<Arc<Transaction>> = block
116        .vtx
117        .iter()
118        .map(|tx| compact_to_v4(tx).expect("A fake compact tx should be convertible to V4."))
119        .map(Arc::new)
120        .collect();
121
122    let coinbase_input = Input::Coinbase {
123        height: Height(1),
124        data: CoinbaseData::new(vec![]),
125        sequence: u32::MAX,
126    };
127
128    let coinbase = Transaction::V4 {
129        inputs: vec![coinbase_input],
130        outputs: vec![],
131        lock_time: LockTime::Height(Height(1)),
132        expiry_height: Height(1),
133        joinsplit_data: None,
134        sapling_shielded_data: None,
135    };
136
137    transactions.insert(0, Arc::new(coinbase));
138
139    let sapling_tree_size = block
140        .chain_metadata
141        .as_ref()
142        .unwrap()
143        .sapling_commitment_tree_size;
144
145    (
146        Block {
147            header: Arc::new(header),
148            transactions,
149        },
150        sapling_tree_size,
151    )
152}
153
154/// Create a fake compact block with provided fake account data.
155// This is a copy of zcash_primitives `fake_compact_block` where the `value` argument was changed to
156// be a number for easier conversion:
157// https://github.com/zcash/librustzcash/blob/zcash_primitives-0.13.0/zcash_client_backend/src/scanning.rs#L635
158// We need to copy because this is a test private function upstream.
159pub fn fake_compact_block(
160    height: BlockHeight,
161    prev_hash: BlockHash,
162    nf: Nullifier,
163    dfvk: &zip32::DiversifiableFullViewingKey,
164    value: u64,
165    tx_after: bool,
166    initial_sapling_tree_size: Option<u32>,
167) -> CompactBlock {
168    let to = dfvk.default_address().1;
169
170    // Create a fake Note for the account
171    let mut rng = OsRng;
172    let rseed = generate_random_rseed(
173        ::sapling_crypto::note_encryption::Zip212Enforcement::Off,
174        &mut rng,
175    );
176
177    let note = Note::from_parts(to, NoteValue::from_raw(value), rseed);
178    let encryptor = sapling_note_encryption::<_>(
179        Some(dfvk.fvk().ovk),
180        note.clone(),
181        *MemoBytes::empty().as_array(),
182        &mut rng,
183    );
184    let cmu = note.cmu().to_bytes().to_vec();
185    let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec();
186    let enc_ciphertext = encryptor.encrypt_note_plaintext();
187
188    // Create a fake CompactBlock containing the note
189    let mut cb = CompactBlock {
190        hash: {
191            let mut hash = vec![0; 32];
192            rng.fill_bytes(&mut hash);
193            hash
194        },
195        prev_hash: prev_hash.0.to_vec(),
196        height: height.into(),
197        ..Default::default()
198    };
199
200    // Add a random Sapling tx before ours
201    {
202        let mut tx = random_compact_tx(&mut rng);
203        tx.index = cb.vtx.len() as u64;
204        cb.vtx.push(tx);
205    }
206
207    let cspend = CompactSaplingSpend { nf: nf.0.to_vec() };
208    let cout = CompactSaplingOutput {
209        cmu,
210        ephemeral_key,
211        ciphertext: enc_ciphertext[..52].to_vec(),
212    };
213    let mut ctx = CompactTx::default();
214    let mut txid = vec![0; 32];
215    rng.fill_bytes(&mut txid);
216    ctx.hash = txid;
217    ctx.spends.push(cspend);
218    ctx.outputs.push(cout);
219    ctx.index = cb.vtx.len() as u64;
220    cb.vtx.push(ctx);
221
222    // Optionally add another random Sapling tx after ours
223    if tx_after {
224        let mut tx = random_compact_tx(&mut rng);
225        tx.index = cb.vtx.len() as u64;
226        cb.vtx.push(tx);
227    }
228
229    cb.chain_metadata = initial_sapling_tree_size.map(|s| ChainMetadata {
230        sapling_commitment_tree_size: s + cb
231            .vtx
232            .iter()
233            .map(|tx| tx.outputs.len() as u32)
234            .sum::<u32>(),
235        ..Default::default()
236    });
237
238    cb
239}
240
241/// Create a random compact transaction.
242// This is an exact copy of `zcash_client_backend::scanning::random_compact_tx`:
243// https://github.com/zcash/librustzcash/blob/zcash_primitives-0.13.0/zcash_client_backend/src/scanning.rs#L597
244// We need to copy because this is a test private function upstream.
245pub fn random_compact_tx(mut rng: impl RngCore) -> CompactTx {
246    let fake_nf = {
247        let mut nf = vec![0; 32];
248        rng.fill_bytes(&mut nf);
249        nf
250    };
251    let fake_cmu = {
252        let fake_cmu = bls12_381::Scalar::random(&mut rng);
253        fake_cmu.to_repr().to_vec()
254    };
255    let fake_epk = {
256        let mut buffer = [0; 64];
257        rng.fill_bytes(&mut buffer);
258        let fake_esk = jubjub::Fr::from_bytes_wide(&buffer);
259        let fake_epk = SPENDING_KEY_GENERATOR * fake_esk;
260        fake_epk.to_bytes().to_vec()
261    };
262    let cspend = CompactSaplingSpend { nf: fake_nf };
263    let cout = CompactSaplingOutput {
264        cmu: fake_cmu,
265        ephemeral_key: fake_epk,
266        ciphertext: vec![0; 52],
267    };
268    let mut ctx = CompactTx::default();
269    let mut txid = vec![0; 32];
270    rng.fill_bytes(&mut txid);
271    ctx.hash = txid;
272    ctx.spends.push(cspend);
273    ctx.outputs.push(cout);
274    ctx
275}
276
277/// Converts [`CompactTx`] to [`Transaction::V4`].
278pub fn compact_to_v4(tx: &CompactTx) -> Result<Transaction> {
279    let sk = redjubjub::SigningKey::<redjubjub::SpendAuth>::new(thread_rng());
280    let vk = redjubjub::VerificationKey::from(&sk);
281    let dummy_rk = sapling::keys::ValidatingKey::try_from(vk)
282        .expect("Internally generated verification key should be convertible to a validating key.");
283
284    let spends = tx
285        .spends
286        .iter()
287        .map(|spend| {
288            Ok(Spend {
289                cv: sapling::NotSmallOrderValueCommitment::default(),
290                per_spend_anchor: sapling::tree::Root::default(),
291                nullifier: sapling::Nullifier::from(
292                    spend.nf().map_err(|_| Report::msg("Invalid nullifier."))?.0,
293                ),
294                rk: dummy_rk.clone(),
295                zkproof: Groth16Proof([0; 192]),
296                spend_auth_sig: redjubjub::Signature::<redjubjub::SpendAuth>::from([0; 64]),
297            })
298        })
299        .collect::<Result<Vec<Spend<PerSpendAnchor>>>>()?;
300
301    let spends = AtLeastOne::<Spend<PerSpendAnchor>>::try_from(spends)?;
302
303    let maybe_outputs = tx
304        .outputs
305        .iter()
306        .map(|output| {
307            let mut ciphertext = output.ciphertext.clone();
308            ciphertext.resize(580, 0);
309            let ciphertext: [u8; 580] = ciphertext
310                .try_into()
311                .map_err(|_| Report::msg("Could not convert ciphertext to `[u8; 580]`"))?;
312            let enc_ciphertext = sapling::EncryptedNote::from(ciphertext);
313
314            Ok(sapling::Output {
315                cv: sapling::NotSmallOrderValueCommitment::default(),
316                cm_u: Option::from(jubjub::Fq::from_bytes(
317                    &output
318                        .cmu()
319                        .map_err(|_| Report::msg("Invalid commitment."))?
320                        .to_bytes(),
321                ))
322                .ok_or(Report::msg("Invalid commitment."))?,
323                ephemeral_key: sapling::keys::EphemeralPublicKey::try_from(
324                    output
325                        .ephemeral_key()
326                        .map_err(|_| Report::msg("Invalid ephemeral key."))?
327                        .0,
328                )
329                .map_err(Report::msg)?,
330                enc_ciphertext,
331                out_ciphertext: sapling::WrappedNoteKey::from([0; 80]),
332                zkproof: Groth16Proof([0; 192]),
333            })
334        })
335        .collect::<Result<Vec<sapling::Output>>>()?;
336
337    let transfers = TransferData::SpendsAndMaybeOutputs {
338        shared_anchor: sapling::FieldNotPresent,
339        spends,
340        maybe_outputs,
341    };
342
343    let shielded_data = sapling::ShieldedData {
344        value_balance: Amount::<NegativeAllowed>::default(),
345        transfers,
346        binding_sig: redjubjub::Signature::<redjubjub::Binding>::from([0; 64]),
347    };
348
349    Ok(Transaction::V4 {
350        inputs: vec![],
351        outputs: vec![],
352        lock_time: LockTime::Height(Height(0)),
353        expiry_height: Height(0),
354        joinsplit_data: None,
355        sapling_shielded_data: (Some(shielded_data)),
356    })
357}