Skip to content

Commit a86158f

Browse files
Batch verification with blame (#66)
When batch verification fails, it may be useful to know which proofs are invalid. Unfortunately, `Proof::verify_batch` cannot identify this. The optimal method for this depends on the caller's needs. If the caller only needs to identify one invalid proof in a failed batch, it's more efficient to use a binary search to identify it. But if the caller needs to identify all invalid proofs, we need to check them all individually. This PR adds both of these. The new `Proof::verify_batch_with_single_blame` uses a binary search on batch failure, and returns an error containing the index of an invalid proof. The new `Proof::verify_batch_with_all_blame` iteratively checks all proofs on batch failure, and returns an error containing the indexes of all invalid proofs. BREAKING CHANGE: Empty batches are now considered valid by definition.
1 parent 49dc76d commit a86158f

File tree

1 file changed

+239
-11
lines changed

1 file changed

+239
-11
lines changed

src/proof.rs

Lines changed: 239 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,24 @@ pub enum ProofError {
5959
/// Proof deserialization failed.
6060
#[snafu(display("Proof deserialization failed"))]
6161
FailedDeserialization,
62-
/// Proof verification failed.
63-
#[snafu[display("Proof verification failed")]]
62+
/// Single proof verification failed.
63+
#[snafu[display("Single proof verification failed")]]
6464
FailedVerification,
65+
/// Batch proof verification failed.
66+
#[snafu[display("Batch proof verification failed")]]
67+
FailedBatchVerification,
68+
/// Batch proof verification failed.
69+
#[snafu[display("Batch proof verification failed")]]
70+
FailedBatchVerificationWithSingleBlame {
71+
/// The index of a failed proof, or `None` if no such index was found due to an internal error.
72+
index: Option<usize>,
73+
},
74+
/// Batch proof verification failed.
75+
#[snafu[display("Batch proof verification failed")]]
76+
FailedBatchVerificationWithFullBlame {
77+
/// The indexes of all failed proofs.
78+
indexes: Vec<usize>,
79+
},
6580
}
6681

6782
impl Proof {
@@ -369,14 +384,131 @@ impl Proof {
369384
)
370385
}
371386

387+
/// Verify a batch of Triptych [`Proofs`](`Proof`), identifying a single invalid proof if verification fails.
388+
///
389+
/// An empty batch is valid by definition.
390+
///
391+
/// If verification fails, this performs a subsequent number of verifications logarithmic in the size of the batch.
392+
///
393+
/// Verification requires that the `statements` and `transcripts` match those used when the `proofs` were generated,
394+
/// and that they share a common [`InputSet`](`crate::statement::InputSet`) and
395+
/// [`Parameters`](`crate::parameters::Parameters`).
396+
///
397+
/// If any of the above requirements are not met, returns a [`ProofError`].
398+
/// If any batch in the proof is invalid, returns a [`ProofError`] containing the index of an invalid proof.
399+
/// It is not guaranteed that this index represents the _only_ invalid proof in the batch.
400+
pub fn verify_batch_with_single_blame(
401+
statements: &[Statement],
402+
proofs: &[Proof],
403+
transcripts: &mut [Transcript],
404+
) -> Result<(), ProofError> {
405+
// Try to verify the full batch
406+
if Self::verify_batch(statements, proofs, &mut transcripts.to_vec()).is_ok() {
407+
return Ok(());
408+
}
409+
410+
// The batch failed, so find an invalid proof using a binary search
411+
let mut left = 0;
412+
let mut right = proofs.len();
413+
414+
while left < right {
415+
#[allow(clippy::arithmetic_side_effects)]
416+
let average = left
417+
.checked_add(
418+
// This cannot underflow since `left < right`
419+
(right - left) / 2,
420+
)
421+
.ok_or(ProofError::FailedBatchVerificationWithSingleBlame { index: None })?;
422+
423+
#[allow(clippy::arithmetic_side_effects)]
424+
// This cannot underflow since `left < right`
425+
let mid = if (right - left) % 2 == 0 {
426+
average
427+
} else {
428+
average
429+
.checked_add(1)
430+
.ok_or(ProofError::FailedBatchVerificationWithSingleBlame { index: None })?
431+
};
432+
433+
let failure_on_left = Self::verify_batch(
434+
&statements[left..mid],
435+
&proofs[left..mid],
436+
&mut transcripts.to_vec()[left..mid],
437+
)
438+
.is_err();
439+
440+
if failure_on_left {
441+
let left_check = mid
442+
.checked_sub(1)
443+
.ok_or(ProofError::FailedBatchVerificationWithSingleBlame { index: None })?;
444+
if left == left_check {
445+
return Err(ProofError::FailedBatchVerificationWithSingleBlame { index: Some(left) });
446+
}
447+
448+
right = mid;
449+
} else {
450+
let right_check = mid
451+
.checked_add(1)
452+
.ok_or(ProofError::FailedBatchVerificationWithSingleBlame { index: None })?;
453+
if right == right_check {
454+
let right_result = right
455+
.checked_sub(1)
456+
.ok_or(ProofError::FailedBatchVerificationWithSingleBlame { index: None })?;
457+
return Err(ProofError::FailedBatchVerificationWithSingleBlame {
458+
index: Some(right_result),
459+
});
460+
}
461+
462+
left = mid
463+
}
464+
}
465+
466+
// The batch failed, but we couldn't find a single failure! This should never happen.
467+
Err(ProofError::FailedBatchVerificationWithSingleBlame { index: None })
468+
}
469+
470+
/// Verify a batch of Triptych [`Proofs`](`Proof`), identifying all invalid proofs if verification fails.
471+
///
472+
/// An empty batch is valid by definition.
473+
///
474+
/// If verification fails, this performs a subsequent number of verifications linear in the size of the batch.
475+
///
476+
/// Verification requires that the `statements` and `transcripts` match those used when the `proofs` were generated,
477+
/// and that they share a common [`InputSet`](`crate::statement::InputSet`) and
478+
/// [`Parameters`](`crate::parameters::Parameters`).
479+
///
480+
/// If any of the above requirements are not met, returns a [`ProofError`].
481+
/// If any batch in the proof is invalid, returns a [`ProofError`] containing the indexes of all invalid proofs.
482+
pub fn verify_batch_with_full_blame(
483+
statements: &[Statement],
484+
proofs: &[Proof],
485+
transcripts: &mut [Transcript],
486+
) -> Result<(), ProofError> {
487+
// Try to verify the full batch
488+
if Self::verify_batch(statements, proofs, &mut transcripts.to_vec()).is_ok() {
489+
return Ok(());
490+
}
491+
492+
// The batch failed, so check each proof and keep track of which are invalid
493+
let mut failures = Vec::with_capacity(proofs.len());
494+
for (index, (statement, proof, transcript)) in izip!(statements, proofs, transcripts.iter_mut()).enumerate() {
495+
if proof.verify(statement, transcript).is_err() {
496+
failures.push(index);
497+
}
498+
}
499+
500+
Err(ProofError::FailedBatchVerificationWithFullBlame { indexes: failures })
501+
}
502+
372503
/// Verify a batch of Triptych [`Proofs`](`Proof`).
373504
///
505+
/// An empty batch is valid by definition.
506+
///
374507
/// Verification requires that the `statements` and `transcripts` match those used when the `proofs` were generated,
375508
/// and that they share a common [`InputSet`](`crate::statement::InputSet`) and
376509
/// [`Parameters`](`crate::parameters::Parameters`).
377510
///
378-
/// If any of the above requirements are not met, or if the batch is empty, or if any proof is invalid, returns a
379-
/// [`ProofError`].
511+
/// If any of the above requirements are not met, or if any proof is invalid, returns a [`ProofError`].
380512
#[allow(clippy::too_many_lines, non_snake_case)]
381513
pub fn verify_batch(
382514
statements: &[Statement],
@@ -391,8 +523,11 @@ impl Proof {
391523
return Err(ProofError::InvalidParameter);
392524
}
393525

394-
// An empty batch is considered trivially invalid
395-
let first_statement = statements.first().ok_or(ProofError::InvalidParameter)?;
526+
// An empty batch is considered trivially valid
527+
let first_statement = match statements.first() {
528+
Some(statement) => statement,
529+
None => return Ok(()),
530+
};
396531

397532
// Each statement must use the same input set (checked using the hash for efficiency)
398533
if !statements
@@ -799,7 +934,7 @@ mod test {
799934

800935
use crate::{
801936
parameters::Parameters,
802-
proof::Proof,
937+
proof::{Proof, ProofError},
803938
statement::{InputSet, Statement},
804939
witness::Witness,
805940
};
@@ -948,17 +1083,110 @@ mod test {
9481083
let mut rng = ChaCha12Rng::seed_from_u64(8675309);
9491084
let (witnesses, statements, mut transcripts) = generate_data(n, m, batch, &mut rng);
9501085

951-
// Generate the proofs and verify as a batch
1086+
// Generate the proofs
9521087
let proofs = izip!(witnesses.iter(), statements.iter(), transcripts.clone().iter_mut())
9531088
.map(|(w, s, t)| Proof::prove_with_rng_vartime(w, s, &mut rng, t).unwrap())
9541089
.collect::<Vec<Proof>>();
955-
assert!(Proof::verify_batch(&statements, &proofs, &mut transcripts).is_ok());
1090+
1091+
// Verify the batch with and without blame
1092+
assert!(Proof::verify_batch(&statements, &proofs, &mut transcripts.clone()).is_ok());
1093+
assert!(Proof::verify_batch_with_single_blame(&statements, &proofs, &mut transcripts.clone()).is_ok());
1094+
assert!(Proof::verify_batch_with_full_blame(&statements, &proofs, &mut transcripts).is_ok());
9561095
}
9571096

9581097
#[test]
9591098
fn test_prove_verify_empty_batch() {
960-
// An empty batch is invalid by definition
961-
assert!(Proof::verify_batch(&[], &[], &mut []).is_err());
1099+
// An empty batch is valid by definition
1100+
assert!(Proof::verify_batch(&[], &[], &mut []).is_ok());
1101+
assert!(Proof::verify_batch_with_single_blame(&[], &[], &mut []).is_ok());
1102+
assert!(Proof::verify_batch_with_full_blame(&[], &[], &mut []).is_ok());
1103+
}
1104+
1105+
#[test]
1106+
#[allow(non_snake_case, non_upper_case_globals)]
1107+
fn test_prove_verify_invalid_batch() {
1108+
// Generate data
1109+
const n: u32 = 2;
1110+
const m: u32 = 4;
1111+
const batch: usize = 3; // batch size
1112+
let mut rng = ChaCha12Rng::seed_from_u64(8675309);
1113+
let (witnesses, statements, mut transcripts) = generate_data(n, m, batch, &mut rng);
1114+
1115+
// Generate the proofs
1116+
let proofs = izip!(witnesses.iter(), statements.iter(), transcripts.clone().iter_mut())
1117+
.map(|(w, s, t)| Proof::prove_with_rng_vartime(w, s, &mut rng, t).unwrap())
1118+
.collect::<Vec<Proof>>();
1119+
1120+
// Manipulate a transcript so the corresponding proof is invalid
1121+
transcripts[0] = Transcript::new(b"Evil transcript");
1122+
1123+
// Verification should fail
1124+
assert!(Proof::verify_batch(&statements, &proofs, &mut transcripts).is_err());
1125+
}
1126+
1127+
#[test]
1128+
#[allow(non_snake_case, non_upper_case_globals)]
1129+
fn test_prove_verify_invalid_batch_single_blame() {
1130+
// Generate data
1131+
const n: u32 = 2;
1132+
const m: u32 = 4;
1133+
1134+
// Test against batches of even and odd size
1135+
for batch in [4, 5] {
1136+
let mut rng = ChaCha12Rng::seed_from_u64(8675309);
1137+
let (witnesses, statements, transcripts) = generate_data(n, m, batch, &mut rng);
1138+
1139+
// Generate the proofs
1140+
let proofs = izip!(witnesses.iter(), statements.iter(), transcripts.clone().iter_mut())
1141+
.map(|(w, s, t)| Proof::prove_with_rng_vartime(w, s, &mut rng, t).unwrap())
1142+
.collect::<Vec<Proof>>();
1143+
1144+
// Iteratively manipulate each transcript to make the corresponding proof invalid
1145+
for i in 0..proofs.len() {
1146+
let mut evil_transcripts = transcripts.clone();
1147+
evil_transcripts[i] = Transcript::new(b"Evil transcript");
1148+
1149+
// Verification should fail and blame the correct proof
1150+
let error =
1151+
Proof::verify_batch_with_single_blame(&statements, &proofs, &mut evil_transcripts).unwrap_err();
1152+
if let ProofError::FailedBatchVerificationWithSingleBlame { index: Some(index) } = error {
1153+
assert_eq!(index, i);
1154+
} else {
1155+
panic!();
1156+
}
1157+
}
1158+
}
1159+
}
1160+
1161+
#[test]
1162+
#[allow(non_snake_case, non_upper_case_globals)]
1163+
fn test_prove_verify_invalid_batch_full_blame() {
1164+
// Generate data
1165+
const n: u32 = 2;
1166+
const m: u32 = 4;
1167+
const batch: usize = 4;
1168+
const failures: [usize; 2] = [1, 3];
1169+
1170+
let mut rng = ChaCha12Rng::seed_from_u64(8675309);
1171+
let (witnesses, statements, mut transcripts) = generate_data(n, m, batch, &mut rng);
1172+
1173+
// Generate the proofs
1174+
let proofs = izip!(witnesses.iter(), statements.iter(), transcripts.clone().iter_mut())
1175+
.map(|(w, s, t)| Proof::prove_with_rng_vartime(w, s, &mut rng, t).unwrap())
1176+
.collect::<Vec<Proof>>();
1177+
1178+
// Manipulate some of the transcripts to make the corresponding proofs invalid
1179+
for i in failures {
1180+
transcripts[i] = Transcript::new(b"Evil transcript");
1181+
}
1182+
1183+
// Verification should fail and blame the correct proof
1184+
let error = Proof::verify_batch_with_full_blame(&statements, &proofs, &mut transcripts).unwrap_err();
1185+
if let ProofError::FailedBatchVerificationWithFullBlame { indexes } = error {
1186+
assert_eq!(indexes, failures);
1187+
} else {
1188+
panic!();
1189+
}
9621190
}
9631191

9641192
#[test]

0 commit comments

Comments
 (0)