mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2025-01-18 15:54:13 +00:00
b6ce6c2eba
When a late block arrives and the beacon is proposing the next block, we perform several checks to allow for the next block to reorg the incoming late block. Among those checks, we check that the parent block has been heavily attested (currently 160% of the committee size). We perform this check in these circumstances: - When the late block arrives - At 10 seconds into the slot - At 0 seconds into the next slot (at proposing time) The problem is that for blocks that arrive between 4 seconds and 10 seconds, the parent block will not have yet this expected weight since attestations from the current committee were not imported yet, and thus Prysm will send an FCU with payload attributes anyway at this time. What happens is that Prysm keeps the EL building different blocks based on different parents at the same time, when later in the next slot it calls to propose, it will reorg the late block anyway and the EL would have been computing a second payload uselessly. This PR enables this check only when calling `ShouldOverrideFCU` after 10 seconds into the slot which we do only after having imported the current attestations. We may want to actually remove this check entirely from `ShouldOverrideFCU` and only keep it in `ProposerHead`. Shout out to Anthithesis for reporting an issue that led to this discoverly.
166 lines
5.1 KiB
Go
166 lines
5.1 KiB
Go
package doublylinkedtree
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/prysmaticlabs/prysm/v5/config/params"
|
|
"github.com/prysmaticlabs/prysm/v5/time/slots"
|
|
)
|
|
|
|
// orphanLateBlockProposingEarly determines the maximum threshold that we
|
|
// consider the node is proposing early and sure to receive proposer boost
|
|
const orphanLateBlockProposingEarly = 2
|
|
|
|
// ShouldOverrideFCU returns whether the current forkchoice head is weak
|
|
// and thus may be reorged when proposing the next block.
|
|
// This function should only be called if the following two conditions are
|
|
// satisfied:
|
|
// 1- It is immediately after receiving a block that may be subject to a reorg
|
|
//
|
|
// or
|
|
//
|
|
// It is right after processAttestationsThreshold and we have processed the
|
|
// current slots attestations.
|
|
//
|
|
// 2- The caller has already called Forkchoice.Head() so that forkchoice has
|
|
// been updated.
|
|
// 3- The beacon node is serving a validator that will propose during the next
|
|
// slot.
|
|
//
|
|
// This function only applies a heuristic to decide if the beacon will update
|
|
// the engine's view of head with the parent block or the incoming block. It
|
|
// does not guarantee an attempted reorg. This will only be decided later at
|
|
// proposal time by calling GetProposerHead.
|
|
func (f *ForkChoice) ShouldOverrideFCU() (override bool) {
|
|
override = false
|
|
|
|
// We only need to override FCU if our current head is from the current
|
|
// slot. This differs from the spec implementation in that we assume
|
|
// that we will call this function in the previous slot to proposing.
|
|
head := f.store.headNode
|
|
if head == nil {
|
|
return
|
|
}
|
|
|
|
if head.slot != slots.CurrentSlot(f.store.genesisTime) {
|
|
return
|
|
}
|
|
|
|
// Do not reorg on epoch boundaries
|
|
if (head.slot+1)%params.BeaconConfig().SlotsPerEpoch == 0 {
|
|
return
|
|
}
|
|
// Only reorg blocks that arrive late
|
|
early, err := head.arrivedEarly(f.store.genesisTime)
|
|
if err != nil {
|
|
log.WithError(err).Error("could not check if block arrived early")
|
|
return
|
|
}
|
|
if early {
|
|
return
|
|
}
|
|
// Only reorg if we have been finalizing
|
|
finalizedEpoch := f.store.finalizedCheckpoint.Epoch
|
|
if slots.ToEpoch(head.slot+1) > finalizedEpoch+params.BeaconConfig().ReorgMaxEpochsSinceFinalization {
|
|
return
|
|
}
|
|
// Only orphan a single block
|
|
parent := head.parent
|
|
if parent == nil {
|
|
return
|
|
}
|
|
if head.slot > parent.slot+1 {
|
|
return
|
|
}
|
|
// Do not orphan a block that has higher justification than the parent
|
|
// if head.unrealizedJustifiedEpoch > parent.unrealizedJustifiedEpoch {
|
|
// return
|
|
// }
|
|
|
|
// Only orphan a block if the head LMD vote is weak
|
|
if head.weight*100 > f.store.committeeWeight*params.BeaconConfig().ReorgWeightThreshold {
|
|
return
|
|
}
|
|
|
|
// Return early if we are checking before 10 seconds into the slot
|
|
secs, err := slots.SecondsSinceSlotStart(head.slot, f.store.genesisTime, uint64(time.Now().Unix()))
|
|
if err != nil {
|
|
log.WithError(err).Error("could not check current slot")
|
|
return true
|
|
}
|
|
if secs < ProcessAttestationsThreshold {
|
|
return true
|
|
}
|
|
// Only orphan a block if the parent LMD vote is strong
|
|
if parent.weight*100 < f.store.committeeWeight*params.BeaconConfig().ReorgParentWeightThreshold {
|
|
return
|
|
}
|
|
return true
|
|
}
|
|
|
|
// GetProposerHead returns the block root that has to be used as ParentRoot by a
|
|
// proposer. It may not be the actual head of the canonical chain, in certain
|
|
// cases it may be its parent, when the last head block has arrived early and is
|
|
// considered safe to be orphaned.
|
|
//
|
|
// This function needs to be called only when proposing a block and all
|
|
// attestation processing has already happened.
|
|
func (f *ForkChoice) GetProposerHead() [32]byte {
|
|
head := f.store.headNode
|
|
if head == nil {
|
|
return [32]byte{}
|
|
}
|
|
|
|
// Only reorg blocks from the previous slot.
|
|
if head.slot+1 != slots.CurrentSlot(f.store.genesisTime) {
|
|
return head.root
|
|
}
|
|
// Do not reorg on epoch boundaries
|
|
if (head.slot+1)%params.BeaconConfig().SlotsPerEpoch == 0 {
|
|
return head.root
|
|
}
|
|
// Only reorg blocks that arrive late
|
|
early, err := head.arrivedEarly(f.store.genesisTime)
|
|
if err != nil {
|
|
log.WithError(err).Error("could not check if block arrived early")
|
|
return head.root
|
|
}
|
|
if early {
|
|
return head.root
|
|
}
|
|
// Only reorg if we have been finalizing
|
|
finalizedEpoch := f.store.finalizedCheckpoint.Epoch
|
|
if slots.ToEpoch(head.slot+1) > finalizedEpoch+params.BeaconConfig().ReorgMaxEpochsSinceFinalization {
|
|
return head.root
|
|
}
|
|
// Only orphan a single block
|
|
parent := head.parent
|
|
if parent == nil {
|
|
return head.root
|
|
}
|
|
if head.slot > parent.slot+1 {
|
|
return head.root
|
|
}
|
|
|
|
// Only orphan a block if the head LMD vote is weak
|
|
if head.weight*100 > f.store.committeeWeight*params.BeaconConfig().ReorgWeightThreshold {
|
|
return head.root
|
|
}
|
|
|
|
// Only orphan a block if the parent LMD vote is strong
|
|
if parent.weight*100 < f.store.committeeWeight*params.BeaconConfig().ReorgParentWeightThreshold {
|
|
return head.root
|
|
}
|
|
|
|
// Only reorg if we are proposing early
|
|
secs, err := slots.SecondsSinceSlotStart(head.slot+1, f.store.genesisTime, uint64(time.Now().Unix()))
|
|
if err != nil {
|
|
log.WithError(err).Error("could not check if proposing early")
|
|
return head.root
|
|
}
|
|
if secs >= orphanLateBlockProposingEarly {
|
|
return head.root
|
|
}
|
|
return parent.root
|
|
}
|