366 lines
12 KiB
Rust
366 lines
12 KiB
Rust
//! Test that we can scan the Zebra blockchain using the external `zcash_client_backend` crate
|
|
//! scanning functionality.
|
|
//!
|
|
//! This tests belong to the proof of concept stage of the external wallet support functionality.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use chrono::{DateTime, Utc};
|
|
|
|
use color_eyre::{Report, Result};
|
|
use ff::{Field, PrimeField};
|
|
use group::GroupEncoding;
|
|
use rand::{rngs::OsRng, thread_rng, RngCore};
|
|
|
|
use zcash_client_backend::{
|
|
encoding::encode_extended_full_viewing_key,
|
|
proto::compact_formats::{
|
|
ChainMetadata, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
|
|
},
|
|
};
|
|
use zcash_note_encryption::Domain;
|
|
use zcash_primitives::{
|
|
block::BlockHash,
|
|
consensus::BlockHeight,
|
|
constants::SPENDING_KEY_GENERATOR,
|
|
memo::MemoBytes,
|
|
sapling::{
|
|
note_encryption::{sapling_note_encryption, SaplingDomain},
|
|
util::generate_random_rseed,
|
|
value::NoteValue,
|
|
Note, Nullifier,
|
|
},
|
|
zip32,
|
|
};
|
|
|
|
use zebra_chain::{
|
|
amount::{Amount, NegativeAllowed},
|
|
block::{self, merkle, Block, Header, Height},
|
|
fmt::HexDebug,
|
|
parameters::Network,
|
|
primitives::{redjubjub, Groth16Proof},
|
|
sapling::{self, PerSpendAnchor, Spend, TransferData},
|
|
serialization::AtLeastOne,
|
|
transaction::{LockTime, Transaction},
|
|
transparent::{CoinbaseData, Input},
|
|
work::{difficulty::CompactDifficulty, equihash::Solution},
|
|
};
|
|
use zebra_state::SaplingScanningKey;
|
|
|
|
#[cfg(test)]
|
|
mod vectors;
|
|
|
|
/// The extended Sapling viewing key of [ZECpages](https://zecpages.com/boardinfo)
|
|
pub const ZECPAGES_SAPLING_VIEWING_KEY: &str = "zxviews1q0duytgcqqqqpqre26wkl45gvwwwd706xw608hucmvfalr759ejwf7qshjf5r9aa7323zulvz6plhttp5mltqcgs9t039cx2d09mgq05ts63n8u35hyv6h9nc9ctqqtue2u7cer2mqegunuulq2luhq3ywjcz35yyljewa4mgkgjzyfwh6fr6jd0dzd44ghk0nxdv2hnv4j5nxfwv24rwdmgllhe0p8568sgqt9ckt02v2kxf5ahtql6s0ltjpkckw8gtymxtxuu9gcr0swvz";
|
|
|
|
/// A fake viewing key in an incorrect format.
|
|
pub const FAKE_SAPLING_VIEWING_KEY: &str = "zxviewsfake";
|
|
|
|
/// Generates `num_keys` of [`SaplingScanningKey`]s for tests for the given [`Network`].
|
|
///
|
|
/// The keys are seeded only from their index in the returned `Vec`, so repeated calls return same
|
|
/// keys at a particular index.
|
|
pub fn mock_sapling_scanning_keys(num_keys: u8, network: Network) -> Vec<SaplingScanningKey> {
|
|
let mut keys: Vec<SaplingScanningKey> = vec![];
|
|
|
|
for seed in 0..num_keys {
|
|
keys.push(encode_extended_full_viewing_key(
|
|
network.sapling_efvk_hrp(),
|
|
&mock_sapling_efvk(&[seed]),
|
|
));
|
|
}
|
|
|
|
keys
|
|
}
|
|
|
|
/// Generates an [`zip32::sapling::ExtendedFullViewingKey`] from `seed` for tests.
|
|
#[allow(deprecated)]
|
|
pub fn mock_sapling_efvk(seed: &[u8]) -> zip32::sapling::ExtendedFullViewingKey {
|
|
// TODO: Use `to_diversifiable_full_viewing_key` since `to_extended_full_viewing_key` is
|
|
// deprecated.
|
|
zip32::sapling::ExtendedSpendingKey::master(seed).to_extended_full_viewing_key()
|
|
}
|
|
|
|
/// 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.
|
|
pub fn fake_block(
|
|
height: BlockHeight,
|
|
nf: Nullifier,
|
|
dfvk: &zip32::sapling::DiversifiableFullViewingKey,
|
|
value: u64,
|
|
tx_after: bool,
|
|
initial_sapling_tree_size: Option<u32>,
|
|
) -> (Block, u32) {
|
|
let header = Header {
|
|
version: 4,
|
|
previous_block_hash: block::Hash::default(),
|
|
merkle_root: merkle::Root::default(),
|
|
commitment_bytes: HexDebug::default(),
|
|
time: DateTime::<Utc>::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<Arc<Transaction>> = 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:
|
|
// https://github.com/zcash/librustzcash/blob/zcash_primitives-0.13.0/zcash_client_backend/src/scanning.rs#L635
|
|
// We need to copy because this is a test private function upstream.
|
|
pub fn fake_compact_block(
|
|
height: BlockHeight,
|
|
prev_hash: BlockHash,
|
|
nf: Nullifier,
|
|
dfvk: &zip32::sapling::DiversifiableFullViewingKey,
|
|
value: u64,
|
|
tx_after: bool,
|
|
initial_sapling_tree_size: Option<u32>,
|
|
) -> CompactBlock {
|
|
let to = dfvk.default_address().1;
|
|
|
|
// Create a fake Note for the account
|
|
let mut rng = OsRng;
|
|
let rseed = generate_random_rseed(
|
|
&zcash_primitives::consensus::Network::TestNetwork,
|
|
height,
|
|
&mut rng,
|
|
);
|
|
let note = Note::from_parts(to, NoteValue::from_raw(value), rseed);
|
|
let encryptor = sapling_note_encryption::<_, zcash_primitives::consensus::Network>(
|
|
Some(dfvk.fvk().ovk),
|
|
note.clone(),
|
|
MemoBytes::empty(),
|
|
&mut rng,
|
|
);
|
|
let cmu = note.cmu().to_bytes().to_vec();
|
|
let ephemeral_key =
|
|
SaplingDomain::<zcash_primitives::consensus::Network>::epk_bytes(encryptor.epk())
|
|
.0
|
|
.to_vec();
|
|
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
|
|
|
// Create a fake CompactBlock containing the note
|
|
let mut cb = CompactBlock {
|
|
hash: {
|
|
let mut hash = vec![0; 32];
|
|
rng.fill_bytes(&mut hash);
|
|
hash
|
|
},
|
|
prev_hash: prev_hash.0.to_vec(),
|
|
height: height.into(),
|
|
..Default::default()
|
|
};
|
|
|
|
// Add a random Sapling tx before ours
|
|
{
|
|
let mut tx = random_compact_tx(&mut rng);
|
|
tx.index = cb.vtx.len() as u64;
|
|
cb.vtx.push(tx);
|
|
}
|
|
|
|
let cspend = CompactSaplingSpend { nf: nf.0.to_vec() };
|
|
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;
|
|
ctx.spends.push(cspend);
|
|
ctx.outputs.push(cout);
|
|
ctx.index = cb.vtx.len() as u64;
|
|
cb.vtx.push(ctx);
|
|
|
|
// Optionally add another random Sapling tx after ours
|
|
if tx_after {
|
|
let mut tx = random_compact_tx(&mut rng);
|
|
tx.index = cb.vtx.len() as u64;
|
|
cb.vtx.push(tx);
|
|
}
|
|
|
|
cb.chain_metadata = initial_sapling_tree_size.map(|s| ChainMetadata {
|
|
sapling_commitment_tree_size: s + cb
|
|
.vtx
|
|
.iter()
|
|
.map(|tx| tx.outputs.len() as u32)
|
|
.sum::<u32>(),
|
|
..Default::default()
|
|
});
|
|
|
|
cb
|
|
}
|
|
|
|
/// Create a random compact transaction.
|
|
// This is an exact copy of `zcash_client_backend::scanning::random_compact_tx`:
|
|
// https://github.com/zcash/librustzcash/blob/zcash_primitives-0.13.0/zcash_client_backend/src/scanning.rs#L597
|
|
// We need to copy because this is a test private function upstream.
|
|
pub fn random_compact_tx(mut rng: impl RngCore) -> CompactTx {
|
|
let fake_nf = {
|
|
let mut nf = vec![0; 32];
|
|
rng.fill_bytes(&mut nf);
|
|
nf
|
|
};
|
|
let fake_cmu = {
|
|
let fake_cmu = bls12_381::Scalar::random(&mut rng);
|
|
fake_cmu.to_repr().as_ref().to_owned()
|
|
};
|
|
let fake_epk = {
|
|
let mut buffer = [0; 64];
|
|
rng.fill_bytes(&mut buffer);
|
|
let fake_esk = jubjub::Fr::from_bytes_wide(&buffer);
|
|
let fake_epk = SPENDING_KEY_GENERATOR * fake_esk;
|
|
fake_epk.to_bytes().to_vec()
|
|
};
|
|
let cspend = CompactSaplingSpend { nf: fake_nf };
|
|
let cout = CompactSaplingOutput {
|
|
cmu: fake_cmu,
|
|
ephemeral_key: fake_epk,
|
|
ciphertext: vec![0; 52],
|
|
};
|
|
let mut ctx = CompactTx::default();
|
|
let mut txid = vec![0; 32];
|
|
rng.fill_bytes(&mut txid);
|
|
ctx.hash = txid;
|
|
ctx.spends.push(cspend);
|
|
ctx.outputs.push(cout);
|
|
ctx
|
|
}
|
|
|
|
/// Converts [`CompactTx`] to [`Transaction::V4`].
|
|
pub fn compact_to_v4(tx: &CompactTx) -> Result<Transaction> {
|
|
let sk = redjubjub::SigningKey::<redjubjub::SpendAuth>::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::<redjubjub::SpendAuth>::from([0; 64]),
|
|
})
|
|
})
|
|
.collect::<Result<Vec<Spend<PerSpendAnchor>>>>()?;
|
|
|
|
let spends = AtLeastOne::<Spend<PerSpendAnchor>>::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::<Result<Vec<sapling::Output>>>()?;
|
|
|
|
let transfers = TransferData::SpendsAndMaybeOutputs {
|
|
shared_anchor: sapling::FieldNotPresent,
|
|
spends,
|
|
maybe_outputs,
|
|
};
|
|
|
|
let shielded_data = sapling::ShieldedData {
|
|
value_balance: Amount::<NegativeAllowed>::default(),
|
|
transfers,
|
|
binding_sig: redjubjub::Signature::<redjubjub::Binding>::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)),
|
|
})
|
|
}
|