avoid quadratic behavior exiting validators (#6161)

* avoid quadratic behavior exiting validators

* fix libnfuzz callers
This commit is contained in:
tersec 2024-04-02 12:18:40 +00:00 committed by GitHub
parent 4457334dd0
commit 109007dc93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 184 additions and 79 deletions

View File

@ -90,18 +90,9 @@ func get_validator_activation_churn_limit*(
cfg.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT,
get_validator_churn_limit(cfg, state, cache))
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/beacon-chain.md#initiate_validator_exit
func initiate_validator_exit*(
cfg: RuntimeConfig, state: var ForkyBeaconState,
index: ValidatorIndex, cache: var StateCache): Result[void, cstring] =
## Initiate the exit of the validator with index ``index``.
if state.validators.item(index).exit_epoch != FAR_FUTURE_EPOCH:
return ok() # Before touching cache
# Return if validator already initiated exit
let validator = addr state.validators.mitem(index)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0/specs/phase0/beacon-chain.md#initiate_validator_exit
func get_state_exit_queue_info*(
cfg: RuntimeConfig, state: var ForkyBeaconState, cache: var StateCache): ExitQueueInfo =
var
exit_queue_epoch = compute_activation_exit_epoch(get_current_epoch(state))
exit_queue_churn: uint64
@ -125,9 +116,34 @@ func initiate_validator_exit*(
if exit_epoch == exit_queue_epoch:
inc exit_queue_churn
ExitQueueInfo(
exit_queue_epoch: exit_queue_epoch, exit_queue_churn: exit_queue_churn)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0/specs/phase0/beacon-chain.md#initiate_validator_exit
func initiate_validator_exit*(
cfg: RuntimeConfig, state: var ForkyBeaconState,
index: ValidatorIndex, exit_queue_info: ExitQueueInfo, cache: var StateCache):
Result[ExitQueueInfo, cstring] =
## Initiate the exit of the validator with index ``index``.
if state.validators.item(index).exit_epoch != FAR_FUTURE_EPOCH:
return ok(exit_queue_info) # Before touching cache
# Return if validator already initiated exit
let validator = addr state.validators.mitem(index)
var
exit_queue_epoch = exit_queue_info.exit_queue_epoch
exit_queue_churn = exit_queue_info.exit_queue_churn
if exit_queue_churn >= get_validator_churn_limit(cfg, state, cache):
inc exit_queue_epoch
# Bookkeeping for inter-operation caching; include this exit for next time
exit_queue_churn = 1
else:
inc exit_queue_churn
# Set validator exit epoch and withdrawable epoch
validator.exit_epoch = exit_queue_epoch
@ -138,7 +154,8 @@ func initiate_validator_exit*(
validator.withdrawable_epoch =
validator.exit_epoch + cfg.MIN_VALIDATOR_WITHDRAWABILITY_DELAY
ok()
ok(ExitQueueInfo(
exit_queue_epoch: exit_queue_epoch, exit_queue_churn: exit_queue_churn))
from ./datatypes/deneb import BeaconState
@ -183,23 +200,16 @@ func get_proposer_reward(state: ForkyBeaconState, whistleblower_reward: Gwei): G
# https://github.com/ethereum/consensus-specs/blob/v1.4.0/specs/bellatrix/beacon-chain.md#modified-slash_validator
proc slash_validator*(
cfg: RuntimeConfig, state: var ForkyBeaconState,
slashed_index: ValidatorIndex, cache: var StateCache):
Result[Gwei, cstring] =
slashed_index: ValidatorIndex, pre_exit_queue_info: ExitQueueInfo,
cache: var StateCache): Result[(Gwei, ExitQueueInfo), cstring] =
## Slash the validator with index ``index``.
let epoch = get_current_epoch(state)
? initiate_validator_exit(cfg, state, slashed_index, cache)
let
epoch = get_current_epoch(state)
post_exit_queue_info = ? initiate_validator_exit(
cfg, state, slashed_index, pre_exit_queue_info, cache)
let validator = addr state.validators.mitem(slashed_index)
trace "slash_validator: ejecting validator via slashing (validator_leaving)",
index = slashed_index,
num_validators = state.validators.len,
current_epoch = get_current_epoch(state),
validator_slashed = validator.slashed,
validator_withdrawable_epoch = validator.withdrawable_epoch,
validator_exit_epoch = validator.exit_epoch,
validator_effective_balance = validator.effective_balance
validator.slashed = true
validator.withdrawable_epoch =
max(validator.withdrawable_epoch, epoch + EPOCHS_PER_SLASHINGS_VECTOR)
@ -212,7 +222,7 @@ proc slash_validator*(
# The rest doesn't make sense without there being any proposer index, so skip
let proposer_index = get_beacon_proposer_index(state, cache).valueOr:
debug "No beacon proposer index and probably no active validators"
return ok(0.Gwei)
return ok((0.Gwei, post_exit_queue_info))
# Apply proposer and whistleblower rewards
let
@ -223,11 +233,13 @@ proc slash_validator*(
increase_balance(state, proposer_index, proposer_reward)
# TODO: evaluate if spec bug / underflow can be triggered
doAssert(whistleblower_reward >= proposer_reward, "Spec bug: underflow in slash_validator")
doAssert(
whistleblower_reward >= proposer_reward,
"Spec bug: underflow in slash_validator")
increase_balance(
state, whistleblower_index, whistleblower_reward - proposer_reward)
ok(proposer_reward)
ok((proposer_reward, post_exit_queue_info))
func genesis_time_from_eth1_timestamp(
cfg: RuntimeConfig, eth1_timestamp: uint64): uint64 =

View File

@ -580,6 +580,10 @@ type
flags*: set[RewardFlags]
ExitQueueInfo* = object
exit_queue_epoch*: Epoch
exit_queue_churn*: uint64
func pubkey*(v: HashedValidatorPubKey): ValidatorPubKey =
if isNil(v.value):
# This should never happen but we guard against it in case a

View File

@ -186,9 +186,10 @@ proc check_proposer_slashing*(
proc process_proposer_slashing*(
cfg: RuntimeConfig, state: var ForkyBeaconState,
proposer_slashing: SomeProposerSlashing, flags: UpdateFlags,
cache: var StateCache): Result[Gwei, cstring] =
exit_queue_info: ExitQueueInfo, cache: var StateCache):
Result[(Gwei, ExitQueueInfo), cstring] =
let proposer_index = ? check_proposer_slashing(state, proposer_slashing, flags)
slash_validator(cfg, state, proposer_index, cache)
slash_validator(cfg, state, proposer_index, exit_queue_info, cache)
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/beacon-chain.md#is_slashable_attestation_data
func is_slashable_attestation_data(
@ -250,17 +251,24 @@ proc process_attester_slashing*(
state: var ForkyBeaconState,
attester_slashing: SomeAttesterSlashing,
flags: UpdateFlags,
cache: var StateCache
): Result[Gwei, cstring] =
exit_queue_info: ExitQueueInfo, cache: var StateCache
): Result[(Gwei, ExitQueueInfo), cstring] =
let slashed_attesters =
? check_attester_slashing(state, attester_slashing, flags)
var proposer_reward: Gwei
var
proposer_reward: Gwei
cur_exit_queue_info = exit_queue_info
for index in slashed_attesters:
proposer_reward += ? slash_validator(cfg, state, index, cache)
doAssert strictVerification notin flags or
cur_exit_queue_info == get_state_exit_queue_info(cfg, state, cache)
let (new_proposer_reward, new_exit_queue_info) = ? slash_validator(
cfg, state, index, cur_exit_queue_info, cache)
proposer_reward += new_proposer_reward
cur_exit_queue_info = new_exit_queue_info
ok(proposer_reward)
ok((proposer_reward, cur_exit_queue_info))
func findValidatorIndex*(state: ForkyBeaconState, pubkey: ValidatorPubKey):
Opt[ValidatorIndex] =
@ -410,12 +418,12 @@ proc process_voluntary_exit*(
cfg: RuntimeConfig,
state: var ForkyBeaconState,
signed_voluntary_exit: SomeSignedVoluntaryExit,
flags: UpdateFlags,
cache: var StateCache): Result[void, cstring] =
flags: UpdateFlags, exit_queue_info: ExitQueueInfo,
cache: var StateCache): Result[ExitQueueInfo, cstring] =
let exited_validator =
? check_voluntary_exit(cfg, state, signed_voluntary_exit, flags)
? initiate_validator_exit(cfg, state, exited_validator, cache)
ok()
ok(? initiate_validator_exit(
cfg, state, exited_validator, exit_queue_info, cache))
proc process_bls_to_execution_change*(
cfg: RuntimeConfig,
@ -464,12 +472,25 @@ proc process_operations(cfg: RuntimeConfig,
var operations_rewards: BlockRewards
# It costs a full validator set scan to construct these values; only do so if
# there will be some kind of exit.
var exit_queue_info =
if body.proposer_slashings.len + body.attester_slashings.len +
body.voluntary_exits.len > 0:
get_state_exit_queue_info(cfg, state, cache)
else:
default(ExitQueueInfo) # not used
for op in body.proposer_slashings:
operations_rewards.proposer_slashings +=
? process_proposer_slashing(cfg, state, op, flags, cache)
let (proposer_slashing_reward, new_exit_queue_info) =
? process_proposer_slashing(cfg, state, op, flags, exit_queue_info, cache)
operations_rewards.proposer_slashings += proposer_slashing_reward
exit_queue_info = new_exit_queue_info
for op in body.attester_slashings:
operations_rewards.attester_slashings +=
? process_attester_slashing(cfg, state, op, flags, cache)
let (attester_slashing_reward, new_exit_queue_info) =
? process_attester_slashing(cfg, state, op, flags, exit_queue_info, cache)
operations_rewards.attester_slashings += attester_slashing_reward
exit_queue_info = new_exit_queue_info
for op in body.attestations:
operations_rewards.attestations +=
? process_attestation(state, op, flags, base_reward_per_increment, cache)
@ -478,7 +499,8 @@ proc process_operations(cfg: RuntimeConfig,
for op in body.deposits:
? process_deposit(cfg, state, bloom_filter[], op, flags)
for op in body.voluntary_exits:
? process_voluntary_exit(cfg, state, op, flags, cache)
exit_queue_info = ? process_voluntary_exit(
cfg, state, op, flags, exit_queue_info, cache)
when typeof(body).kind >= ConsensusFork.Capella:
for op in body.bls_to_execution_changes:
? process_bls_to_execution_change(cfg, state, op)

View File

@ -897,6 +897,9 @@ func process_registry_updates*(
get_validator_activation_churn_limit(cfg, state, cache)
else:
get_validator_churn_limit(cfg, state, cache)
var maybe_exit_queue_info: Opt[ExitQueueInfo]
for vidx in state.validators.vindices:
if is_eligible_for_activation_queue(state.validators.item(vidx)):
state.validators.mitem(vidx).activation_eligibility_epoch =
@ -904,7 +907,17 @@ func process_registry_updates*(
if is_active_validator(state.validators.item(vidx), get_current_epoch(state)) and
state.validators.item(vidx).effective_balance <= cfg.EJECTION_BALANCE.Gwei:
? initiate_validator_exit(cfg, state, vidx, cache)
# Typically, there will be no ejected validators, and even more rarely,
# more than one. Therefore, only calculate the information required for
# initiate_validator_exit if there actually is at least one.
let exit_queue_info = maybe_exit_queue_info.valueOr:
let initial_exit_queue_info = get_state_exit_queue_info(
cfg, state, cache)
maybe_exit_queue_info = Opt.some initial_exit_queue_info
initial_exit_queue_info
maybe_exit_queue_info = Opt.some (? initiate_validator_exit(
cfg, state, vidx, exit_queue_info, cache))
let validator = unsafeAddr state.validators.item(vidx)
if is_eligible_for_activation(state, validator[]):

View File

@ -108,7 +108,9 @@ proc nfuzz_attestation(input: openArray[byte], xoutput: ptr byte,
proc nfuzz_attester_slashing(input: openArray[byte], xoutput: ptr byte,
xoutput_size: ptr uint, disable_bls: bool): bool {.exportc, raises: [FuzzCrashError].} =
decodeAndProcess(AttesterSlashingInput):
process_attester_slashing(getRuntimeConfig(some "mainnet"), data.state, data.attesterSlashing, flags, cache).isOk
process_attester_slashing(getRuntimeConfig(some "mainnet"), data.state,
data.attesterSlashing, flags, get_state_exit_queue_info(
getRuntimeConfig(some "mainnet"), data.state, cache), cache).isOk
proc nfuzz_block(input: openArray[byte], xoutput: ptr byte,
xoutput_size: ptr uint, disable_bls: bool): bool {.exportc, raises: [FuzzCrashError].} =
@ -152,12 +154,16 @@ proc nfuzz_deposit(input: openArray[byte], xoutput: ptr byte,
proc nfuzz_proposer_slashing(input: openArray[byte], xoutput: ptr byte,
xoutput_size: ptr uint, disable_bls: bool): bool {.exportc, raises: [FuzzCrashError].} =
decodeAndProcess(ProposerSlashingInput):
process_proposer_slashing(getRuntimeConfig(some "mainnet"), data.state, data.proposerSlashing, flags, cache).isOk
process_proposer_slashing(getRuntimeConfig(some "mainnet"), data.state,
data.proposerSlashing, flags, get_state_exit_queue_info(
getRuntimeConfig(some "mainnet"), data.state, cache), cache).isOk
proc nfuzz_voluntary_exit(input: openArray[byte], xoutput: ptr byte,
xoutput_size: ptr uint, disable_bls: bool): bool {.exportc, raises: [FuzzCrashError].} =
decodeAndProcess(VoluntaryExitInput):
process_voluntary_exit(getRuntimeConfig(some "mainnet"), data.state, data.exit, flags, cache).isOk
process_voluntary_exit(getRuntimeConfig(some "mainnet"), data.state,
data.exit, flags, get_state_exit_queue_info(
getRuntimeConfig(some "mainnet"), data.state, cache), cache).isOk
# Note: Could also accept raw input pointer and access list_size + seed here.
# However, list_size needs to be known also outside this proc to allocate xoutput.

View File

@ -95,7 +95,9 @@ suite baseDescription & "Attester Slashing " & preset():
Result[void, cstring] =
var cache: StateCache
doAssert (? process_attester_slashing(
defaultRuntimeConfig, preState, attesterSlashing, {}, cache)) > 0.Gwei
defaultRuntimeConfig, preState, attesterSlashing, {strictVerification},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache))[0] > 0.Gwei
ok()
for path in walkTests(OpAttSlashingDir):
@ -134,7 +136,9 @@ suite baseDescription & "Proposer Slashing " & preset():
Result[void, cstring] =
var cache: StateCache
doAssert (? process_proposer_slashing(
defaultRuntimeConfig, preState, proposerSlashing, {}, cache)) > 0.Gwei
defaultRuntimeConfig, preState, proposerSlashing, {},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache))[0] > 0.Gwei
ok()
for path in walkTests(OpProposerSlashingDir):
@ -162,8 +166,13 @@ suite baseDescription & "Voluntary Exit " & preset():
preState: var altair.BeaconState, voluntaryExit: SignedVoluntaryExit):
Result[void, cstring] =
var cache: StateCache
process_voluntary_exit(
defaultRuntimeConfig, preState, voluntaryExit, {}, cache)
if process_voluntary_exit(
defaultRuntimeConfig, preState, voluntaryExit, {},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache).isOk:
ok()
else:
err("")
for path in walkTests(OpVoluntaryExitDir):
runTest[SignedVoluntaryExit, typeof applyVoluntaryExit](

View File

@ -24,7 +24,8 @@ import
from std/sequtils import mapIt, toSeq
from std/strutils import contains
from ../../../beacon_chain/spec/beaconstate import
get_base_reward_per_increment, get_total_active_balance, process_attestation
get_base_reward_per_increment, get_state_exit_queue_info,
get_total_active_balance, process_attestation
const
OpDir = SszTestsDir/const_preset/"bellatrix"/"operations"
@ -100,7 +101,9 @@ suite baseDescription & "Attester Slashing " & preset():
Result[void, cstring] =
var cache: StateCache
doAssert (? process_attester_slashing(
defaultRuntimeConfig, preState, attesterSlashing, {}, cache)) > 0.Gwei
defaultRuntimeConfig, preState, attesterSlashing, {strictVerification},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache))[0] > 0.Gwei
ok()
for path in walkTests(OpAttSlashingDir):
@ -158,7 +161,9 @@ suite baseDescription & "Proposer Slashing " & preset():
Result[void, cstring] =
var cache: StateCache
doAssert (? process_proposer_slashing(
defaultRuntimeConfig, preState, proposerSlashing, {}, cache)) > 0.Gwei
defaultRuntimeConfig, preState, proposerSlashing, {},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache))[0] > 0.Gwei
ok()
for path in walkTests(OpProposerSlashingDir):
@ -186,8 +191,13 @@ suite baseDescription & "Voluntary Exit " & preset():
preState: var bellatrix.BeaconState, voluntaryExit: SignedVoluntaryExit):
Result[void, cstring] =
var cache: StateCache
process_voluntary_exit(
defaultRuntimeConfig, preState, voluntaryExit, {}, cache)
if process_voluntary_exit(
defaultRuntimeConfig, preState, voluntaryExit, {},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache).isOk:
ok()
else:
err("")
for path in walkTests(OpVoluntaryExitDir):
runTest[SignedVoluntaryExit, typeof applyVoluntaryExit](

View File

@ -24,7 +24,8 @@ import
from std/sequtils import mapIt, toSeq
from std/strutils import contains
from ../../../beacon_chain/spec/beaconstate import
get_base_reward_per_increment, get_total_active_balance, process_attestation
get_base_reward_per_increment, get_state_exit_queue_info,
get_total_active_balance, process_attestation
const
OpDir = SszTestsDir/const_preset/"capella"/"operations"
@ -104,7 +105,9 @@ suite baseDescription & "Attester Slashing " & preset():
Result[void, cstring] =
var cache: StateCache
doAssert (? process_attester_slashing(
defaultRuntimeConfig, preState, attesterSlashing, {}, cache)) > 0.Gwei
defaultRuntimeConfig, preState, attesterSlashing, {strictVerification},
get_state_exit_queue_info(defaultRuntimeConfig, preState,
cache), cache))[0] > 0.Gwei
ok()
for path in walkTests(OpAttSlashingDir):
@ -175,7 +178,9 @@ suite baseDescription & "Proposer Slashing " & preset():
Result[void, cstring] =
var cache: StateCache
doAssert (? process_proposer_slashing(
defaultRuntimeConfig, preState, proposerSlashing, {}, cache)) > 0.Gwei
defaultRuntimeConfig, preState, proposerSlashing, {},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache))[0] > 0.Gwei
ok()
for path in walkTests(OpProposerSlashingDir):
@ -203,8 +208,13 @@ suite baseDescription & "Voluntary Exit " & preset():
preState: var capella.BeaconState, voluntaryExit: SignedVoluntaryExit):
Result[void, cstring] =
var cache: StateCache
process_voluntary_exit(
defaultRuntimeConfig, preState, voluntaryExit, {}, cache)
if process_voluntary_exit(
defaultRuntimeConfig, preState, voluntaryExit, {},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache).isOk:
ok()
else:
err("")
for path in walkTests(OpVoluntaryExitDir):
runTest[SignedVoluntaryExit, typeof applyVoluntaryExit](

View File

@ -24,7 +24,8 @@ import
from std/sequtils import mapIt, toSeq
from std/strutils import contains
from ../../../beacon_chain/spec/beaconstate import
get_base_reward_per_increment, get_total_active_balance, process_attestation
get_base_reward_per_increment, get_state_exit_queue_info,
get_total_active_balance, process_attestation
const
OpDir = SszTestsDir/const_preset/"deneb"/"operations"
@ -90,7 +91,7 @@ suite baseDescription & "Attestation " & preset():
# This returns the proposer reward for including the attestation, which
# isn't tested here.
discard ? process_attestation(
preState, attestation, {}, base_reward_per_increment, cache)
preState, attestation, {strictVerification}, base_reward_per_increment, cache)
ok()
for path in walkTests(OpAttestationsDir):
@ -104,7 +105,9 @@ suite baseDescription & "Attester Slashing " & preset():
Result[void, cstring] =
var cache: StateCache
doAssert (? process_attester_slashing(
defaultRuntimeConfig, preState, attesterSlashing, {}, cache)) > 0.Gwei
defaultRuntimeConfig, preState, attesterSlashing, {strictVerification},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache))[0] > 0.Gwei
ok()
for path in walkTests(OpAttSlashingDir):
@ -177,7 +180,9 @@ suite baseDescription & "Proposer Slashing " & preset():
Result[void, cstring] =
var cache: StateCache
doAssert (? process_proposer_slashing(
defaultRuntimeConfig, preState, proposerSlashing, {}, cache)) > 0.Gwei
defaultRuntimeConfig, preState, proposerSlashing, {},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache))[0] > 0.Gwei
ok()
for path in walkTests(OpProposerSlashingDir):
@ -205,8 +210,13 @@ suite baseDescription & "Voluntary Exit " & preset():
preState: var deneb.BeaconState, voluntaryExit: SignedVoluntaryExit):
Result[void, cstring] =
var cache: StateCache
process_voluntary_exit(
defaultRuntimeConfig, preState, voluntaryExit, {}, cache)
if process_voluntary_exit(
defaultRuntimeConfig, preState, voluntaryExit, {},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache).isOk:
ok()
else:
err("")
for path in walkTests(OpVoluntaryExitDir):
runTest[SignedVoluntaryExit, typeof applyVoluntaryExit](

View File

@ -86,7 +86,9 @@ suite baseDescription & "Attester Slashing " & preset():
Result[void, cstring] =
var cache: StateCache
doAssert (? process_attester_slashing(
defaultRuntimeConfig, preState, attesterSlashing, {}, cache)) > 0.Gwei
defaultRuntimeConfig, preState, attesterSlashing, {strictVerification},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache))[0] > 0.Gwei
ok()
for path in walkTests(OpAttSlashingDir):
@ -126,7 +128,9 @@ suite baseDescription & "Proposer Slashing " & preset():
Result[void, cstring] =
var cache: StateCache
doAssert (? process_proposer_slashing(
defaultRuntimeConfig, preState, proposerSlashing, {}, cache)) > 0.Gwei
defaultRuntimeConfig, preState, proposerSlashing, {},
get_state_exit_queue_info(defaultRuntimeConfig, preState,
cache), cache))[0] > 0.Gwei
ok()
for path in walkTests(OpProposerSlashingDir):
@ -139,8 +143,13 @@ suite baseDescription & "Voluntary Exit " & preset():
preState: var phase0.BeaconState, voluntaryExit: SignedVoluntaryExit):
Result[void, cstring] =
var cache: StateCache
process_voluntary_exit(
defaultRuntimeConfig, preState, voluntaryExit, {}, cache)
if process_voluntary_exit(
defaultRuntimeConfig, preState, voluntaryExit, {},
get_state_exit_queue_info(defaultRuntimeConfig, preState, cache),
cache).isOk:
ok()
else:
err("")
for path in walkTests(OpVoluntaryExitDir):
runTest[SignedVoluntaryExit, typeof applyVoluntaryExit](