mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-02-23 11:48:33 +00:00
builder API liveness failsafe (#4746)
* builder API liveness failsafe * add test summary change
This commit is contained in:
parent
c9eb89e9e9
commit
fc1f9a2065
@ -258,11 +258,12 @@ OK: 1/1 Fail: 0/1 Skip: 0/1
|
|||||||
## Honest validator
|
## Honest validator
|
||||||
```diff
|
```diff
|
||||||
+ General pubsub topics OK
|
+ General pubsub topics OK
|
||||||
|
+ Liveness failsafe conditions OK
|
||||||
+ Mainnet attestation topics OK
|
+ Mainnet attestation topics OK
|
||||||
+ isNearSyncCommitteePeriod OK
|
+ isNearSyncCommitteePeriod OK
|
||||||
+ is_aggregator OK
|
+ is_aggregator OK
|
||||||
```
|
```
|
||||||
OK: 4/4 Fail: 0/4 Skip: 0/4
|
OK: 5/5 Fail: 0/5 Skip: 0/5
|
||||||
## ImportKeystores requests [Beacon Node] [Preset: mainnet]
|
## ImportKeystores requests [Beacon Node] [Preset: mainnet]
|
||||||
```diff
|
```diff
|
||||||
+ ImportKeystores/ListKeystores/DeleteKeystores [Beacon Node] [Preset: mainnet] OK
|
+ ImportKeystores/ListKeystores/DeleteKeystores [Beacon Node] [Preset: mainnet] OK
|
||||||
@ -635,4 +636,4 @@ OK: 2/2 Fail: 0/2 Skip: 0/2
|
|||||||
OK: 9/9 Fail: 0/9 Skip: 0/9
|
OK: 9/9 Fail: 0/9 Skip: 0/9
|
||||||
|
|
||||||
---TOTAL---
|
---TOTAL---
|
||||||
OK: 352/357 Fail: 0/357 Skip: 5/357
|
OK: 353/358 Fail: 0/358 Skip: 5/358
|
||||||
|
@ -409,3 +409,55 @@ func is_aggregator*(committee_len: uint64, slot_signature: ValidatorSig): bool =
|
|||||||
modulo = max(1'u64, committee_len div TARGET_AGGREGATORS_PER_COMMITTEE)
|
modulo = max(1'u64, committee_len div TARGET_AGGREGATORS_PER_COMMITTEE)
|
||||||
bytes_to_uint64(eth2digest(
|
bytes_to_uint64(eth2digest(
|
||||||
slot_signature.toRaw()).data.toOpenArray(0, 7)) mod modulo == 0
|
slot_signature.toRaw()).data.toOpenArray(0, 7)) mod modulo == 0
|
||||||
|
|
||||||
|
# https://github.com/ethereum/builder-specs/pull/47
|
||||||
|
func livenessFailsafeInEffect*(
|
||||||
|
block_roots: array[Limit SLOTS_PER_HISTORICAL_ROOT, Eth2Digest],
|
||||||
|
slot: Slot): bool =
|
||||||
|
const
|
||||||
|
MAX_MISSING_CONTIGUOUS = 3
|
||||||
|
MAX_MISSING_WINDOW = 5
|
||||||
|
|
||||||
|
static: doAssert MAX_MISSING_WINDOW > MAX_MISSING_CONTIGUOUS
|
||||||
|
if slot <= MAX_MISSING_CONTIGUOUS:
|
||||||
|
# Cannot ever trigger and allows a bit of safe arithmetic. Furthermore
|
||||||
|
# there's notionally always a genesis block, which pushes the earliest
|
||||||
|
# possible failure out an additional slot.
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Using this slightly convoluted construction to handle wraparound better;
|
||||||
|
# baseIndex + faultInspectionWindow can overflow array but only exactly by
|
||||||
|
# the required amount. Furthermore, go back one more slot to address using
|
||||||
|
# that it looks ahead rather than looks back and whether a block's missing
|
||||||
|
# requires seeing the previous block_root.
|
||||||
|
let
|
||||||
|
faultInspectionWindow = min(distinctBase(slot) - 1, SLOTS_PER_EPOCH)
|
||||||
|
baseIndex = (slot + SLOTS_PER_HISTORICAL_ROOT - faultInspectionWindow) mod
|
||||||
|
SLOTS_PER_HISTORICAL_ROOT
|
||||||
|
endIndex = baseIndex + faultInspectionWindow - 1
|
||||||
|
|
||||||
|
doAssert endIndex mod SLOTS_PER_HISTORICAL_ROOT ==
|
||||||
|
(slot - 1) mod SLOTS_PER_HISTORICAL_ROOT
|
||||||
|
|
||||||
|
var
|
||||||
|
totalMissing = 0
|
||||||
|
streakLen = 0
|
||||||
|
maxStreakLen = 0
|
||||||
|
|
||||||
|
for i in baseIndex .. endIndex:
|
||||||
|
# This look-forward means checking slot i for being missing uses i - 1
|
||||||
|
if block_roots[(i mod SLOTS_PER_HISTORICAL_ROOT).int] ==
|
||||||
|
block_roots[((i + 1) mod SLOTS_PER_HISTORICAL_ROOT).int]:
|
||||||
|
totalMissing += 1
|
||||||
|
if totalMissing > MAX_MISSING_WINDOW:
|
||||||
|
return true
|
||||||
|
|
||||||
|
streakLen += 1
|
||||||
|
if streakLen > maxStreakLen:
|
||||||
|
maxStreakLen = streakLen
|
||||||
|
if maxStreakLen > MAX_MISSING_CONTIGUOUS:
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
streakLen = 0
|
||||||
|
|
||||||
|
false
|
||||||
|
@ -681,6 +681,7 @@ proc proposeBlockMEV[
|
|||||||
node: BeaconNode, head: BlockRef, validator: AttachedValidator, slot: Slot,
|
node: BeaconNode, head: BlockRef, validator: AttachedValidator, slot: Slot,
|
||||||
randao: ValidatorSig, validator_index: ValidatorIndex):
|
randao: ValidatorSig, validator_index: ValidatorIndex):
|
||||||
Future[Opt[BlockRef]] {.async.} =
|
Future[Opt[BlockRef]] {.async.} =
|
||||||
|
# Used by the BN's own validators, but not the REST server
|
||||||
when SBBB is bellatrix_mev.SignedBlindedBeaconBlock:
|
when SBBB is bellatrix_mev.SignedBlindedBeaconBlock:
|
||||||
type EPH = bellatrix.ExecutionPayloadHeader
|
type EPH = bellatrix.ExecutionPayloadHeader
|
||||||
elif SBBB is capella_mev.SignedBlindedBeaconBlock:
|
elif SBBB is capella_mev.SignedBlindedBeaconBlock:
|
||||||
@ -749,6 +750,9 @@ proc makeBlindedBeaconBlockForHeadAndSlot*[
|
|||||||
## Requests a beacon node to produce a valid blinded block, which can then be
|
## Requests a beacon node to produce a valid blinded block, which can then be
|
||||||
## signed by a validator. A blinded block is a block with only a transactions
|
## signed by a validator. A blinded block is a block with only a transactions
|
||||||
## root, rather than a full transactions list.
|
## root, rather than a full transactions list.
|
||||||
|
##
|
||||||
|
## This function is used by the validator client, but not the beacon node for
|
||||||
|
## its own validators.
|
||||||
when BBB is bellatrix_mev.BlindedBeaconBlock:
|
when BBB is bellatrix_mev.BlindedBeaconBlock:
|
||||||
type EPH = bellatrix.ExecutionPayloadHeader
|
type EPH = bellatrix.ExecutionPayloadHeader
|
||||||
elif BBB is capella_mev.BlindedBeaconBlock:
|
elif BBB is capella_mev.BlindedBeaconBlock:
|
||||||
@ -760,6 +764,11 @@ proc makeBlindedBeaconBlockForHeadAndSlot*[
|
|||||||
pubkey =
|
pubkey =
|
||||||
# Relevant state for knowledge of validators
|
# Relevant state for knowledge of validators
|
||||||
withState(node.dag.headState):
|
withState(node.dag.headState):
|
||||||
|
if livenessFailsafeInEffect(
|
||||||
|
forkyState.data.block_roots.data, forkyState.data.slot):
|
||||||
|
# It's head block's slot which matters here, not proposal slot
|
||||||
|
return err("Builder API liveness failsafe in effect")
|
||||||
|
|
||||||
if distinctBase(validator_index) >= forkyState.data.validators.lenu64:
|
if distinctBase(validator_index) >= forkyState.data.validators.lenu64:
|
||||||
debug "makeBlindedBeaconBlockForHeadAndSlot: invalid validator index",
|
debug "makeBlindedBeaconBlockForHeadAndSlot: invalid validator index",
|
||||||
head = shortLog(head),
|
head = shortLog(head),
|
||||||
@ -819,29 +828,35 @@ proc proposeBlock(node: BeaconNode,
|
|||||||
res.get()
|
res.get()
|
||||||
|
|
||||||
if node.config.payloadBuilderEnable:
|
if node.config.payloadBuilderEnable:
|
||||||
let newBlockMEV =
|
let failsafeInEffect =
|
||||||
if slot.epoch >= node.dag.cfg.DENEB_FORK_EPOCH:
|
withState(node.dag.headState):
|
||||||
debugRaiseAssert $denebImplementationMissing & ": proposeBlock"
|
# Head slot, not proposal slot, matters here
|
||||||
await proposeBlockMEV[
|
livenessFailsafeInEffect(
|
||||||
capella_mev.SignedBlindedBeaconBlock](
|
forkyState.data.block_roots.data, forkyState.data.slot)
|
||||||
node, head, validator, slot, randao, validator_index)
|
if not failsafeInEffect:
|
||||||
elif slot.epoch >= node.dag.cfg.CAPELLA_FORK_EPOCH:
|
let newBlockMEV =
|
||||||
await proposeBlockMEV[
|
if slot.epoch >= node.dag.cfg.DENEB_FORK_EPOCH:
|
||||||
capella_mev.SignedBlindedBeaconBlock](
|
debugRaiseAssert $denebImplementationMissing & ": proposeBlock"
|
||||||
node, head, validator, slot, randao, validator_index)
|
await proposeBlockMEV[
|
||||||
else:
|
capella_mev.SignedBlindedBeaconBlock](
|
||||||
await proposeBlockMEV[
|
node, head, validator, slot, randao, validator_index)
|
||||||
bellatrix_mev.SignedBlindedBeaconBlock](
|
elif slot.epoch >= node.dag.cfg.CAPELLA_FORK_EPOCH:
|
||||||
node, head, validator, slot, randao, validator_index)
|
await proposeBlockMEV[
|
||||||
|
capella_mev.SignedBlindedBeaconBlock](
|
||||||
|
node, head, validator, slot, randao, validator_index)
|
||||||
|
else:
|
||||||
|
await proposeBlockMEV[
|
||||||
|
bellatrix_mev.SignedBlindedBeaconBlock](
|
||||||
|
node, head, validator, slot, randao, validator_index)
|
||||||
|
|
||||||
if newBlockMEV.isSome:
|
if newBlockMEV.isSome:
|
||||||
# This might be equivalent to the `head` passed in, but it signals that
|
# This might be equivalent to the `head` passed in, but it signals that
|
||||||
# `submitBlindedBlock` ran, so don't do anything else. Otherwise, it is
|
# `submitBlindedBlock` ran, so don't do anything else. Otherwise, it is
|
||||||
# fine to try again with the local EL.
|
# fine to try again with the local EL.
|
||||||
if newBlockMEV.get == head:
|
if newBlockMEV.get == head:
|
||||||
# Returning same block as head indicates failure to generate new block
|
# Returning same block as head indicates failure to generate new block
|
||||||
beacon_block_builder_missed_without_fallback.inc()
|
beacon_block_builder_missed_without_fallback.inc()
|
||||||
return newBlockMEV.get
|
return newBlockMEV.get
|
||||||
|
|
||||||
# TODO Compare the value of the MEV block and the execution block
|
# TODO Compare the value of the MEV block and the execution block
|
||||||
# obtained from the EL below:
|
# obtained from the EL below:
|
||||||
|
@ -198,3 +198,86 @@ suite "Honest validator":
|
|||||||
for i in 1'u64 .. 20'u64:
|
for i in 1'u64 .. 20'u64:
|
||||||
for j in (SYNC_COMMITTEE_SUBNET_COUNT + 1'u64) .. 7'u64:
|
for j in (SYNC_COMMITTEE_SUBNET_COUNT + 1'u64) .. 7'u64:
|
||||||
check: nearSyncCommitteePeriod((EPOCHS_PER_SYNC_COMMITTEE_PERIOD * i - j).Epoch).isNone
|
check: nearSyncCommitteePeriod((EPOCHS_PER_SYNC_COMMITTEE_PERIOD * i - j).Epoch).isNone
|
||||||
|
|
||||||
|
test "Liveness failsafe conditions":
|
||||||
|
var x: array[Limit SLOTS_PER_HISTORICAL_ROOT, Eth2Digest]
|
||||||
|
const MAX_MISSING_CONTIGUOUS = 3
|
||||||
|
const MAX_MISSING_WINDOW = 5
|
||||||
|
const FAULT_INSPECTION_WINDOW = 32
|
||||||
|
|
||||||
|
# There haven't been enough slots to trigger any of the conditions
|
||||||
|
for i in 0 .. MAX_MISSING_CONTIGUOUS + 1:
|
||||||
|
check: not livenessFailsafeInEffect(x, i.Slot)
|
||||||
|
# But once there are, the default all-equals array shouldn't allow it. An
|
||||||
|
# additional slot is gained because it's notionally not possible for some
|
||||||
|
# genesis block not to exist.
|
||||||
|
for i in MAX_MISSING_CONTIGUOUS + 2 .. FAULT_INSPECTION_WINDOW + 10:
|
||||||
|
check: livenessFailsafeInEffect(x, i.Slot)
|
||||||
|
|
||||||
|
for i in FAULT_INSPECTION_WINDOW * 2 ..< FAULT_INSPECTION_WINDOW * 3:
|
||||||
|
x[i].data[0] = i.uint8
|
||||||
|
|
||||||
|
# There haven't been enough slots to trigger any of the conditions; unlike
|
||||||
|
# first round this doesn't line up with genesis-adjacent slots and doesn't
|
||||||
|
# have that additional genesis block additional-slot-before-trigger.
|
||||||
|
for i in
|
||||||
|
FAULT_INSPECTION_WINDOW * 3 ..
|
||||||
|
FAULT_INSPECTION_WINDOW * 3 + MAX_MISSING_CONTIGUOUS:
|
||||||
|
check: not livenessFailsafeInEffect(x, i.Slot)
|
||||||
|
for i in
|
||||||
|
FAULT_INSPECTION_WINDOW * 3 + MAX_MISSING_CONTIGUOUS + 1 ..
|
||||||
|
FAULT_INSPECTION_WINDOW * 4:
|
||||||
|
check: livenessFailsafeInEffect(x, i.Slot)
|
||||||
|
|
||||||
|
# This time, add some extant blocks to extend non-liveness-failsafe conditions
|
||||||
|
for i in FAULT_INSPECTION_WINDOW * 4 ..< FAULT_INSPECTION_WINDOW * 5:
|
||||||
|
x[i].data[0] = i.uint8
|
||||||
|
# extend last entry to simulate missing blocks
|
||||||
|
for i in
|
||||||
|
FAULT_INSPECTION_WINDOW * 5 ..<
|
||||||
|
FAULT_INSPECTION_WINDOW * 5 + MAX_MISSING_CONTIGUOUS:
|
||||||
|
x[i].data[0] = (FAULT_INSPECTION_WINDOW * 5 - 1).uint8
|
||||||
|
# next real block
|
||||||
|
x[FAULT_INSPECTION_WINDOW * 5 + MAX_MISSING_CONTIGUOUS].data[0] = 34
|
||||||
|
|
||||||
|
for i in
|
||||||
|
FAULT_INSPECTION_WINDOW * 5 ..
|
||||||
|
FAULT_INSPECTION_WINDOW * 3 + MAX_MISSING_CONTIGUOUS * 2:
|
||||||
|
check: not livenessFailsafeInEffect(x, i.Slot)
|
||||||
|
for i in
|
||||||
|
FAULT_INSPECTION_WINDOW * 5 + MAX_MISSING_CONTIGUOUS * 2 + 1 ..
|
||||||
|
FAULT_INSPECTION_WINDOW * 6:
|
||||||
|
check: livenessFailsafeInEffect(x, i.Slot)
|
||||||
|
|
||||||
|
# Add some all-present blocks for a few epochs
|
||||||
|
for i in FAULT_INSPECTION_WINDOW * 6 ..< FAULT_INSPECTION_WINDOW * 9:
|
||||||
|
x[i].data[0] = i.uint8
|
||||||
|
static: doAssert MAX_MISSING_WINDOW > MAX_MISSING_CONTIGUOUS
|
||||||
|
# This satisfies contiguous-missing limit, but not total-per-window limit
|
||||||
|
for i in countup(
|
||||||
|
FAULT_INSPECTION_WINDOW * 9,
|
||||||
|
FAULT_INSPECTION_WINDOW * 9 + MAX_MISSING_CONTIGUOUS * 2, 2):
|
||||||
|
x[i].data[0] = i.uint8
|
||||||
|
x[i + 1].data[0] = i.uint8 # missing block
|
||||||
|
|
||||||
|
for i in
|
||||||
|
FAULT_INSPECTION_WINDOW * 7 ..
|
||||||
|
FAULT_INSPECTION_WINDOW * 9 + MAX_MISSING_WINDOW * 2 - 1:
|
||||||
|
# i.e. two fullly covered epochs then get into MAX_MISSING_WINDOW * 2 - 1
|
||||||
|
# of the every-other-block is present. Because only MAX_MISSING_WINDOW of
|
||||||
|
# these can exist, it's the ones at (FIW*9 base of 0): 1, 3, 5, 7, 9 that
|
||||||
|
# are missing. Can get up to 9 here, i.e. by 2 * MAX_MISSING_WINDOW, as a
|
||||||
|
# result of 50% duty cycle pattern.
|
||||||
|
check: not livenessFailsafeInEffect(x, i.Slot)
|
||||||
|
for i in
|
||||||
|
FAULT_INSPECTION_WINDOW * 9 + MAX_MISSING_WINDOW * 2 ..
|
||||||
|
FAULT_INSPECTION_WINDOW * 10:
|
||||||
|
check: livenessFailsafeInEffect(x, i.Slot)
|
||||||
|
|
||||||
|
# Check wraparound is sane; same mod-equivalent slots but actually near
|
||||||
|
# genesis don't trigger liveness failures, as they clamp the inspection
|
||||||
|
# window at element 0 of array rather than wrapping backwards.
|
||||||
|
for i in
|
||||||
|
SLOTS_PER_HISTORICAL_ROOT ..
|
||||||
|
SLOTS_PER_HISTORICAL_ROOT + FAULT_INSPECTION_WINDOW:
|
||||||
|
check: livenessFailsafeInEffect(x, i.Slot)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user