|
1 | 1 | //! BLS12-381 threshold VRF implementation of the [`Scheme`] trait for `simplex`. |
2 | 2 | //! |
3 | | -//! Certificates contain a vote signature and a view signature (a seed that can be used |
| 3 | +//! Certificates contain a vote signature and a round signature (a seed that can be used |
4 | 4 | //! as a VRF). |
5 | 5 | //! |
6 | 6 | //! # Using the VRF |
@@ -314,7 +314,7 @@ where |
314 | 314 | pub struct Signature<V: Variant> { |
315 | 315 | /// Signature over the consensus vote message (partial or recovered aggregate). |
316 | 316 | pub vote_signature: V::Signature, |
317 | | - /// Signature over the per-view seed (partial or recovered aggregate). |
| 317 | + /// Signature over the per-round seed (partial or recovered aggregate). |
318 | 318 | pub seed_signature: V::Signature, |
319 | 319 | } |
320 | 320 |
|
@@ -833,18 +833,18 @@ impl<P: PublicKey, V: Variant> certificate::Scheme for Scheme<P, V> { |
833 | 833 | let vote_message = context.message(); |
834 | 834 | entries.push((vote_namespace, vote_message, cert.vote_signature)); |
835 | 835 |
|
836 | | - // Seed signatures are per-view, so multiple certificates for the same view |
| 836 | + // Seed signatures are per-round, so multiple certificates for the same round |
837 | 837 | // (e.g., notarization and finalization) share the same seed. We only include |
838 | 838 | // each unique seed once in the aggregate, but verify all certificates for a |
839 | | - // view have matching seeds. |
840 | | - if let Some(previous) = seeds.get(&context.view()) { |
| 839 | + // round have matching seeds. |
| 840 | + let seed_message = seed_message_from_subject(&context); |
| 841 | + if let Some(previous) = seeds.get(&seed_message) { |
841 | 842 | if *previous != cert.seed_signature { |
842 | 843 | return false; |
843 | 844 | } |
844 | 845 | } else { |
845 | | - let seed_message = seed_message_from_subject(&context); |
846 | | - entries.push((&namespace.seed, seed_message, cert.seed_signature)); |
847 | | - seeds.insert(context.view(), cert.seed_signature); |
| 846 | + entries.push((&namespace.seed, seed_message.clone(), cert.seed_signature)); |
| 847 | + seeds.insert(seed_message, cert.seed_signature); |
848 | 848 | } |
849 | 849 | } |
850 | 850 |
|
@@ -2027,6 +2027,154 @@ mod tests { |
2027 | 2027 | verify_certificates_rejects_malleability::<MinSig>(); |
2028 | 2028 | } |
2029 | 2029 |
|
| 2030 | + fn assemble_notarization_certificate<V: Variant>( |
| 2031 | + schemes: &[Scheme<V>], |
| 2032 | + proposal: &Proposal<Sha256Digest>, |
| 2033 | + ) -> Certificate<V> { |
| 2034 | + let quorum = N3f1::quorum(schemes.len()) as usize; |
| 2035 | + let votes: Vec<_> = schemes |
| 2036 | + .iter() |
| 2037 | + .take(quorum) |
| 2038 | + .map(|scheme| scheme.sign(Subject::Notarize { proposal }).unwrap()) |
| 2039 | + .collect(); |
| 2040 | + |
| 2041 | + schemes[0] |
| 2042 | + .assemble::<_, N3f1>(votes, &Sequential) |
| 2043 | + .expect("assemble notarization certificate") |
| 2044 | + } |
| 2045 | + |
| 2046 | + fn assemble_finalization_certificate<V: Variant>( |
| 2047 | + schemes: &[Scheme<V>], |
| 2048 | + proposal: &Proposal<Sha256Digest>, |
| 2049 | + ) -> Certificate<V> { |
| 2050 | + let quorum = N3f1::quorum(schemes.len()) as usize; |
| 2051 | + let votes: Vec<_> = schemes |
| 2052 | + .iter() |
| 2053 | + .skip(schemes.len() - quorum) |
| 2054 | + .map(|scheme| scheme.sign(Subject::Finalize { proposal }).unwrap()) |
| 2055 | + .collect(); |
| 2056 | + |
| 2057 | + schemes[0] |
| 2058 | + .assemble::<_, N3f1>(votes, &Sequential) |
| 2059 | + .expect("assemble finalization certificate") |
| 2060 | + } |
| 2061 | + |
| 2062 | + fn verify_certificates_accepts_shared_round_seed<V: Variant>() { |
| 2063 | + let mut rng = test_rng(); |
| 2064 | + let (schemes, verifier) = setup_signers::<V>(4, 81); |
| 2065 | + let proposal = sample_proposal(Epoch::new(1), View::new(35), 19); |
| 2066 | + let notarization_certificate = assemble_notarization_certificate(&schemes, &proposal); |
| 2067 | + let finalization_certificate = assemble_finalization_certificate(&schemes, &proposal); |
| 2068 | + |
| 2069 | + assert!(verifier.verify_certificates::<_, Sha256Digest, _, N3f1>( |
| 2070 | + &mut rng, |
| 2071 | + [ |
| 2072 | + ( |
| 2073 | + Subject::Notarize { |
| 2074 | + proposal: &proposal, |
| 2075 | + }, |
| 2076 | + ¬arization_certificate, |
| 2077 | + ), |
| 2078 | + ( |
| 2079 | + Subject::Finalize { |
| 2080 | + proposal: &proposal, |
| 2081 | + }, |
| 2082 | + &finalization_certificate, |
| 2083 | + ), |
| 2084 | + ] |
| 2085 | + .into_iter(), |
| 2086 | + &Sequential, |
| 2087 | + )); |
| 2088 | + } |
| 2089 | + |
| 2090 | + #[test] |
| 2091 | + fn test_verify_certificates_accepts_shared_round_seed() { |
| 2092 | + verify_certificates_accepts_shared_round_seed::<MinPk>(); |
| 2093 | + verify_certificates_accepts_shared_round_seed::<MinSig>(); |
| 2094 | + } |
| 2095 | + |
| 2096 | + fn verify_certificates_rejects_cross_epoch_seed_replay<V: Variant>() { |
| 2097 | + let mut rng = test_rng(); |
| 2098 | + let (schemes, verifier) = setup_signers::<V>(4, 83); |
| 2099 | + let view = View::new(35); |
| 2100 | + let proposal1 = sample_proposal(Epoch::new(1), view, 19); |
| 2101 | + let proposal2 = sample_proposal(Epoch::new(2), view, 20); |
| 2102 | + let certificate1 = assemble_notarization_certificate(&schemes, &proposal1); |
| 2103 | + let certificate2 = assemble_notarization_certificate(&schemes, &proposal2); |
| 2104 | + |
| 2105 | + assert!(verifier.verify_certificates::<_, Sha256Digest, _, N3f1>( |
| 2106 | + &mut rng, |
| 2107 | + [ |
| 2108 | + ( |
| 2109 | + Subject::Notarize { |
| 2110 | + proposal: &proposal1, |
| 2111 | + }, |
| 2112 | + &certificate1, |
| 2113 | + ), |
| 2114 | + ( |
| 2115 | + Subject::Notarize { |
| 2116 | + proposal: &proposal2, |
| 2117 | + }, |
| 2118 | + &certificate2, |
| 2119 | + ), |
| 2120 | + ] |
| 2121 | + .into_iter(), |
| 2122 | + &Sequential, |
| 2123 | + )); |
| 2124 | + |
| 2125 | + let cert1 = certificate1.get().unwrap(); |
| 2126 | + let cert2 = certificate2.get().unwrap(); |
| 2127 | + let forged_certificate2: Certificate<V> = Signature { |
| 2128 | + vote_signature: cert2.vote_signature, |
| 2129 | + seed_signature: cert1.seed_signature, |
| 2130 | + } |
| 2131 | + .into(); |
| 2132 | + |
| 2133 | + assert!(!verifier.verify_certificate::<_, Sha256Digest, N3f1>( |
| 2134 | + &mut rng, |
| 2135 | + Subject::Notarize { |
| 2136 | + proposal: &proposal2, |
| 2137 | + }, |
| 2138 | + &forged_certificate2, |
| 2139 | + &Sequential, |
| 2140 | + )); |
| 2141 | + |
| 2142 | + let batch = [ |
| 2143 | + ( |
| 2144 | + Subject::Notarize { |
| 2145 | + proposal: &proposal1, |
| 2146 | + }, |
| 2147 | + &certificate1, |
| 2148 | + ), |
| 2149 | + ( |
| 2150 | + Subject::Notarize { |
| 2151 | + proposal: &proposal2, |
| 2152 | + }, |
| 2153 | + &forged_certificate2, |
| 2154 | + ), |
| 2155 | + ]; |
| 2156 | + |
| 2157 | + assert!(!verifier.verify_certificates::<_, Sha256Digest, _, N3f1>( |
| 2158 | + &mut rng, |
| 2159 | + batch.iter().copied(), |
| 2160 | + &Sequential, |
| 2161 | + )); |
| 2162 | + assert_eq!( |
| 2163 | + verifier.verify_certificates_bisect::<_, Sha256Digest, N3f1>( |
| 2164 | + &mut rng, |
| 2165 | + &batch, |
| 2166 | + &Sequential, |
| 2167 | + ), |
| 2168 | + vec![true, false], |
| 2169 | + ); |
| 2170 | + } |
| 2171 | + |
| 2172 | + #[test] |
| 2173 | + fn test_verify_certificates_rejects_cross_epoch_seed_replay() { |
| 2174 | + verify_certificates_rejects_cross_epoch_seed_replay::<MinPk>(); |
| 2175 | + verify_certificates_rejects_cross_epoch_seed_replay::<MinSig>(); |
| 2176 | + } |
| 2177 | + |
2030 | 2178 | #[cfg(feature = "arbitrary")] |
2031 | 2179 | mod conformance { |
2032 | 2180 | use super::*; |
|
0 commit comments