Skip to content

Commit be11437

Browse files
committed
Batch BLS verification for attestations (#2399)
## Issue Addressed NA ## Proposed Changes Adds the ability to verify batches of aggregated/unaggregated attestations from the network. When the `BeaconProcessor` finds there are messages in the aggregated or unaggregated attestation queues, it will first check the length of the queue: - `== 1` verify the attestation individually. - `>= 2` take up to 64 of those attestations and verify them in a batch. Notably, we only perform batch verification if the queue has a backlog. We don't apply any artificial delays to attestations to try and force them into batches. ### Batching Details To assist with implementing batches we modify `beacon_chain::attestation_verification` to have two distinct categories for attestations: - *Indexed* attestations: those which have passed initial validation and were valid enough for us to derive an `IndexedAttestation`. - *Verified* attestations: those attestations which were indexed *and also* passed signature verification. These are well-formed, interesting messages which were signed by validators. The batching functions accept `n` attestations and then return `n` attestation verification `Result`s, where those `Result`s can be any combination of `Ok` or `Err`. In other words, we attempt to verify as many attestations as possible and return specific per-attestation results so peer scores can be updated, if required. When we batch verify attestations, we first try to map all those attestations to *indexed* attestations. If any of those attestations were able to be indexed, we then perform batch BLS verification on those indexed attestations. If the batch verification succeeds, we convert them into *verified* attestations, disabling individual signature checking. If the batch fails, we convert to verified attestations with individual signature checking enabled. Ultimately, we optimistically try to do a batch verification of attestation signatures and fall-back to individual verification if it fails. This opens an attach vector for "poisoning" the attestations and causing us to waste a batch verification. I argue that peer scoring should do a good-enough job of defending against this and the typical-case gains massively outweigh the worst-case losses. ## Additional Info Before this PR, attestation verification took the attestations by value (instead of by reference). It turns out that this was unnecessary and, in my opinion, resulted in some undesirable ergonomics (e.g., we had to pass the attestation back in the `Err` variant to avoid clones). In this PR I've modified attestation verification so that it now takes a reference. I refactored the `beacon_chain/tests/attestation_verification.rs` tests so they use a builder-esque "tester" struct instead of a weird macro. It made it easier for me to test individual/batch with the same set of tests and I think it was a nice tidy-up. Notably, I did this last to try and make sure my new refactors to *actual* production code would pass under the existing test suite.
1 parent 9667dc2 commit be11437

File tree

13 files changed

+1940
-1015
lines changed

13 files changed

+1940
-1015
lines changed

beacon_node/beacon_chain/src/attestation_verification.rs

Lines changed: 291 additions & 152 deletions
Large diffs are not rendered by default.
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
//! These two `batch_...` functions provide verification of batches of attestations. They provide
2+
//! significant CPU-time savings by performing batch verification of BLS signatures.
3+
//!
4+
//! In each function, attestations are "indexed" (i.e., the `IndexedAttestation` is computed), to
5+
//! determine if they should progress to signature verification. Then, all attestations which were
6+
//! successfully indexed have their signatures verified in a batch. If that signature batch fails
7+
//! then all attestation signatures are verified independently.
8+
//!
9+
//! The outcome of each function is a `Vec<Result>` with a one-to-one mapping to the attestations
10+
//! supplied as input. Each result provides the exact success or failure result of the corresponding
11+
//! attestation, with no loss of fidelity when compared to individual verification.
12+
use super::{
13+
CheckAttestationSignature, Error, IndexedAggregatedAttestation, IndexedUnaggregatedAttestation,
14+
VerifiedAggregatedAttestation, VerifiedUnaggregatedAttestation,
15+
};
16+
use crate::{
17+
beacon_chain::VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT, metrics, BeaconChain, BeaconChainError,
18+
BeaconChainTypes,
19+
};
20+
use bls::verify_signature_sets;
21+
use state_processing::signature_sets::{
22+
indexed_attestation_signature_set_from_pubkeys, signed_aggregate_selection_proof_signature_set,
23+
signed_aggregate_signature_set,
24+
};
25+
use std::borrow::Cow;
26+
use types::*;
27+
28+
/// Verify aggregated attestations using batch BLS signature verification.
29+
///
30+
/// See module-level docs for more info.
31+
pub fn batch_verify_aggregated_attestations<'a, T, I>(
32+
aggregates: I,
33+
chain: &BeaconChain<T>,
34+
) -> Result<Vec<Result<VerifiedAggregatedAttestation<'a, T>, Error>>, Error>
35+
where
36+
T: BeaconChainTypes,
37+
I: Iterator<Item = &'a SignedAggregateAndProof<T::EthSpec>> + ExactSizeIterator,
38+
{
39+
let mut num_indexed = 0;
40+
let mut num_failed = 0;
41+
42+
// Perform indexing of all attestations, collecting the results.
43+
let indexing_results = aggregates
44+
.map(|aggregate| {
45+
let result = IndexedAggregatedAttestation::verify(aggregate, chain);
46+
if result.is_ok() {
47+
num_indexed += 1;
48+
} else {
49+
num_failed += 1;
50+
}
51+
result
52+
})
53+
.collect::<Vec<_>>();
54+
55+
// May be set to `No` if batch verification succeeds.
56+
let mut check_signatures = CheckAttestationSignature::Yes;
57+
58+
// Perform batch BLS verification, if any attestation signatures are worth checking.
59+
if num_indexed > 0 {
60+
let signature_setup_timer =
61+
metrics::start_timer(&metrics::ATTESTATION_PROCESSING_BATCH_AGG_SIGNATURE_SETUP_TIMES);
62+
63+
let pubkey_cache = chain
64+
.validator_pubkey_cache
65+
.try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT)
66+
.ok_or(BeaconChainError::ValidatorPubkeyCacheLockTimeout)?;
67+
68+
let fork = chain.with_head(|head| Ok::<_, BeaconChainError>(head.beacon_state.fork()))?;
69+
70+
let mut signature_sets = Vec::with_capacity(num_indexed * 3);
71+
72+
// Iterate, flattening to get only the `Ok` values.
73+
for indexed in indexing_results.iter().flatten() {
74+
let signed_aggregate = &indexed.signed_aggregate;
75+
let indexed_attestation = &indexed.indexed_attestation;
76+
77+
signature_sets.push(
78+
signed_aggregate_selection_proof_signature_set(
79+
|validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed),
80+
signed_aggregate,
81+
&fork,
82+
chain.genesis_validators_root,
83+
&chain.spec,
84+
)
85+
.map_err(BeaconChainError::SignatureSetError)?,
86+
);
87+
signature_sets.push(
88+
signed_aggregate_signature_set(
89+
|validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed),
90+
signed_aggregate,
91+
&fork,
92+
chain.genesis_validators_root,
93+
&chain.spec,
94+
)
95+
.map_err(BeaconChainError::SignatureSetError)?,
96+
);
97+
signature_sets.push(
98+
indexed_attestation_signature_set_from_pubkeys(
99+
|validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed),
100+
&indexed_attestation.signature,
101+
indexed_attestation,
102+
&fork,
103+
chain.genesis_validators_root,
104+
&chain.spec,
105+
)
106+
.map_err(BeaconChainError::SignatureSetError)?,
107+
);
108+
}
109+
110+
metrics::stop_timer(signature_setup_timer);
111+
112+
let _signature_verification_timer =
113+
metrics::start_timer(&metrics::ATTESTATION_PROCESSING_BATCH_AGG_SIGNATURE_TIMES);
114+
115+
if verify_signature_sets(signature_sets.iter()) {
116+
// Since all the signatures verified in a batch, there's no reason for them to be
117+
// checked again later.
118+
check_signatures = CheckAttestationSignature::No
119+
}
120+
}
121+
122+
// Complete the attestation verification, potentially verifying all signatures independently.
123+
let final_results = indexing_results
124+
.into_iter()
125+
.map(|result| match result {
126+
Ok(indexed) => {
127+
VerifiedAggregatedAttestation::from_indexed(indexed, chain, check_signatures)
128+
}
129+
Err(e) => Err(e),
130+
})
131+
.collect();
132+
133+
Ok(final_results)
134+
}
135+
136+
/// Verify unaggregated attestations using batch BLS signature verification.
137+
///
138+
/// See module-level docs for more info.
139+
pub fn batch_verify_unaggregated_attestations<'a, T, I>(
140+
attestations: I,
141+
chain: &BeaconChain<T>,
142+
) -> Result<Vec<Result<VerifiedUnaggregatedAttestation<'a, T>, Error>>, Error>
143+
where
144+
T: BeaconChainTypes,
145+
I: Iterator<Item = (&'a Attestation<T::EthSpec>, Option<SubnetId>)> + ExactSizeIterator,
146+
{
147+
let mut num_partially_verified = 0;
148+
let mut num_failed = 0;
149+
150+
// Perform partial verification of all attestations, collecting the results.
151+
let partial_results = attestations
152+
.map(|(attn, subnet_opt)| {
153+
let result = IndexedUnaggregatedAttestation::verify(attn, subnet_opt, chain);
154+
if result.is_ok() {
155+
num_partially_verified += 1;
156+
} else {
157+
num_failed += 1;
158+
}
159+
result
160+
})
161+
.collect::<Vec<_>>();
162+
163+
// May be set to `No` if batch verification succeeds.
164+
let mut check_signatures = CheckAttestationSignature::Yes;
165+
166+
// Perform batch BLS verification, if any attestation signatures are worth checking.
167+
if num_partially_verified > 0 {
168+
let signature_setup_timer = metrics::start_timer(
169+
&metrics::ATTESTATION_PROCESSING_BATCH_UNAGG_SIGNATURE_SETUP_TIMES,
170+
);
171+
172+
let pubkey_cache = chain
173+
.validator_pubkey_cache
174+
.try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT)
175+
.ok_or(BeaconChainError::ValidatorPubkeyCacheLockTimeout)?;
176+
177+
let fork = chain.with_head(|head| Ok::<_, BeaconChainError>(head.beacon_state.fork()))?;
178+
179+
let mut signature_sets = Vec::with_capacity(num_partially_verified);
180+
181+
// Iterate, flattening to get only the `Ok` values.
182+
for partially_verified in partial_results.iter().flatten() {
183+
let indexed_attestation = &partially_verified.indexed_attestation;
184+
185+
let signature_set = indexed_attestation_signature_set_from_pubkeys(
186+
|validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed),
187+
&indexed_attestation.signature,
188+
indexed_attestation,
189+
&fork,
190+
chain.genesis_validators_root,
191+
&chain.spec,
192+
)
193+
.map_err(BeaconChainError::SignatureSetError)?;
194+
195+
signature_sets.push(signature_set);
196+
}
197+
198+
metrics::stop_timer(signature_setup_timer);
199+
200+
let _signature_verification_timer =
201+
metrics::start_timer(&metrics::ATTESTATION_PROCESSING_BATCH_UNAGG_SIGNATURE_TIMES);
202+
203+
if verify_signature_sets(signature_sets.iter()) {
204+
// Since all the signatures verified in a batch, there's no reason for them to be
205+
// checked again later.
206+
check_signatures = CheckAttestationSignature::No
207+
}
208+
}
209+
210+
// Complete the attestation verification, potentially verifying all signatures independently.
211+
let final_results = partial_results
212+
.into_iter()
213+
.map(|result| match result {
214+
Ok(partial) => {
215+
VerifiedUnaggregatedAttestation::from_indexed(partial, chain, check_signatures)
216+
}
217+
Err(e) => Err(e),
218+
})
219+
.collect();
220+
221+
Ok(final_results)
222+
}

beacon_node/beacon_chain/src/beacon_chain.rs

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::attestation_verification::{
2-
Error as AttestationError, SignatureVerifiedAttestation, VerifiedAggregatedAttestation,
2+
batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations,
3+
Error as AttestationError, VerifiedAggregatedAttestation, VerifiedAttestation,
34
VerifiedUnaggregatedAttestation,
45
};
56
use crate::attester_cache::{AttesterCache, AttesterCacheKey};
@@ -1510,17 +1511,32 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
15101511
})
15111512
}
15121513

1514+
/// Performs the same validation as `Self::verify_unaggregated_attestation_for_gossip`, but for
1515+
/// multiple attestations using batch BLS verification. Batch verification can provide
1516+
/// significant CPU-time savings compared to individual verification.
1517+
pub fn batch_verify_unaggregated_attestations_for_gossip<'a, I>(
1518+
&self,
1519+
attestations: I,
1520+
) -> Result<
1521+
Vec<Result<VerifiedUnaggregatedAttestation<'a, T>, AttestationError>>,
1522+
AttestationError,
1523+
>
1524+
where
1525+
I: Iterator<Item = (&'a Attestation<T::EthSpec>, Option<SubnetId>)> + ExactSizeIterator,
1526+
{
1527+
batch_verify_unaggregated_attestations(attestations, self)
1528+
}
1529+
15131530
/// Accepts some `Attestation` from the network and attempts to verify it, returning `Ok(_)` if
15141531
/// it is valid to be (re)broadcast on the gossip network.
15151532
///
15161533
/// The attestation must be "unaggregated", that is it must have exactly one
15171534
/// aggregation bit set.
1518-
pub fn verify_unaggregated_attestation_for_gossip(
1535+
pub fn verify_unaggregated_attestation_for_gossip<'a>(
15191536
&self,
1520-
unaggregated_attestation: Attestation<T::EthSpec>,
1537+
unaggregated_attestation: &'a Attestation<T::EthSpec>,
15211538
subnet_id: Option<SubnetId>,
1522-
) -> Result<VerifiedUnaggregatedAttestation<T>, (AttestationError, Attestation<T::EthSpec>)>
1523-
{
1539+
) -> Result<VerifiedUnaggregatedAttestation<'a, T>, AttestationError> {
15241540
metrics::inc_counter(&metrics::UNAGGREGATED_ATTESTATION_PROCESSING_REQUESTS);
15251541
let _timer =
15261542
metrics::start_timer(&metrics::UNAGGREGATED_ATTESTATION_GOSSIP_VERIFICATION_TIMES);
@@ -1539,15 +1555,25 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
15391555
)
15401556
}
15411557

1558+
/// Performs the same validation as `Self::verify_aggregated_attestation_for_gossip`, but for
1559+
/// multiple attestations using batch BLS verification. Batch verification can provide
1560+
/// significant CPU-time savings compared to individual verification.
1561+
pub fn batch_verify_aggregated_attestations_for_gossip<'a, I>(
1562+
&self,
1563+
aggregates: I,
1564+
) -> Result<Vec<Result<VerifiedAggregatedAttestation<'a, T>, AttestationError>>, AttestationError>
1565+
where
1566+
I: Iterator<Item = &'a SignedAggregateAndProof<T::EthSpec>> + ExactSizeIterator,
1567+
{
1568+
batch_verify_aggregated_attestations(aggregates, self)
1569+
}
1570+
15421571
/// Accepts some `SignedAggregateAndProof` from the network and attempts to verify it,
15431572
/// returning `Ok(_)` if it is valid to be (re)broadcast on the gossip network.
1544-
pub fn verify_aggregated_attestation_for_gossip(
1573+
pub fn verify_aggregated_attestation_for_gossip<'a>(
15451574
&self,
1546-
signed_aggregate: SignedAggregateAndProof<T::EthSpec>,
1547-
) -> Result<
1548-
VerifiedAggregatedAttestation<T>,
1549-
(AttestationError, SignedAggregateAndProof<T::EthSpec>),
1550-
> {
1575+
signed_aggregate: &'a SignedAggregateAndProof<T::EthSpec>,
1576+
) -> Result<VerifiedAggregatedAttestation<'a, T>, AttestationError> {
15511577
metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_PROCESSING_REQUESTS);
15521578
let _timer =
15531579
metrics::start_timer(&metrics::AGGREGATED_ATTESTATION_GOSSIP_VERIFICATION_TIMES);
@@ -1597,13 +1623,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
15971623
/// Accepts some attestation-type object and attempts to verify it in the context of fork
15981624
/// choice. If it is valid it is applied to `self.fork_choice`.
15991625
///
1600-
/// Common items that implement `SignatureVerifiedAttestation`:
1626+
/// Common items that implement `VerifiedAttestation`:
16011627
///
16021628
/// - `VerifiedUnaggregatedAttestation`
16031629
/// - `VerifiedAggregatedAttestation`
16041630
pub fn apply_attestation_to_fork_choice(
16051631
&self,
1606-
verified: &impl SignatureVerifiedAttestation<T>,
1632+
verified: &impl VerifiedAttestation<T>,
16071633
) -> Result<(), Error> {
16081634
let _timer = metrics::start_timer(&metrics::FORK_CHOICE_PROCESS_ATTESTATION_TIMES);
16091635

@@ -1623,8 +1649,8 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
16231649
/// and no error is returned.
16241650
pub fn add_to_naive_aggregation_pool(
16251651
&self,
1626-
unaggregated_attestation: VerifiedUnaggregatedAttestation<T>,
1627-
) -> Result<VerifiedUnaggregatedAttestation<T>, AttestationError> {
1652+
unaggregated_attestation: &impl VerifiedAttestation<T>,
1653+
) -> Result<(), AttestationError> {
16281654
let _timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_APPLY_TO_AGG_POOL);
16291655

16301656
let attestation = unaggregated_attestation.attestation();
@@ -1660,7 +1686,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
16601686
}
16611687
};
16621688

1663-
Ok(unaggregated_attestation)
1689+
Ok(())
16641690
}
16651691

16661692
/// Accepts a `VerifiedSyncCommitteeMessage` and attempts to apply it to the "naive
@@ -1727,13 +1753,13 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
17271753
Ok(verified_sync_committee_message)
17281754
}
17291755

1730-
/// Accepts a `VerifiedAggregatedAttestation` and attempts to apply it to `self.op_pool`.
1756+
/// Accepts a `VerifiedAttestation` and attempts to apply it to `self.op_pool`.
17311757
///
17321758
/// The op pool is used by local block producers to pack blocks with operations.
17331759
pub fn add_to_block_inclusion_pool(
17341760
&self,
1735-
signed_aggregate: VerifiedAggregatedAttestation<T>,
1736-
) -> Result<VerifiedAggregatedAttestation<T>, AttestationError> {
1761+
verified_attestation: &impl VerifiedAttestation<T>,
1762+
) -> Result<(), AttestationError> {
17371763
let _timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_APPLY_TO_OP_POOL);
17381764

17391765
// If there's no eth1 chain then it's impossible to produce blocks and therefore
@@ -1745,15 +1771,15 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
17451771
self.op_pool
17461772
.insert_attestation(
17471773
// TODO: address this clone.
1748-
signed_aggregate.attestation().clone(),
1774+
verified_attestation.attestation().clone(),
17491775
&fork,
17501776
self.genesis_validators_root,
17511777
&self.spec,
17521778
)
17531779
.map_err(Error::from)?;
17541780
}
17551781

1756-
Ok(signed_aggregate)
1782+
Ok(())
17571783
}
17581784

17591785
/// Accepts a `VerifiedSyncContribution` and attempts to apply it to `self.op_pool`.

beacon_node/beacon_chain/src/metrics.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,26 @@ lazy_static! {
199199
"Time spent on the signature verification of attestation processing"
200200
);
201201

202+
/*
203+
* Batch Attestation Processing
204+
*/
205+
pub static ref ATTESTATION_PROCESSING_BATCH_AGG_SIGNATURE_SETUP_TIMES: Result<Histogram> = try_create_histogram(
206+
"beacon_attestation_processing_batch_agg_signature_setup_times",
207+
"Time spent on setting up for the signature verification of batch aggregate processing"
208+
);
209+
pub static ref ATTESTATION_PROCESSING_BATCH_AGG_SIGNATURE_TIMES: Result<Histogram> = try_create_histogram(
210+
"beacon_attestation_processing_batch_agg_signature_times",
211+
"Time spent on the signature verification of batch aggregate attestation processing"
212+
);
213+
pub static ref ATTESTATION_PROCESSING_BATCH_UNAGG_SIGNATURE_SETUP_TIMES: Result<Histogram> = try_create_histogram(
214+
"beacon_attestation_processing_batch_unagg_signature_setup_times",
215+
"Time spent on setting up for the signature verification of batch unaggregate processing"
216+
);
217+
pub static ref ATTESTATION_PROCESSING_BATCH_UNAGG_SIGNATURE_TIMES: Result<Histogram> = try_create_histogram(
218+
"beacon_attestation_processing_batch_unagg_signature_times",
219+
"Time spent on the signature verification of batch unaggregate attestation processing"
220+
);
221+
202222
/*
203223
* Shuffling cache
204224
*/

0 commit comments

Comments
 (0)