From 5b7267042d847a874aacec51a423fa28dbd5dd33 Mon Sep 17 00:00:00 2001 From: Marco Granelli Date: Thu, 11 Apr 2024 20:02:28 +0200 Subject: [PATCH] Updates benchmarks for parallel masp verification --- crates/benches/Cargo.toml | 1 + crates/benches/native_vps.rs | 368 ++++++++++++++++++++++++++---- crates/benches/process_wrapper.rs | 20 +- 3 files changed, 332 insertions(+), 57 deletions(-) diff --git a/crates/benches/Cargo.toml b/crates/benches/Cargo.toml index 688b6db6012..cb07ef3288d 100644 --- a/crates/benches/Cargo.toml +++ b/crates/benches/Cargo.toml @@ -49,6 +49,7 @@ path = "wasm_opcodes.rs" namada = { path = "../namada", features = ["rand", "benches"] } namada_apps = { path = "../apps", features = ["benches"] } masp_primitives.workspace = true +masp_proofs = { workspace = true, features = ["multicore"] } borsh.workspace = true borsh-ext.workspace = true criterion = { version = "0.5", features = ["html_reports"] } diff --git a/crates/benches/native_vps.rs b/crates/benches/native_vps.rs index ca7cece8960..1225dd77930 100644 --- a/crates/benches/native_vps.rs +++ b/crates/benches/native_vps.rs @@ -5,9 +5,13 @@ use std::rc::Rc; use std::str::FromStr; use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; +use masp_primitives::sapling::redjubjub::PublicKey; use masp_primitives::sapling::Node; use masp_primitives::transaction::sighash::{signature_hash, SignableInput}; use masp_primitives::transaction::txid::TxIdDigester; +use masp_primitives::transaction::TransactionData; +use masp_proofs::group::GroupEncoding; +use masp_proofs::sapling::BatchValidator; use namada::core::address::{self, Address, InternalAddress}; use namada::core::eth_bridge_pool::{GasFee, PendingTransfer}; use namada::core::masp::{TransferSource, TransferTarget}; @@ -44,13 +48,10 @@ use namada::ledger::pgf::PgfVp; use namada::ledger::pos::PosVP; use namada::proof_of_stake; use namada::proof_of_stake::KeySeg; -use namada::sdk::masp::{ - check_convert, check_output, check_spend, partial_deauthorize, - preload_verifying_keys, PVKs, -}; +use namada::sdk::masp::{partial_deauthorize, preload_verifying_keys, PVKs}; use namada::sdk::masp_primitives::merkle_tree::CommitmentTree; use namada::sdk::masp_primitives::transaction::Transaction; -use namada::sdk::masp_proofs::sapling::SaplingVerificationContext; +use namada::sdk::masp_proofs::sapling::SaplingVerificationContextInner; use namada::state::{Epoch, StorageRead, StorageWrite, TxIndex}; use namada::token::{Amount, Transfer}; use namada::tx::{Code, Section, Tx}; @@ -62,6 +63,7 @@ use namada_apps::bench_utils::{ TX_TRANSFER_WASM, TX_UPDATE_STEWARD_COMMISSION, TX_VOTE_PROPOSAL_WASM, }; use namada_apps::wallet::defaults; +use rand_core::OsRng; fn governance(c: &mut Criterion) { let mut group = c.benchmark_group("vp_governance"); @@ -643,11 +645,11 @@ fn masp(c: &mut Criterion) { group.finish(); } +// Instead of benchmarking BatchValidator::check_bundle we benchmark the 4 +// functions that are called internally for better resolution fn masp_check_spend(c: &mut Criterion) { - let spend_vk = &preload_verifying_keys().spend_vk; - c.bench_function("vp_masp_check_spend", |b| { - b.iter_batched_ref( + b.iter_batched( || { let (_, signed_tx) = setup_storage_for_masp_verification("shielded"); @@ -670,7 +672,7 @@ fn masp_check_spend(c: &mut Criterion) { .first() .unwrap() .to_owned(); - let ctx = SaplingVerificationContext::new(true); + let ctx = SaplingVerificationContextInner::new(); let tx_data = transaction.deref(); // Partially deauthorize the transparent bundle let unauth_tx_data = partial_deauthorize(tx_data).unwrap(); @@ -680,11 +682,28 @@ fn masp_check_spend(c: &mut Criterion) { &SignableInput::Shielded, &txid_parts, ); + let zkproof = masp_proofs::bellman::groth16::Proof::read( + spend.zkproof.as_slice(), + ) + .unwrap(); - (ctx, spend, sighash) + (ctx, spend, sighash, zkproof) }, - |(ctx, spend, sighash)| { - assert!(check_spend(spend, sighash.as_ref(), ctx, spend_vk)); + |(mut ctx, spend, sighash, zkproof)| { + assert!(ctx.check_spend( + spend.cv, + spend.anchor, + &spend.nullifier.0, + PublicKey(spend.rk.0), + sighash.as_ref(), + spend.spend_auth_sig, + zkproof, + &mut (), + // We do sig and proofs verification in parallel, so just + // use dummy verifiers here + |_, _, _, _| true, + |_, _, _| true + )); }, BatchSize::SmallInput, ) @@ -692,10 +711,8 @@ fn masp_check_spend(c: &mut Criterion) { } fn masp_check_convert(c: &mut Criterion) { - let convert_vk = &preload_verifying_keys().convert_vk; - c.bench_function("vp_masp_check_convert", |b| { - b.iter_batched_ref( + b.iter_batched( || { let (_, signed_tx) = setup_storage_for_masp_verification("shielded"); @@ -718,12 +735,24 @@ fn masp_check_convert(c: &mut Criterion) { .first() .unwrap() .to_owned(); - let ctx = SaplingVerificationContext::new(true); + let ctx = SaplingVerificationContextInner::new(); + let zkproof = masp_proofs::bellman::groth16::Proof::read( + convert.zkproof.as_slice(), + ) + .unwrap(); - (ctx, convert) + (ctx, convert, zkproof) }, - |(ctx, convert)| { - assert!(check_convert(convert, ctx, convert_vk)); + |(mut ctx, convert, zkproof)| { + assert!(ctx.check_convert( + convert.cv, + convert.anchor, + zkproof, + &mut (), + // We do proofs verification in parallel, so just use dummy + // verifier here + |_, _, _| true, + )); }, BatchSize::SmallInput, ) @@ -731,10 +760,8 @@ fn masp_check_convert(c: &mut Criterion) { } fn masp_check_output(c: &mut Criterion) { - let output_vk = &preload_verifying_keys().output_vk; - c.bench_function("masp_vp_check_output", |b| { - b.iter_batched_ref( + b.iter_batched( || { let (_, signed_tx) = setup_storage_for_masp_verification("shielded"); @@ -757,12 +784,28 @@ fn masp_check_output(c: &mut Criterion) { .first() .unwrap() .to_owned(); - let ctx = SaplingVerificationContext::new(true); + let ctx = SaplingVerificationContextInner::new(); + let zkproof = masp_proofs::bellman::groth16::Proof::read( + output.zkproof.as_slice(), + ) + .unwrap(); + let epk = masp_proofs::jubjub::ExtendedPoint::from_bytes( + &output.ephemeral_key.0, + ) + .unwrap(); - (ctx, output) + (ctx, output, epk, zkproof) }, - |(ctx, output)| { - assert!(check_output(output, ctx, output_vk)); + |(mut ctx, output, epk, zkproof)| { + assert!(ctx.check_output( + output.cv, + output.cmu, + epk, + zkproof, + // We do proofs verification in parallel, so just use dummy + // verifier here + |_, _| true + )); }, BatchSize::SmallInput, ) @@ -770,12 +813,6 @@ fn masp_check_output(c: &mut Criterion) { } fn masp_final_check(c: &mut Criterion) { - let PVKs { - spend_vk, - convert_vk, - output_vk, - } = preload_verifying_keys(); - let (_, signed_tx) = setup_storage_for_masp_verification("shielded"); let transaction = signed_tx @@ -790,41 +827,277 @@ fn masp_final_check(c: &mut Criterion) { .unwrap() .to_owned(); let sapling_bundle = transaction.sapling_bundle().unwrap(); - let mut ctx = SaplingVerificationContext::new(true); // Partially deauthorize the transparent bundle let unauth_tx_data = partial_deauthorize(transaction.deref()).unwrap(); let txid_parts = unauth_tx_data.digest(TxIdDigester); let sighash = signature_hash(&unauth_tx_data, &SignableInput::Shielded, &txid_parts); + let mut ctx = SaplingVerificationContextInner::new(); // Check spends, converts and outputs before the final check assert!(sapling_bundle.shielded_spends.iter().all(|spend| { - check_spend(spend, sighash.as_ref(), &mut ctx, spend_vk) + let zkproof = masp_proofs::bellman::groth16::Proof::read( + spend.zkproof.as_slice(), + ) + .unwrap(); + + ctx.check_spend( + spend.cv, + spend.anchor, + &spend.nullifier.0, + PublicKey(spend.rk.0), + sighash.as_ref(), + spend.spend_auth_sig, + zkproof, + &mut (), + |_, _, _, _| true, + |_, _, _| true, + ) + })); + assert!(sapling_bundle.shielded_converts.iter().all(|convert| { + let zkproof = masp_proofs::bellman::groth16::Proof::read( + convert.zkproof.as_slice(), + ) + .unwrap(); + ctx.check_convert( + convert.cv, + convert.anchor, + zkproof, + &mut (), + |_, _, _| true, + ) + })); + assert!(sapling_bundle.shielded_outputs.iter().all(|output| { + let zkproof = masp_proofs::bellman::groth16::Proof::read( + output.zkproof.as_slice(), + ) + .unwrap(); + let epk = masp_proofs::jubjub::ExtendedPoint::from_bytes( + &output.ephemeral_key.0, + ) + .unwrap(); + ctx.check_output( + output.cv, + output.cmu, + epk.to_owned(), + zkproof, + |_, _| true, + ) })); - assert!( - sapling_bundle - .shielded_converts - .iter() - .all(|convert| check_convert(convert, &mut ctx, convert_vk)) - ); - assert!( - sapling_bundle - .shielded_outputs - .iter() - .all(|output| check_output(output, &mut ctx, output_vk)) - ); c.bench_function("vp_masp_final_check", |b| { b.iter(|| { assert!(ctx.final_check( sapling_bundle.value_balance.clone(), sighash.as_ref(), - sapling_bundle.authorization.binding_sig + sapling_bundle.authorization.binding_sig, + // We do sig verification in parallel, so just use dummy + // verifier here + |_, _, _| true )) }) }); } +#[derive(Debug)] +enum RequestedItem { + Signature, + Spend, + Convert, + Output, +} + +// Removes the unneeded notes from the generated transaction and replicates the +// remaining ones if needed +fn customize_masp_tx_data( + multi: bool, + request: &RequestedItem, +) -> Option<( + TransactionData, + Transaction, +)> { + let (_, tx) = setup_storage_for_masp_verification("unshielding"); + let transaction = tx + .sections + .into_iter() + .filter_map(|section| match section { + Section::MaspTx(transaction) => Some(transaction), + _ => None, + }) + .collect::>() + .first() + .unwrap() + .to_owned(); + let mut sapling_bundle = transaction.sapling_bundle().unwrap().to_owned(); + + match request { + RequestedItem::Signature => { + if multi { + // No multisig benchmark + return None; + } + // ensure we only have one signature to verify (the binding one) and + // no proofs + sapling_bundle.shielded_spends.clear(); + sapling_bundle.shielded_converts.clear(); + sapling_bundle.shielded_outputs.clear(); + assert_eq!(sapling_bundle.shielded_spends.len(), 0); + assert_eq!(sapling_bundle.shielded_converts.len(), 0); + assert_eq!(sapling_bundle.shielded_outputs.len(), 0); + } + RequestedItem::Spend => { + if multi { + // ensure we only have two spend proofs + sapling_bundle.shielded_spends = [ + sapling_bundle.shielded_spends.clone(), + sapling_bundle.shielded_spends, + ] + .concat(); + sapling_bundle.shielded_outputs.clear(); + sapling_bundle.shielded_converts.clear(); + assert_eq!(sapling_bundle.shielded_spends.len(), 2); + } else { + // ensure we only have one spend proof + sapling_bundle.shielded_outputs.clear(); + sapling_bundle.shielded_converts.clear(); + assert_eq!(sapling_bundle.shielded_spends.len(), 1); + } + assert_eq!(sapling_bundle.shielded_converts.len(), 0); + assert_eq!(sapling_bundle.shielded_outputs.len(), 0); + } + RequestedItem::Convert => { + if multi { + // ensure we only have two convert proofs + sapling_bundle.shielded_converts = [ + sapling_bundle.shielded_converts.clone(), + sapling_bundle.shielded_converts, + ] + .concat(); + sapling_bundle.shielded_spends.clear(); + sapling_bundle.shielded_outputs.clear(); + assert_eq!(sapling_bundle.shielded_converts.len(), 2); + } else { + // ensure we only have one convert proof + sapling_bundle.shielded_spends.clear(); + sapling_bundle.shielded_outputs.clear(); + assert_eq!(sapling_bundle.shielded_converts.len(), 1); + } + assert_eq!(sapling_bundle.shielded_spends.len(), 0); + assert_eq!(sapling_bundle.shielded_outputs.len(), 0); + } + RequestedItem::Output => { + // From the cost remove the cost of signature(s) validation + if multi { + // ensure we only have two output proofs + sapling_bundle.shielded_outputs = [ + sapling_bundle.shielded_outputs.clone(), + sapling_bundle.shielded_outputs, + ] + .concat(); + sapling_bundle.shielded_spends.clear(); + sapling_bundle.shielded_converts.clear(); + assert_eq!(sapling_bundle.shielded_outputs.len(), 2); + } else { + // ensure we only have one output proof + sapling_bundle.shielded_spends.clear(); + sapling_bundle.shielded_converts.clear(); + assert_eq!(sapling_bundle.shielded_outputs.len(), 1); + } + assert_eq!(sapling_bundle.shielded_spends.len(), 0); + assert_eq!(sapling_bundle.shielded_converts.len(), 0); + } + }; + + Some(( + TransactionData::from_parts( + transaction.version(), + transaction.consensus_branch_id(), + transaction.lock_time(), + transaction.expiry_height(), + transaction.transparent_bundle().cloned(), + Some(sapling_bundle), + ), + transaction, + )) +} + +// For signatures: it's impossible to benchmark more than one signature without +// pulling in the cost of a proof, so we just benchmark the cost of one binding +// signature and then use its cost. We don't discount by the numebr of cores +// because there might be the need for a call to the fallback single +// verification.. For proofs: benchmarks both one and two proofs and take the +// difference as the variable cost for every proofs/sigs. Compute the cost of +// the non parallel parts (with the diff) and charge that if at least one note +// is present, then charge the variable cost multiplied by the number of notes +// and divided by the number of cores +fn masp_batch_validate(c: &mut Criterion) { + let mut group = c.benchmark_group("masp_batch_validate"); + let PVKs { + spend_vk, + convert_vk, + output_vk, + } = preload_verifying_keys(); + + for multi in [true, false] { + for bench in [ + RequestedItem::Signature, + RequestedItem::Spend, + RequestedItem::Convert, + RequestedItem::Output, + ] { + // From the cost of proofs remove the cost of signature(s) + // validation + let (tx_data, transaction) = + if let Some(data) = customize_masp_tx_data(multi, &bench) { + data + } else { + continue; + }; + + // Partially deauthorize the transparent bundle + let unauth_tx_data = + partial_deauthorize(transaction.deref()).unwrap(); + let txid_parts = unauth_tx_data.digest(TxIdDigester); + let sighash = signature_hash( + &unauth_tx_data, + &SignableInput::Shielded, + &txid_parts, + ) + .as_ref() + .to_owned(); + let sapling_bundle = tx_data.sapling_bundle().unwrap(); + + let bench_name = if multi { + format!("{:#?}_multi", bench) + } else { + format!("{:#?}_single", bench) + }; + group.bench_function(bench_name, |b| { + b.iter_batched( + || { + let mut ctx = BatchValidator::new(); + // Check bundle first + if !ctx.check_bundle(sapling_bundle.to_owned(), sighash) + { + panic!("Failed check bundle"); + } + + ctx + }, + |ctx| { + assert!( + ctx.validate( + spend_vk, convert_vk, output_vk, OsRng + ) + ) + }, + BatchSize::SmallInput, + ) + }); + } + } +} + fn pgf(c: &mut Criterion) { let mut group = c.benchmark_group("vp_pgf"); @@ -1416,6 +1689,7 @@ criterion_group!( masp_check_convert, masp_check_output, masp_final_check, + masp_batch_validate, vp_multitoken, pgf, eth_bridge_nut, diff --git a/crates/benches/process_wrapper.rs b/crates/benches/process_wrapper.rs index da26796e780..d8f8b8bb4a3 100644 --- a/crates/benches/process_wrapper.rs +++ b/crates/benches/process_wrapper.rs @@ -56,7 +56,7 @@ fn process_tx(c: &mut Criterion) { let datetime = DateTimeUtc::now(); c.bench_function("wrapper_tx_validation", |b| { - b.iter_batched( + b.iter_batched_ref( || { ( shell.state.in_mem().tx_queue.clone(), @@ -70,10 +70,10 @@ fn process_tx(c: &mut Criterion) { }, |( tx_queue, - mut temp_state, - mut validation_meta, - mut vp_wasm_cache, - mut tx_wasm_cache, + temp_state, + validation_meta, + vp_wasm_cache, + tx_wasm_cache, block_proposer, )| { assert_eq!( @@ -82,12 +82,12 @@ fn process_tx(c: &mut Criterion) { .check_proposal_tx( &wrapper, &mut tx_queue.iter(), - &mut validation_meta, - &mut temp_state, + validation_meta, + temp_state, datetime, - &mut vp_wasm_cache, - &mut tx_wasm_cache, - &block_proposer + vp_wasm_cache, + tx_wasm_cache, + block_proposer ) .code, 0