diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index aa3e59ac2..7562bfb08 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -76,3 +76,7 @@ zebra-test = { path = "../zebra-test/" } [[bench]] name = "block" harness = false + +[[bench]] +name = "redpallas" +harness = false diff --git a/zebra-chain/benches/redpallas.rs b/zebra-chain/benches/redpallas.rs new file mode 100644 index 000000000..febb0c0e9 --- /dev/null +++ b/zebra-chain/benches/redpallas.rs @@ -0,0 +1,96 @@ +use std::convert::TryFrom; + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use rand::{thread_rng, Rng}; + +use zebra_chain::primitives::redpallas::*; + +enum Item { + SpendAuth { + vk_bytes: VerificationKeyBytes, + sig: Signature, + }, + Binding { + vk_bytes: VerificationKeyBytes, + sig: Signature, + }, +} + +fn sigs_with_distinct_keys() -> impl Iterator { + std::iter::repeat_with(|| { + let mut rng = thread_rng(); + let msg = b""; + match rng.gen::() % 2 { + 0 => { + let sk = SigningKey::::new(thread_rng()); + let vk_bytes = VerificationKey::from(&sk).into(); + let sig = sk.sign(thread_rng(), &msg[..]); + Item::SpendAuth { vk_bytes, sig } + } + 1 => { + let sk = SigningKey::::new(thread_rng()); + let vk_bytes = VerificationKey::from(&sk).into(); + let sig = sk.sign(thread_rng(), &msg[..]); + Item::Binding { vk_bytes, sig } + } + _ => panic!(), + } + }) +} + +fn bench_batch_verify(c: &mut Criterion) { + let mut group = c.benchmark_group("Batch Verification"); + for &n in [8usize, 16, 24, 32, 40, 48, 56, 64].iter() { + group.throughput(Throughput::Elements(n as u64)); + + let sigs = sigs_with_distinct_keys().take(n).collect::>(); + + group.bench_with_input( + BenchmarkId::new("Unbatched verification", n), + &sigs, + |b, sigs| { + b.iter(|| { + for item in sigs.iter() { + let msg = b"Bench"; + match item { + Item::SpendAuth { vk_bytes, sig } => { + let _ = VerificationKey::try_from(*vk_bytes) + .and_then(|vk| vk.verify(msg, sig)); + } + Item::Binding { vk_bytes, sig } => { + let _ = VerificationKey::try_from(*vk_bytes) + .and_then(|vk| vk.verify(msg, sig)); + } + } + } + }) + }, + ); + + group.bench_with_input( + BenchmarkId::new("Batched verification", n), + &sigs, + |b, sigs| { + b.iter(|| { + let mut batch = batch::Verifier::new(); + for item in sigs.iter() { + let msg = b"Bench"; + match item { + Item::SpendAuth { vk_bytes, sig } => { + batch.queue((*vk_bytes, *sig, msg)); + } + Item::Binding { vk_bytes, sig } => { + batch.queue((*vk_bytes, *sig, msg)); + } + } + } + batch.verify(thread_rng()) + }) + }, + ); + } + group.finish(); +} + +criterion_group!(benches, bench_batch_verify); +criterion_main!(benches); diff --git a/zebra-chain/src/primitives/redpallas.rs b/zebra-chain/src/primitives/redpallas.rs index 79b07fed5..17bcc2503 100644 --- a/zebra-chain/src/primitives/redpallas.rs +++ b/zebra-chain/src/primitives/redpallas.rs @@ -13,10 +13,12 @@ use group::GroupEncoding; use halo2::pasta::pallas; +pub mod batch; mod constants; #[allow(missing_docs)] mod error; mod hash; +mod scalar_mul; mod signature; mod signing_key; #[cfg(test)] diff --git a/zebra-chain/src/primitives/redpallas/batch.rs b/zebra-chain/src/primitives/redpallas/batch.rs new file mode 100644 index 000000000..1857e24aa --- /dev/null +++ b/zebra-chain/src/primitives/redpallas/batch.rs @@ -0,0 +1,265 @@ +// -*- mode: rust; -*- +// +// This file is part of redpallas. +// Copyright (c) 2019-2021 Zcash Foundation +// See LICENSE for licensing information. +// +// Authors: +// - Deirdre Connolly +// - Henry de Valence + +//! Performs batch RedPallas signature verification. +//! +//! Batch verification asks whether *all* signatures in some set are valid, +//! rather than asking whether *each* of them is valid. This allows sharing +//! computations among all signature verifications, performing less work overall +//! at the cost of higher latency (the entire batch must complete), complexity of +//! caller code (which must assemble a batch of signatures across work-items), +//! and loss of the ability to easily pinpoint failing signatures. +//! + +use std::convert::TryFrom; + +use group::{Group, GroupEncoding}; +use halo2::arithmetic::FieldExt; +use rand_core::{CryptoRng, RngCore}; + +use super::{private::Sealed, scalar_mul::VartimeMultiscalarMul, *}; + +// Shim to generate a random 128bit value in a [u64; 4], without +// importing `rand`. +fn gen_128_bits(mut rng: R) -> [u64; 4] { + let mut bytes = [0u64; 4]; + bytes[0] = rng.next_u64(); + bytes[1] = rng.next_u64(); + bytes +} + +#[derive(Clone, Debug)] +enum Inner { + SpendAuth { + vk_bytes: VerificationKeyBytes, + sig: Signature, + c: pallas::Scalar, + }, + Binding { + vk_bytes: VerificationKeyBytes, + sig: Signature, + c: pallas::Scalar, + }, +} + +/// A batch verification item. +/// +/// This struct exists to allow batch processing to be decoupled from the +/// lifetime of the message. This is useful when using the batch verification API +/// in an async context. +#[derive(Clone, Debug)] +pub struct Item { + inner: Inner, +} + +impl<'msg, M: AsRef<[u8]>> + From<( + VerificationKeyBytes, + Signature, + &'msg M, + )> for Item +{ + fn from( + (vk_bytes, sig, msg): ( + VerificationKeyBytes, + Signature, + &'msg M, + ), + ) -> Self { + // Compute c now to avoid dependency on the msg lifetime. + let c = HStar::default() + .update(&sig.r_bytes[..]) + .update(&vk_bytes.bytes[..]) + .update(msg) + .finalize(); + Self { + inner: Inner::SpendAuth { vk_bytes, sig, c }, + } + } +} + +impl<'msg, M: AsRef<[u8]>> From<(VerificationKeyBytes, Signature, &'msg M)> + for Item +{ + fn from( + (vk_bytes, sig, msg): (VerificationKeyBytes, Signature, &'msg M), + ) -> Self { + // Compute c now to avoid dependency on the msg lifetime. + let c = HStar::default() + .update(&sig.r_bytes[..]) + .update(&vk_bytes.bytes[..]) + .update(msg) + .finalize(); + Self { + inner: Inner::Binding { vk_bytes, sig, c }, + } + } +} + +impl Item { + /// Perform non-batched verification of this `Item`. + /// + /// This is useful (in combination with `Item::clone`) for implementing fallback + /// logic when batch verification fails. In contrast to + /// [`VerificationKey::verify`](crate::VerificationKey::verify), which requires + /// borrowing the message data, the `Item` type is unlinked from the lifetime of + /// the message. + #[allow(non_snake_case)] + pub fn verify_single(self) -> Result<(), Error> { + match self.inner { + Inner::Binding { vk_bytes, sig, c } => VerificationKey::::try_from(vk_bytes) + .and_then(|vk| vk.verify_prehashed(&sig, c)), + Inner::SpendAuth { vk_bytes, sig, c } => { + VerificationKey::::try_from(vk_bytes) + .and_then(|vk| vk.verify_prehashed(&sig, c)) + } + } + } +} + +#[derive(Default)] +/// A batch verification context. +pub struct Verifier { + /// Signature data queued for verification. + signatures: Vec, +} + +impl Verifier { + /// Construct a new batch verifier. + pub fn new() -> Verifier { + Verifier::default() + } + + /// Queue an Item for verification. + pub fn queue>(&mut self, item: I) { + self.signatures.push(item.into()); + } + + /// Perform batch verification, returning `Ok(())` if all signatures were + /// valid and `Err` otherwise. + /// + /// The batch verification equation is: + /// + /// h_G * -[sum(z_i * s_i)]P_G + sum(\[z_i\]R_i + [z_i * c_i]VK_i) = 0_G + /// + /// which we split out into: + /// + /// h_G * -[sum(z_i * s_i)]P_G + sum(\[z_i\]R_i) + sum([z_i * c_i]VK_i) = 0_G + /// + /// so that we can use multiscalar multiplication speedups. + /// + /// where for each signature i, + /// - VK_i is the verification key; + /// - R_i is the signature's R value; + /// - s_i is the signature's s value; + /// - c_i is the hash of the message and other data; + /// - z_i is a random 128-bit Scalar; + /// - h_G is the cofactor of the group; + /// - P_G is the generator of the subgroup; + /// + /// Since RedPallas uses different subgroups for different types + /// of signatures, SpendAuth's and Binding's, we need to have yet + /// another point and associated scalar accumulator for all the + /// signatures of each type in our batch, but we can still + /// amortize computation nicely in one multiscalar multiplication: + /// + /// h_G * ( [-sum(z_i * s_i): i_type == SpendAuth]P_SpendAuth + [-sum(z_i * s_i): i_type == Binding]P_Binding + sum(\[z_i\]R_i) + sum([z_i * c_i]VK_i) ) = 0_G + /// + /// As follows elliptic curve scalar multiplication convention, + /// scalar variables are lowercase and group point variables + /// are uppercase. This does not exactly match the RedDSA + /// notation in the [protocol specification §B.1][ps]. + /// + /// [ps]: https://zips.z.cash/protocol/protocol.pdf#reddsabatchverify + #[allow(non_snake_case)] + pub fn verify(self, mut rng: R) -> Result<(), Error> { + let n = self.signatures.len(); + + let mut VK_coeffs = Vec::with_capacity(n); + let mut VKs = Vec::with_capacity(n); + let mut R_coeffs = Vec::with_capacity(self.signatures.len()); + let mut Rs = Vec::with_capacity(self.signatures.len()); + let mut P_spendauth_coeff = pallas::Scalar::zero(); + let mut P_binding_coeff = pallas::Scalar::zero(); + + for item in self.signatures.iter() { + let (s_bytes, r_bytes, c) = match item.inner { + Inner::SpendAuth { sig, c, .. } => (sig.s_bytes, sig.r_bytes, c), + Inner::Binding { sig, c, .. } => (sig.s_bytes, sig.r_bytes, c), + }; + + let s = { + // XXX-pallas: should not use CtOption here + let maybe_scalar = pallas::Scalar::from_bytes(&s_bytes); + if maybe_scalar.is_some().into() { + maybe_scalar.unwrap() + } else { + return Err(Error::InvalidSignature); + } + }; + + let R = { + // XXX-pallas: should not use CtOption here + // XXX-pallas: inconsistent ownership in from_bytes + let maybe_point = pallas::Affine::from_bytes(&r_bytes); + if maybe_point.is_some().into() { + pallas::Point::from(maybe_point.unwrap()) + } else { + return Err(Error::InvalidSignature); + } + }; + + let VK = match item.inner { + Inner::SpendAuth { vk_bytes, .. } => { + VerificationKey::::try_from(vk_bytes.bytes)?.point + } + Inner::Binding { vk_bytes, .. } => { + VerificationKey::::try_from(vk_bytes.bytes)?.point + } + }; + + let z = pallas::Scalar::from_raw(gen_128_bits(&mut rng)); + + let P_coeff = z * s; + match item.inner { + Inner::SpendAuth { .. } => { + P_spendauth_coeff -= P_coeff; + } + Inner::Binding { .. } => { + P_binding_coeff -= P_coeff; + } + }; + + R_coeffs.push(z); + Rs.push(R); + + VK_coeffs.push(pallas::Scalar::zero() + (z * c)); + VKs.push(VK); + } + + use std::iter::once; + + let scalars = once(&P_spendauth_coeff) + .chain(once(&P_binding_coeff)) + .chain(VK_coeffs.iter()) + .chain(R_coeffs.iter()); + + let basepoints = [SpendAuth::basepoint(), Binding::basepoint()]; + let points = basepoints.iter().chain(VKs.iter()).chain(Rs.iter()); + + let check = pallas::Point::vartime_multiscalar_mul(scalars, points); + + if check.is_identity().into() { + Ok(()) + } else { + Err(Error::InvalidSignature) + } + } +} diff --git a/zebra-chain/src/primitives/redpallas/scalar_mul.rs b/zebra-chain/src/primitives/redpallas/scalar_mul.rs new file mode 100644 index 000000000..c9802e76a --- /dev/null +++ b/zebra-chain/src/primitives/redpallas/scalar_mul.rs @@ -0,0 +1,207 @@ +// -*- mode: rust; -*- +// +// This file is part of redpallas. +// Copyright (c) 2019-2021 Zcash Foundation +// Copyright (c) 2017-2021 isis agora lovecruft, Henry de Valence +// See LICENSE for licensing information. +// +// Authors: +// - isis agora lovecruft +// - Henry de Valence +// - Deirdre Connolly + +use std::{borrow::Borrow, fmt::Debug}; + +use group::Group; +use halo2::{arithmetic::FieldExt, pasta::pallas}; + +pub trait NonAdjacentForm { + fn non_adjacent_form(&self, w: usize) -> [i8; 256]; +} + +/// A trait for variable-time multiscalar multiplication without precomputation. +pub trait VartimeMultiscalarMul { + /// The type of point being multiplied, e.g., `AffinePoint`. + type Point; + + /// Given an iterator of public scalars and an iterator of + /// `Option`s of points, compute either `Some(Q)`, where + /// $$ + /// Q = c\_1 P\_1 + \cdots + c\_n P\_n, + /// $$ + /// if all points were `Some(P_i)`, or else return `None`. + fn optional_multiscalar_mul(scalars: I, points: J) -> Option + where + I: IntoIterator, + I::Item: Borrow, + J: IntoIterator>; + + /// Given an iterator of public scalars and an iterator of + /// public points, compute + /// $$ + /// Q = c\_1 P\_1 + \cdots + c\_n P\_n, + /// $$ + /// using variable-time operations. + /// + /// It is an error to call this function with two iterators of different lengths. + fn vartime_multiscalar_mul(scalars: I, points: J) -> Self::Point + where + I: IntoIterator, + I::Item: Borrow, + J: IntoIterator, + J::Item: Borrow, + Self::Point: Clone, + { + Self::optional_multiscalar_mul( + scalars, + points.into_iter().map(|p| Some(p.borrow().clone())), + ) + .unwrap() + } +} + +impl NonAdjacentForm for pallas::Scalar { + /// Compute a width-\\(w\\) "Non-Adjacent Form" of this scalar. + /// + /// Thanks to curve25519-dalek + fn non_adjacent_form(&self, w: usize) -> [i8; 256] { + // required by the NAF definition + debug_assert!(w >= 2); + // required so that the NAF digits fit in i8 + debug_assert!(w <= 8); + + use byteorder::{ByteOrder, LittleEndian}; + + let mut naf = [0i8; 256]; + + let mut x_u64 = [0u64; 5]; + LittleEndian::read_u64_into(&self.to_bytes(), &mut x_u64[0..4]); + + let width = 1 << w; + let window_mask = width - 1; + + let mut pos = 0; + let mut carry = 0; + while pos < 256 { + // Construct a buffer of bits of the scalar, starting at bit `pos` + let u64_idx = pos / 64; + let bit_idx = pos % 64; + let bit_buf: u64; + if bit_idx < 64 - w { + // This window's bits are contained in a single u64 + bit_buf = x_u64[u64_idx] >> bit_idx; + } else { + // Combine the current u64's bits with the bits from the next u64 + bit_buf = (x_u64[u64_idx] >> bit_idx) | (x_u64[1 + u64_idx] << (64 - bit_idx)); + } + + // Add the carry into the current window + let window = carry + (bit_buf & window_mask); + + if window & 1 == 0 { + // If the window value is even, preserve the carry and continue. + // Why is the carry preserved? + // If carry == 0 and window & 1 == 0, then the next carry should be 0 + // If carry == 1 and window & 1 == 0, then bit_buf & 1 == 1 so the next carry should be 1 + pos += 1; + continue; + } + + if window < width / 2 { + carry = 0; + naf[pos] = window as i8; + } else { + carry = 1; + naf[pos] = (window as i8).wrapping_sub(width as i8); + } + + pos += w; + } + + naf + } +} + +/// Holds odd multiples 1A, 3A, ..., 15A of a point A. +#[derive(Copy, Clone)] +pub(crate) struct LookupTable5(pub(crate) [T; 8]); + +impl LookupTable5 { + /// Given public, odd \\( x \\) with \\( 0 < x < 2^4 \\), return \\(xA\\). + pub fn select(&self, x: usize) -> T { + debug_assert_eq!(x & 1, 1); + debug_assert!(x < 16); + + self.0[x / 2] + } +} + +impl Debug for LookupTable5 { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + write!(f, "LookupTable5({:?})", self.0) + } +} + +impl<'a> From<&'a pallas::Point> for LookupTable5 { + #[allow(non_snake_case)] + fn from(A: &'a pallas::Point) -> Self { + let mut Ai = [*A; 8]; + let A2 = A.double(); + for i in 0..7 { + Ai[i + 1] = A2 + Ai[i]; + } + // Now Ai = [A, 3A, 5A, 7A, 9A, 11A, 13A, 15A] + LookupTable5(Ai) + } +} + +impl VartimeMultiscalarMul for pallas::Point { + type Point = pallas::Point; + + /// Variable-time multiscalar multiplication using a non-adjacent form of + /// width (5). + /// + /// The non-adjacent form has signed, odd digits. Using only odd digits + /// halves the table size (since we only need odd multiples), or gives fewer + /// additions for the same table size. + /// + /// As the name implies, the runtime varies according to the values of the + /// inputs, thus is not safe for computing over secret data, but is great + /// for computing over public data, such as validating signatures. + #[allow(non_snake_case)] + #[allow(clippy::comparison_chain)] + fn optional_multiscalar_mul(scalars: I, points: J) -> Option + where + I: IntoIterator, + I::Item: Borrow, + J: IntoIterator>, + { + let nafs: Vec<_> = scalars + .into_iter() + .map(|c| c.borrow().non_adjacent_form(5)) + .collect(); + + let lookup_tables = points + .into_iter() + .map(|P_opt| P_opt.map(|P| LookupTable5::::from(&P))) + .collect::>>()?; + + let mut r = pallas::Point::identity(); + + for i in (0..256).rev() { + let mut t = r.double(); + + for (naf, lookup_table) in nafs.iter().zip(lookup_tables.iter()) { + if naf[i] > 0 { + t += lookup_table.select(naf[i] as usize); + } else if naf[i] < 0 { + t -= lookup_table.select(-naf[i] as usize); + } + } + + r = t; + } + + Some(r) + } +} diff --git a/zebra-chain/src/primitives/redpallas/tests.rs b/zebra-chain/src/primitives/redpallas/tests.rs index e234c5158..148b5aa3c 100644 --- a/zebra-chain/src/primitives/redpallas/tests.rs +++ b/zebra-chain/src/primitives/redpallas/tests.rs @@ -1,2 +1,3 @@ mod basepoints; +mod batch; mod prop; diff --git a/zebra-chain/src/primitives/redpallas/tests/batch.rs b/zebra-chain/src/primitives/redpallas/tests/batch.rs new file mode 100644 index 000000000..31c0d5870 --- /dev/null +++ b/zebra-chain/src/primitives/redpallas/tests/batch.rs @@ -0,0 +1,99 @@ +use rand::thread_rng; + +use super::super::*; + +#[test] +fn spendauth_batch_verify() { + let mut rng = thread_rng(); + let mut batch = batch::Verifier::new(); + for _ in 0..32 { + let sk = SigningKey::::new(&mut rng); + let vk = VerificationKey::from(&sk); + let msg = b"BatchVerifyTest"; + let sig = sk.sign(&mut rng, &msg[..]); + batch.queue((vk.into(), sig, msg)); + } + assert!(batch.verify(rng).is_ok()); +} + +#[test] +fn binding_batch_verify() { + let mut rng = thread_rng(); + let mut batch = batch::Verifier::new(); + for _ in 0..32 { + let sk = SigningKey::::new(&mut rng); + let vk = VerificationKey::from(&sk); + let msg = b"BatchVerifyTest"; + let sig = sk.sign(&mut rng, &msg[..]); + batch.queue((vk.into(), sig, msg)); + } + assert!(batch.verify(rng).is_ok()); +} + +#[test] +fn alternating_batch_verify() { + let mut rng = thread_rng(); + let mut batch = batch::Verifier::new(); + for i in 0..32 { + let item: batch::Item = match i % 2 { + 0 => { + let sk = SigningKey::::new(&mut rng); + let vk = VerificationKey::from(&sk); + let msg = b"BatchVerifyTest"; + let sig = sk.sign(&mut rng, &msg[..]); + (vk.into(), sig, msg).into() + } + 1 => { + let sk = SigningKey::::new(&mut rng); + let vk = VerificationKey::from(&sk); + let msg = b"BatchVerifyTest"; + let sig = sk.sign(&mut rng, &msg[..]); + (vk.into(), sig, msg).into() + } + _ => unreachable!(), + }; + batch.queue(item); + } + assert!(batch.verify(rng).is_ok()); +} + +#[test] +fn bad_batch_verify() { + let mut rng = thread_rng(); + let bad_index = 4; // must be even + let mut batch = batch::Verifier::new(); + let mut items = Vec::new(); + for i in 0..32 { + let item: batch::Item = match i % 2 { + 0 => { + let sk = SigningKey::::new(&mut rng); + let vk = VerificationKey::from(&sk); + let msg = b"BatchVerifyTest"; + let sig = if i != bad_index { + sk.sign(&mut rng, &msg[..]) + } else { + sk.sign(&mut rng, b"bad") + }; + (vk.into(), sig, msg).into() + } + 1 => { + let sk = SigningKey::::new(&mut rng); + let vk = VerificationKey::from(&sk); + let msg = b"BatchVerifyTest"; + let sig = sk.sign(&mut rng, &msg[..]); + (vk.into(), sig, msg).into() + } + _ => unreachable!(), + }; + items.push(item.clone()); + batch.queue(item); + } + assert!(batch.verify(rng).is_err()); + for (i, item) in items.drain(..).enumerate() { + if i != bad_index { + assert!(item.verify_single().is_ok()); + } else { + assert!(item.verify_single().is_err()); + } + } +}