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, memo::MemoBytes};
use ::sapling_crypto::{
constants::SPENDING_KEY_GENERATOR,
note_encryption::{sapling_note_encryption, SaplingDomain},
util::generate_random_rseed,
value::NoteValue,
zip32, Note, Nullifier,
};
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;
pub const ZECPAGES_SAPLING_VIEWING_KEY: &str = "zxviews1q0duytgcqqqqpqre26wkl45gvwwwd706xw608hucmvfalr759ejwf7qshjf5r9aa7323zulvz6plhttp5mltqcgs9t039cx2d09mgq05ts63n8u35hyv6h9nc9ctqqtue2u7cer2mqegunuulq2luhq3ywjcz35yyljewa4mgkgjzyfwh6fr6jd0dzd44ghk0nxdv2hnv4j5nxfwv24rwdmgllhe0p8568sgqt9ckt02v2kxf5ahtql6s0ltjpkckw8gtymxtxuu9gcr0swvz";
pub const FAKE_SAPLING_VIEWING_KEY: &str = "zxviewsfake";
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
}
#[allow(deprecated)]
pub fn mock_sapling_efvk(seed: &[u8]) -> zip32::ExtendedFullViewingKey {
zip32::ExtendedSpendingKey::master(seed).to_extended_full_viewing_key()
}
pub fn fake_block(
height: BlockHeight,
nf: Nullifier,
dfvk: &zip32::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,
)
}
pub fn fake_compact_block(
height: BlockHeight,
prev_hash: BlockHash,
nf: Nullifier,
dfvk: &zip32::DiversifiableFullViewingKey,
value: u64,
tx_after: bool,
initial_sapling_tree_size: Option<u32>,
) -> CompactBlock {
let to = dfvk.default_address().1;
let mut rng = OsRng;
let rseed = generate_random_rseed(
::sapling_crypto::note_encryption::Zip212Enforcement::Off,
&mut rng,
);
let note = Note::from_parts(to, NoteValue::from_raw(value), 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 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()
};
{
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);
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
}
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
}
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)),
})
}