zcash-sync/src/ledger/builder.rs

541 lines
21 KiB
Rust

use blake2b_simd::Params;
use byteorder::WriteBytesExt;
use byteorder::LE;
use ff::{PrimeField, Field};
use group::GroupEncoding;
use hex_literal::hex;
use jubjub::{Fr, Fq};
use orchard::keys::Scope;
use rand::{rngs::OsRng, RngCore, SeedableRng};
use rand_chacha::ChaChaRng;
use ripemd::{Digest, Ripemd160};
use secp256k1::PublicKey;
use sha2::Sha256;
use tonic::{Request, transport::Channel};
use zcash_client_backend::encoding::{decode_transparent_address, encode_extended_full_viewing_key, encode_transparent_address};
use zcash_primitives::consensus::Network;
use zcash_primitives::consensus::Parameters;
use zcash_primitives::legacy::{TransparentAddress, Script};
use zcash_primitives::transaction::components::{transparent, TxIn, OutPoint, TxOut};
use zcash_primitives::zip32::ExtendedFullViewingKey;
use crate::taddr::derive_from_pubkey;
use crate::{Destination, Source, TransactionPlan, RawTransaction, CompactTxStreamerClient};
use crate::ledger::transport::*;
use anyhow::{anyhow, Result};
use zcash_primitives::{
consensus::{BlockHeight, BranchId, MainNetwork},
merkle_tree::IncrementalWitness,
sapling::{
note_encryption::sapling_note_encryption, value::{NoteValue, ValueCommitment, ValueSum}, Diversifier, Node, Note,
PaymentAddress, Rseed, Nullifier, prover::TxProver, redjubjub::Signature,
},
transaction::{
components::{sapling::{Bundle, Authorized as SapAuthorized}, GROTH_PROOF_SIZE, Amount, OutputDescription, SpendDescription},
TransactionData, TxVersion, Authorized,
}, constants::PROOF_GENERATION_KEY_GENERATOR,
};
use zcash_proofs::{prover::LocalTxProver, sapling::SaplingProvingContext};
struct TransparentInputUnAuthorized {
utxo: OutPoint,
coin: TxOut,
}
struct SpendDescriptionUnAuthorized {
cv: ValueCommitment,
anchor: Fq,
pub nullifier: Nullifier,
rk: zcash_primitives::sapling::redjubjub::PublicKey,
zkproof: [u8; GROTH_PROOF_SIZE],
}
#[allow(dead_code)]
pub async fn show_public_keys() -> Result<()> {
let network = MainNetwork;
ledger_init().await?;
let pub_key = ledger_get_pubkey().await?;
let pub_key = PublicKey::from_slice(&pub_key)?;
let pub_key = pub_key.serialize();
let pub_key = Ripemd160::digest(&Sha256::digest(&pub_key));
let address = TransparentAddress::PublicKey(pub_key.into());
let address = encode_transparent_address(
&network.b58_pubkey_address_prefix(),
&network.b58_script_address_prefix(),
&address,
);
println!("address {}", address);
let dfvk = ledger_get_dfvk().await?;
let efvk = ExtendedFullViewingKey::from_diversifiable_full_viewing_key(&dfvk);
let efvk = encode_extended_full_viewing_key(MainNetwork.hrp_sapling_extended_full_viewing_key(), &efvk);
println!("efvk {}", efvk);
Ok(())
}
pub async fn build_broadcast_tx(network: &Network, client: &mut CompactTxStreamerClient<Channel>, tx_plan: &TransactionPlan, prover: &LocalTxProver) -> Result<String> {
ledger_init().await?;
let pubkey = ledger_get_pubkey().await?;
let ledger_taddr = derive_from_pubkey(network, &pubkey)?;
if ledger_taddr != tx_plan.taddr {
anyhow::bail!("This ledger wallet has a different address");
}
let taddr = &tx_plan.taddr;
let taddr = decode_transparent_address(
&network.b58_pubkey_address_prefix(),
&network.b58_script_address_prefix(),
taddr
)?.ok_or(anyhow!("Invalid taddr"))?;
let pkh = match taddr {
TransparentAddress::PublicKey(pkh) => pkh,
_ => unreachable!()
};
let tin_pubscript = taddr.script();
// Compute header digest
let mut h = Params::new()
.hash_length(32)
.personal(b"ZTxIdHeadersHash")
.to_state();
h.update(&hex!("050000800a27a726b4d0d6c200000000"));
h.write_u32::<LE>(tx_plan.expiry_height)?;
let header_digest = h.finalize();
let master_seed = ledger_init_tx(header_digest.as_bytes()).await?;
// For testing only
// let esk = "secret-extended-key-main1qwy5cttzqqqqpq8ksfmzqgz90r73yevcw6mvwuv5zuddak9zgl9epp6x308pczzez3hse753heepdk886yf7dmse5qvyl5jsuk5w4ejhtm30cpa862kq0pfu0z4zxxvyd523zeta3rr6lj0vg30mshf6wrlfucg47jv3ldspe0sv464uewwlglr0dzakssj8tdx27vq3dnerfa5z5fgf8vjutlcey3lwn4m6ncg8y4n2cgl64rd768uqg0yfvshljqt3g4x83kngv4guq06xx";
// let extsk = decode_extended_spending_key(MainNetwork.hrp_sapling_extended_spending_key(), &esk)
// .unwrap();
// let ovk = extsk.expsk.ovk;
// let proofgen_key = extsk.expsk.proof_generation_key();
// let dfvk = extsk.to_diversifiable_full_viewing_key();
let dfvk = ledger_get_dfvk().await?;
let ovk = dfvk.fvk.ovk;
let proofgen_key = ledger_get_proofgen_key().await?;
let fvk = dfvk.fvk;
let nf_key = proofgen_key.to_viewing_key().nk;
let o_fvk: [u8; 96] = ledger_get_o_fvk().await?.try_into().unwrap();
let o_fvk = orchard::keys::FullViewingKey::from_bytes(&o_fvk).ok_or(anyhow!("Invalid Orchard FVK"))?;
assert_eq!(PROOF_GENERATION_KEY_GENERATOR * proofgen_key.nsk, fvk.vk.nk.0);
// Derive rseed PRNG
let mut h = Params::new()
.hash_length(32)
.personal(b"ZRSeedPRNG__Hash")
.to_state();
h.update(&master_seed);
let main_rseed = h.finalize();
let mut rseed_rng = ChaChaRng::from_seed(main_rseed.as_bytes().try_into().unwrap());
// Derive alpha PRNG
let mut h = Params::new()
.hash_length(32)
.personal(b"ZAlphaPRNG__Hash")
.to_state();
h.update(&master_seed);
let alpha = h.finalize();
let mut alpha_rng = ChaChaRng::from_seed(alpha.as_bytes().try_into().unwrap());
let mut prevouts_hasher = Params::new()
.hash_length(32)
.personal(b"ZTxIdPrevoutHash")
.to_state();
let mut trscripts_hasher = Params::new()
.hash_length(32)
.personal(b"ZTxTrScriptsHash")
.to_state();
let mut sequences_hasher = Params::new()
.hash_length(32)
.personal(b"ZTxIdSequencHash")
.to_state();
let mut spends_compact_hasher = Params::new()
.hash_length(32)
.personal(b"ZTxIdSSpendCHash")
.to_state();
let mut spends_non_compact_hasher = Params::new()
.hash_length(32)
.personal(b"ZTxIdSSpendNHash")
.to_state();
let mut sapling_context = SaplingProvingContext::new();
let mut value_balance = ValueSum::zero();
let mut vin = vec![];
let mut shielded_spends = vec![];
for sp in tx_plan.spends.iter() {
match sp.source {
Source::Transparent { txid, index } => {
prevouts_hasher.update(&txid);
prevouts_hasher.write_u32::<LE>(index)?;
trscripts_hasher.update(&hex!("1976a914"));
trscripts_hasher.update(&pkh);
trscripts_hasher.update(&hex!("88ac"));
sequences_hasher.update(&hex!("FFFFFFFF"));
vin.push(TransparentInputUnAuthorized {
utxo: OutPoint::new(txid, index),
coin: TxOut { value: Amount::from_u64(sp.amount).unwrap(),
script_pubkey: tin_pubscript.clone(), // will always use the h/w address
}
});
ledger_add_t_input(sp.amount).await?;
}
Source::Sapling { diversifier, rseed, ref witness, .. } => {
let diversifier = Diversifier(diversifier);
let z_address = fvk.vk.to_payment_address(diversifier).ok_or(anyhow!("Invalid diversifier"))?;
let rseed = Rseed::BeforeZip212(Fr::from_bytes(&rseed).unwrap());
let note = Note::from_parts(z_address, NoteValue::from_raw(sp.amount), rseed);
let witness = IncrementalWitness::<Node>::read(&witness[..])?;
let merkle_path = witness.path().ok_or(anyhow!("Invalid merkle path"))?;
let node = Node::from_cmu(&note.cmu());
let anchor = Fq::from_bytes(&merkle_path.root(node).repr).unwrap();
let nullifier = note.nf(&nf_key, merkle_path.position);
let alpha = Fr::random(&mut alpha_rng);
println!("ALPHA {}", hex::encode(&alpha.to_bytes()));
let (zkproof, cv, rk) = prover.spend_proof(&mut sapling_context, proofgen_key.clone(), diversifier, rseed, alpha,
sp.amount, anchor, merkle_path.clone(), OsRng).map_err(|_| anyhow!("Error generating spend"))?;
value_balance = (value_balance + note.value()).ok_or(anyhow!("Invalid amount"))?;
spends_compact_hasher.update(nullifier.as_ref());
spends_non_compact_hasher.update(&cv.to_bytes());
spends_non_compact_hasher.update(&anchor.to_repr());
rk.write(&mut spends_non_compact_hasher)?;
shielded_spends.push(SpendDescriptionUnAuthorized { cv, anchor, nullifier, rk, zkproof });
}
Source::Orchard { .. } => {}
}
}
ledger_set_stage(2).await?;
let prevouts_digest = prevouts_hasher.finalize();
log::info!("PREVOUTS {}", hex::encode(prevouts_digest));
let pubscripts_digest = trscripts_hasher.finalize();
log::info!("PUBSCRIPTS {}", hex::encode(pubscripts_digest));
let sequences_digest = sequences_hasher.finalize();
log::info!("SEQUENCES {}", hex::encode(sequences_digest));
let spends_compact_digest = spends_compact_hasher.finalize();
log::info!("C SPENDS {}", hex::encode(spends_compact_digest));
let spends_non_compact_digest = spends_non_compact_hasher.finalize();
log::info!("NC SPENDS {}", hex::encode(spends_non_compact_digest));
let mut spends_hasher = Params::new()
.hash_length(32)
.personal(b"ZTxIdSSpendsHash")
.to_state();
if !shielded_spends.is_empty() {
spends_hasher.update(spends_compact_digest.as_bytes());
spends_hasher.update(spends_non_compact_digest.as_bytes());
}
let spends_digest = spends_hasher.finalize();
log::info!("SPENDS {}", hex::encode(spends_digest));
let mut output_memos_hasher = Params::new()
.hash_length(32)
.personal(b"ZTxIdSOutM__Hash")
.to_state();
let mut output_non_compact_hasher = Params::new()
.hash_length(32)
.personal(b"ZTxIdSOutN__Hash")
.to_state();
let mut vout = vec![];
let mut shielded_outputs = vec![];
for output in tx_plan.outputs.iter() {
if let Destination::Transparent(raw_address) = output.destination {
if raw_address[0] != 0 {
anyhow::bail!("Only t1 addresses are supported");
}
ledger_add_t_output(output.amount, &raw_address).await?;
let ta = TransparentAddress::PublicKey(raw_address[1..21].try_into().unwrap());
vout.push(TxOut {
value: Amount::from_u64(output.amount).unwrap(),
script_pubkey: ta.script()
});
}
}
ledger_set_stage(3).await?;
let has_transparent = !vin.is_empty() || !vout.is_empty();
for output in tx_plan.outputs.iter() {
if let Destination::Sapling(raw_address) = output.destination {
let recipient = PaymentAddress::from_bytes(&raw_address).unwrap();
let mut rseed = [0u8; 32];
rseed_rng.fill_bytes(&mut rseed);
let rseed = Rseed::AfterZip212(rseed);
let value = NoteValue::from_raw(output.amount);
value_balance = (value_balance - value).ok_or(anyhow!("Invalid amount"))?;
let note = Note::from_parts(recipient, value, rseed);
let rcm = note.rcm();
let cmu = note.cmu();
log::info!("cmu {}", hex::encode(cmu.to_bytes()));
let encryptor =
sapling_note_encryption::<_, MainNetwork>(Some(ovk.clone()), note, recipient, output.memo.clone(), &mut OsRng);
let (zkproof, cv) = prover.output_proof(
&mut sapling_context,
encryptor.esk().0,
recipient,
rcm,
output.amount,
&mut OsRng,
);
let enc_ciphertext = encryptor.encrypt_note_plaintext();
let out_ciphertext = encryptor.encrypt_outgoing_plaintext(&cv, &cmu, &mut OsRng);
let epk = encryptor.epk();
ledger_add_s_output(output.amount, &epk.to_bytes().0, &raw_address, &enc_ciphertext[0..52]).await?;
let memo = &enc_ciphertext[52..564];
output_memos_hasher.update(memo);
output_non_compact_hasher.update(&cv.as_inner().to_bytes());
output_non_compact_hasher.update(&enc_ciphertext[564..]);
output_non_compact_hasher.update(&out_ciphertext);
let ephemeral_key = epk.to_bytes();
shielded_outputs.push(OutputDescription { cv, cmu, ephemeral_key, enc_ciphertext, out_ciphertext, zkproof });
}
}
ledger_set_stage(4).await?;
let memos_digest = output_memos_hasher.finalize();
log::info!("MEMOS {}", hex::encode(memos_digest));
let outputs_nc_digest = output_non_compact_hasher.finalize();
log::info!("NC OUTPUTS {}", hex::encode(outputs_nc_digest));
ledger_set_transparent_merkle_proof(prevouts_digest.as_bytes(),
pubscripts_digest.as_bytes(), sequences_digest.as_bytes()).await?;
ledger_set_sapling_merkle_proof(spends_digest.as_bytes(), memos_digest.as_bytes(), outputs_nc_digest.as_bytes()).await?;
ledger_set_net_sapling(-tx_plan.net_chg[0]).await?;
ledger_set_stage(5).await?;
let orchard_spends: Vec<_> = tx_plan.spends.iter().filter(|&s|
if let Source::Orchard { .. } = s.source { true } else { false }
).cloned().collect();
let orchard_outputs: Vec<_> = tx_plan.outputs.iter().filter(|&o|
if let Destination::Orchard(_) = o.destination { true } else { false }
).cloned().collect();
let num_orchard_spends = orchard_spends.len();
let num_orchard_outputs = orchard_outputs.len();
let num_actions = num_orchard_spends.max(num_orchard_outputs);
let orchard_address = o_fvk.address_at(0u64, Scope::External);
let mut empty_memo = [0u8; 512];
empty_memo[0] = 0xF6;
// let mut actions = vec![];
for i in 0..num_actions {
let rcv = orchard::value::ValueCommitTrapdoor::random(OsRng);
let (_sk, dummy_fvk, dummy_note) = orchard::Note::dummy(&mut OsRng, None);
let _dummy_recipient = dummy_fvk.address_at(0u64, Scope::External);
let alpha = pasta_curves::pallas::Scalar::random(&mut alpha_rng);
let (fvk, spend_note) = if i < num_orchard_spends {
let sp = &tx_plan.spends[i];
let note = match &sp.source {
Source::Orchard { rseed, rho, .. } => {
let rho = orchard::note::Nullifier::from_bytes(rho).unwrap();
let note = orchard::Note::from_parts(
orchard_address.clone(),
orchard::value::NoteValue::from_raw(sp.amount),
rho,
orchard::note::RandomSeed::from_bytes(rseed.clone(), &rho).unwrap()).unwrap();
note
}
_ => unreachable!()
};
(o_fvk.clone(), note)
}
else {
(dummy_fvk, dummy_note)
};
let nf = spend_note.nullifier(&fvk);
let mut rseed = [0; 32];
rseed_rng.fill_bytes(&mut rseed);
let (output_note, memo) = if i < num_orchard_outputs {
let output = &orchard_outputs[i];
let address = match output.destination {
Destination::Orchard(address) => address,
_ => unreachable!()
};
let rseed = orchard::note::RandomSeed::from_bytes(rseed, &nf).unwrap();
let note = orchard::Note::from_parts(
orchard::Address::from_raw_address_bytes(&address).unwrap(),
orchard::value::NoteValue::from_raw(output.amount),
nf.clone(),
rseed).unwrap();
let memo = output.memo.as_array().clone();
(note, memo)
}
else {
(dummy_note.clone(), empty_memo)
};
let _rk = fvk.ak.randomize(&alpha);
let cm = output_note.commitment();
let cmx = cm.into();
let encryptor = orchard::note_encryption::OrchardNoteEncryption::new(
Some(o_fvk.to_ovk(Scope::External)),
output_note,
output_note.recipient(),
memo.clone()
);
let v_net = orchard::value::ValueSum::default();
let cv_net = orchard::value::ValueCommitment::derive(v_net, rcv.clone());
let _encrypted_note = orchard::note::TransmittedNoteCiphertext {
epk_bytes: encryptor.epk().to_bytes().0,
enc_ciphertext: encryptor.encrypt_note_plaintext(),
out_ciphertext: encryptor.encrypt_outgoing_plaintext(&cv_net, &cmx, &mut OsRng),
};
// compact outputs ZTxIdOrcActCHash
// nf
// cmx
// epk_bytes
// enc_ciphertext[..52]
// memo ZTxIdOrcActMHash
// enc_ciphertext[52..564]
// non compact ZTxIdOrcActNHash
// cv_net
// rk
// enc_ciphertext[564..]
// out_ciphertext
}
let mut vins = vec![];
for tin in vin.iter() {
let mut txin_hasher = Params::new()
.hash_length(32)
.personal(b"Zcash___TxInHash")
.to_state();
txin_hasher.update(tin.utxo.hash());
txin_hasher.update(&tin.utxo.n().to_le_bytes());
txin_hasher.update(&tin.coin.value.to_i64_le_bytes());
txin_hasher.update(&[0x19]); // add the script length
txin_hasher.update(&tin.coin.script_pubkey.0);
txin_hasher.update(&0xFFFFFFFFu32.to_le_bytes());
let txin_hash = txin_hasher.finalize();
log::info!("TXIN {}", hex::encode(txin_hash));
let signature = ledger_sign_transparent(txin_hash.as_bytes()).await?;
let signature = secp256k1::ecdsa::Signature::from_der(&signature)?;
let mut signature = signature.serialize_der().to_vec();
signature.extend(&[0x01]); // add SIG_HASH_ALL
// witness is PUSH(signature) PUSH(pk)
let script_sig = Script::default() << &*signature << &*pubkey;
let txin = TxIn::<transparent::Authorized> {
prevout: tin.utxo.clone(),
script_sig,
sequence: 0xFFFFFFFFu32,
};
vins.push(txin);
}
let mut signatures = vec![];
for _sp in shielded_spends.iter() {
let signature = ledger_sign_sapling().await?;
let signature = Signature::read(&*signature)?;
// Signature verification
// let rk = sp.rk();
// let mut message: Vec<u8> = vec![];
// message.write_all(&rk.0.to_bytes())?;
// message.write_all(sig_hash.as_ref())?;
// println!("MSG {}", hex::encode(&message));
// let verified = rk.verify_with_zip216(&message, &signature, SPENDING_KEY_GENERATOR, true);
// assert!(verified);
signatures.push(signature);
}
let transparent_bundle = transparent::Bundle::<transparent::Authorized> {
vin: vins,
vout,
authorization: transparent::Authorized
};
let shielded_spends: Vec<_> = shielded_spends.into_iter().zip(signatures.into_iter()).map(|(sp, spend_auth_sig)|
SpendDescription::<SapAuthorized> { cv: sp.cv, anchor: sp.anchor, nullifier: sp.nullifier, rk: sp.rk, zkproof: sp.zkproof,
spend_auth_sig }).collect();
let has_sapling = !shielded_spends.is_empty() || !shielded_outputs.is_empty();
let value: i64 = value_balance.try_into().unwrap();
let value = Amount::from_i64(value).unwrap();
let sighash = ledger_get_sighash().await?;
log::info!("TXID {}", hex::encode(&sighash));
let binding_sig = sapling_context.binding_sig(value, &sighash.try_into().unwrap()).unwrap();
let sapling_bundle = Bundle::<_>::from_parts(
shielded_spends, shielded_outputs, value,
SapAuthorized { binding_sig } );
let authed_tx: TransactionData<Authorized> = TransactionData {
version: TxVersion::Zip225,
consensus_branch_id: BranchId::Nu5,
lock_time: 0,
expiry_height: BlockHeight::from_u32(tx_plan.expiry_height),
transparent_bundle: if has_transparent { Some(transparent_bundle) } else { None },
sprout_bundle: None,
sapling_bundle: if has_sapling { Some(sapling_bundle) } else { None },
orchard_bundle: None,
};
let tx = authed_tx.freeze().unwrap();
let mut raw_tx = vec![];
tx.write_v5(&mut raw_tx)?;
ledger_end_tx().await?;
let response = client.send_transaction(Request::new(RawTransaction {
data: raw_tx,
height: 0,
})).await?.into_inner();
log::info!("{}", response.error_message);
Ok(response.error_message)
}