-
-
Notifications
You must be signed in to change notification settings - Fork 282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement support for validator next-epoch proposer duties #3782
Changes from all commits
9a0a792
5a1426a
bcdfbb6
4846918
18cde25
9bee893
5a632b1
0a1aba3
ac83ab8
fafca52
7d08ada
f0a9c3f
520c749
d54e124
02a722a
b7e526a
72fd6ea
12aa60d
b6b1b8c
c75d926
3c252a7
162a66b
8b58292
99e973f
be65566
ab35439
1ec1ccb
736cef7
9a51aa4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ import {BLSSignature, CommitteeIndex, Epoch, Slot, ValidatorIndex, phase0, SyncP | |
import {createIBeaconConfig, IBeaconConfig, IChainConfig} from "@chainsafe/lodestar-config"; | ||
import { | ||
ATTESTATION_SUBNET_COUNT, | ||
DOMAIN_BEACON_PROPOSER, | ||
EFFECTIVE_BALANCE_INCREMENT, | ||
FAR_FUTURE_EPOCH, | ||
GENESIS_EPOCH, | ||
|
@@ -18,8 +19,9 @@ import { | |
getChurnLimit, | ||
isActiveValidator, | ||
isAggregatorFromCommitteeLength, | ||
computeProposers, | ||
computeSyncPeriodAtEpoch, | ||
getSeed, | ||
computeProposers, | ||
} from "../util"; | ||
import {computeEpochShuffling, IEpochShuffling} from "../util/epochShuffling"; | ||
import {EffectiveBalanceIncrements, getEffectiveBalanceIncrementsWithLen} from "./effectiveBalanceIncrements"; | ||
|
@@ -47,6 +49,9 @@ export type EpochContextOpts = { | |
skipSyncPubkeys?: boolean; | ||
}; | ||
|
||
/** Defers computing proposers by persisting only the seed, and dropping it once indexes are computed */ | ||
type ProposersDeferred = {computed: false; seed: Uint8Array} | {computed: true; indexes: ValidatorIndex[]}; | ||
|
||
/** | ||
* EpochContext is the parent object of: | ||
* - Any data-structures not part of the spec'ed BeaconState | ||
|
@@ -95,6 +100,14 @@ export class EpochContext { | |
* 32 x Number | ||
*/ | ||
proposers: ValidatorIndex[]; | ||
|
||
/** | ||
* The next proposer seed is only used in the getBeaconProposersNextEpoch call. It cannot be moved into | ||
* getBeaconProposersNextEpoch because it needs state as input and all data needed by getBeaconProposersNextEpoch | ||
* should be in the epoch context. | ||
*/ | ||
proposersNextEpoch: ProposersDeferred; | ||
|
||
/** | ||
* Shuffling of validator indexes. Immutable through the epoch, then it's replaced entirely. | ||
* Note: Per spec definition, shuffling will always be defined. They are never called before loadState() | ||
|
@@ -155,6 +168,7 @@ export class EpochContext { | |
pubkey2index: PubkeyIndexMap; | ||
index2pubkey: Index2PubkeyCache; | ||
proposers: number[]; | ||
proposersNextEpoch: ProposersDeferred; | ||
previousShuffling: IEpochShuffling; | ||
currentShuffling: IEpochShuffling; | ||
nextShuffling: IEpochShuffling; | ||
|
@@ -175,6 +189,7 @@ export class EpochContext { | |
this.pubkey2index = data.pubkey2index; | ||
this.index2pubkey = data.index2pubkey; | ||
this.proposers = data.proposers; | ||
this.proposersNextEpoch = data.proposersNextEpoch; | ||
this.previousShuffling = data.previousShuffling; | ||
this.currentShuffling = data.currentShuffling; | ||
this.nextShuffling = data.nextShuffling; | ||
|
@@ -269,9 +284,18 @@ export class EpochContext { | |
: computeEpochShuffling(state, previousActiveIndices, previousEpoch); | ||
const nextShuffling = computeEpochShuffling(state, nextActiveIndices, nextEpoch); | ||
|
||
const currentProposerSeed = getSeed(state, currentEpoch, DOMAIN_BEACON_PROPOSER); | ||
|
||
// Allow to create CachedBeaconState for empty states | ||
const proposers = | ||
state.validators.length > 0 ? computeProposers(state, currentShuffling, effectiveBalanceIncrements) : []; | ||
state.validators.length > 0 | ||
? computeProposers(currentProposerSeed, currentShuffling, effectiveBalanceIncrements) | ||
: []; | ||
|
||
const proposersNextEpoch: ProposersDeferred = { | ||
computed: false, | ||
seed: getSeed(state, nextEpoch, DOMAIN_BEACON_PROPOSER), | ||
}; | ||
|
||
// Only after altair, compute the indices of the current sync committee | ||
const afterAltairFork = currentEpoch >= config.ALTAIR_FORK_EPOCH; | ||
|
@@ -319,6 +343,7 @@ export class EpochContext { | |
pubkey2index, | ||
index2pubkey, | ||
proposers, | ||
proposersNextEpoch, | ||
previousShuffling, | ||
currentShuffling, | ||
nextShuffling, | ||
|
@@ -351,6 +376,7 @@ export class EpochContext { | |
index2pubkey: this.index2pubkey, | ||
// Immutable data | ||
proposers: this.proposers, | ||
proposersNextEpoch: this.proposersNextEpoch, | ||
previousShuffling: this.previousShuffling, | ||
currentShuffling: this.currentShuffling, | ||
nextShuffling: this.nextShuffling, | ||
|
@@ -389,7 +415,11 @@ export class EpochContext { | |
const nextEpoch = currEpoch + 1; | ||
|
||
this.nextShuffling = computeEpochShuffling(state, epochProcess.nextEpochShufflingActiveValidatorIndices, nextEpoch); | ||
this.proposers = computeProposers(state, this.currentShuffling, this.effectiveBalanceIncrements); | ||
const currentProposerSeed = getSeed(state, this.currentShuffling.epoch, DOMAIN_BEACON_PROPOSER); | ||
this.proposers = computeProposers(currentProposerSeed, this.currentShuffling, this.effectiveBalanceIncrements); | ||
|
||
// Only pre-compute the seed since it's very cheap. Do the expensive computeProposers() call only on demand. | ||
this.proposersNextEpoch = {computed: false, seed: getSeed(state, this.nextShuffling.epoch, DOMAIN_BEACON_PROPOSER)}; | ||
|
||
// TODO: DEDUPLICATE from createEpochContext | ||
// | ||
|
@@ -477,6 +507,60 @@ export class EpochContext { | |
return this.proposers[slot % SLOTS_PER_EPOCH]; | ||
} | ||
|
||
getBeaconProposers(): ValidatorIndex[] { | ||
return this.proposers; | ||
} | ||
|
||
/** | ||
* We allow requesting proposal duties 1 epoch in the future as in normal network conditions it's possible to predict | ||
* the correct shuffling with high probability. While knowing the proposers in advance is not useful for consensus, | ||
* users want to know it to plan manteinance and avoid missing block proposals. | ||
* | ||
* **How to predict future proposers** | ||
* | ||
* Proposer duties for epoch N are guaranteed to be known at epoch N. Proposer duties depend exclusively on: | ||
* 1. seed (from randao_mix): known 2 epochs ahead | ||
* 2. active validator set: known 4 epochs ahead | ||
* 3. effective balance: not known ahead | ||
* | ||
* ```python | ||
* def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex: | ||
* epoch = get_current_epoch(state) | ||
* seed = hash(get_seed(state, epoch, DOMAIN_BEACON_PROPOSER) + uint_to_bytes(state.slot)) | ||
* indices = get_active_validator_indices(state, epoch) | ||
* return compute_proposer_index(state, indices, seed) | ||
* ``` | ||
* | ||
* **1**: If `MIN_SEED_LOOKAHEAD = 1` the randao_mix used for the seed is from 2 epochs ago. So at epoch N, the seed | ||
* is known and unchangable for duties at epoch N+1 and N+2 for proposer duties. | ||
* | ||
* ```python | ||
* def get_seed(state: BeaconState, epoch: Epoch, domain_type: DomainType) -> Bytes32: | ||
* mix = get_randao_mix(state, Epoch(epoch - MIN_SEED_LOOKAHEAD - 1)) | ||
* return hash(domain_type + uint_to_bytes(epoch) + mix) | ||
* ``` | ||
* | ||
* **2**: The active validator set can be predicted `MAX_SEED_LOOKAHEAD` in advance due to how activations are | ||
* processed. We already compute the active validator set for the next epoch to optimize epoch processing, so it's | ||
* reused here. | ||
* | ||
* **3**: Effective balance is not known ahead of time, but it rarely changes. Even if it changes, only a few | ||
* balances are sampled to adjust the probability of the next selection (32 per epoch on average). So to invalidate | ||
* the prediction the effective of one of those 32 samples should change and change the random_byte inequality. | ||
*/ | ||
getBeaconProposersNextEpoch(): ValidatorIndex[] { | ||
if (!this.proposersNextEpoch.computed) { | ||
const indexes = computeProposers( | ||
this.proposersNextEpoch.seed, | ||
this.nextShuffling, | ||
this.effectiveBalanceIncrements | ||
); | ||
this.proposersNextEpoch = {computed: true, indexes}; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now computation of proposers is deferred AND cached when computed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dapplion this won't cache though, and on every request to get next proposers, the You can confirm this by running the application, and trigger the end point multiple times, the if branch will always run. I noticed this while working on the implementation and I think the reason why this is the case is how the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If this is true, then it's a big issue! Can you open a dedicated issue for it? It should be investigated latter
This code runs on the main thread and according to benchmarks it takes ~100ms. That's still a significant amount of time to block the main thread and repeated computations must be avoided if possible There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And please link back to this context after opening a new dedicated issue. :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
return this.proposersNextEpoch.indexes; | ||
} | ||
|
||
/** | ||
* Return the indexed attestation corresponding to ``attestation``. | ||
*/ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a rationale on when and why the proposer prediction stands or is invalidated