diff --git a/packages/lodestar-beacon-state-transition/src/fast/block/index.ts b/packages/lodestar-beacon-state-transition/src/fast/block/index.ts index 36b607fd5145..6bbf2bf1be31 100644 --- a/packages/lodestar-beacon-state-transition/src/fast/block/index.ts +++ b/packages/lodestar-beacon-state-transition/src/fast/block/index.ts @@ -9,7 +9,7 @@ import {processAttestation} from "./processAttestation"; import {processAttesterSlashing} from "./processAttesterSlashing"; import {processDeposit} from "./processDeposit"; import {processProposerSlashing} from "./processProposerSlashing"; -import {processVoluntaryExit} from "./processVoluntaryExit"; +import {processVoluntaryExits} from "./processVoluntaryExit"; export { processBlockHeader, @@ -20,7 +20,7 @@ export { processAttesterSlashing, processDeposit, processProposerSlashing, - processVoluntaryExit, + processVoluntaryExits, }; export function processBlock( diff --git a/packages/lodestar-beacon-state-transition/src/fast/block/initiateValidatorExit.ts b/packages/lodestar-beacon-state-transition/src/fast/block/initiateValidatorExit.ts index 6d0c2bba8c1f..3eebf05e7f8f 100644 --- a/packages/lodestar-beacon-state-transition/src/fast/block/initiateValidatorExit.ts +++ b/packages/lodestar-beacon-state-transition/src/fast/block/initiateValidatorExit.ts @@ -1,10 +1,13 @@ -import {readOnlyMap} from "@chainsafe/ssz"; -import {BeaconState, ValidatorIndex} from "@chainsafe/lodestar-types"; +import {readOnlyForEach, readOnlyMap} from "@chainsafe/ssz"; +import {BeaconState, Validator, ValidatorIndex} from "@chainsafe/lodestar-types"; import {FAR_FUTURE_EPOCH} from "../../constants"; import {computeActivationExitEpoch, getChurnLimit} from "../../util"; import {EpochContext} from "../util"; +/** + * Initiate a single validator exit, similar to the spec, for backward reference only. + */ export function initiateValidatorExit(epochCtx: EpochContext, state: BeaconState, index: ValidatorIndex): void { const config = epochCtx.config; // return if validator already initiated exit @@ -29,3 +32,46 @@ export function initiateValidatorExit(epochCtx: EpochContext, state: BeaconState validator.exitEpoch = exitQueueEpoch; validator.withdrawableEpoch = validator.exitEpoch + config.params.MIN_VALIDATOR_WITHDRAWABILITY_DELAY; } + +/** + * Optimized version of initiateValidatorExit where we process validators in batch. + * The main thing is to loop through state.validators exactly 1 time. + */ +export function initiateMultipleValidatorExits( + epochCtx: EpochContext, + state: BeaconState, + indexes: ValidatorIndex[] = [] +): void { + if (!indexes || indexes.length === 0) return; + const config = epochCtx.config; + // compute exit queue epoch + const validatorExitEpochs = []; + const exitEpochs = []; + readOnlyForEach(state.validators, (v: Validator) => { + validatorExitEpochs.push(v.exitEpoch); + if (v.exitEpoch !== FAR_FUTURE_EPOCH) { + exitEpochs.push(v.exitEpoch); + } + }); + const churnLimit = getChurnLimit(config, epochCtx.currentShuffling.activeIndices.length); + const currentEpoch = epochCtx.currentShuffling.epoch; + const activationExitEpoch = computeActivationExitEpoch(config, currentEpoch); + for (const index of indexes) { + // continue if validator already initiated exit + const validator = state.validators[index]; + if (validator.exitEpoch !== FAR_FUTURE_EPOCH) { + continue; + } + let exitQueueEpoch = Math.max(...exitEpochs, activationExitEpoch); + const exitQueueChurn = validatorExitEpochs.filter((exitEpoch) => exitEpoch === exitQueueEpoch).length; + if (exitQueueChurn >= churnLimit) { + exitQueueEpoch += 1; + } + + // set validator exit epoch and withdrawable epoch + validator.exitEpoch = exitQueueEpoch; + validator.withdrawableEpoch = exitQueueEpoch + config.params.MIN_VALIDATOR_WITHDRAWABILITY_DELAY; + validatorExitEpochs[index] = exitQueueEpoch; + exitEpochs.push(exitQueueEpoch); + } +} diff --git a/packages/lodestar-beacon-state-transition/src/fast/block/processOperations.ts b/packages/lodestar-beacon-state-transition/src/fast/block/processOperations.ts index 0259ac8b6678..36ad8ad283bc 100644 --- a/packages/lodestar-beacon-state-transition/src/fast/block/processOperations.ts +++ b/packages/lodestar-beacon-state-transition/src/fast/block/processOperations.ts @@ -14,7 +14,7 @@ import {processProposerSlashing} from "./processProposerSlashing"; import {processAttesterSlashing} from "./processAttesterSlashing"; import {processAttestation} from "./processAttestation"; import {processDeposit} from "./processDeposit"; -import {processVoluntaryExit} from "./processVoluntaryExit"; +import {processVoluntaryExits} from "./processVoluntaryExit"; type Operation = ProposerSlashing | AttesterSlashing | Attestation | Deposit | VoluntaryExit; type OperationFunction = (epochCtx: EpochContext, state: BeaconState, op: Operation, verify: boolean) => void; @@ -41,10 +41,11 @@ export function processOperations( [body.attesterSlashings, processAttesterSlashing], [body.attestations, processAttestation], [body.deposits, processDeposit], - [body.voluntaryExits, processVoluntaryExit], ] as [List, OperationFunction][]).forEach(([operations, processOp]) => { readOnlyForEach(operations, (op) => { processOp(epochCtx, state, op, verifySignatures); }); }); + // process voluntary exits in batch to optimize performance + processVoluntaryExits(epochCtx, state, body.voluntaryExits, verifySignatures); } diff --git a/packages/lodestar-beacon-state-transition/src/fast/block/processVoluntaryExit.ts b/packages/lodestar-beacon-state-transition/src/fast/block/processVoluntaryExit.ts index cfd355bdf20b..03c8093205f9 100644 --- a/packages/lodestar-beacon-state-transition/src/fast/block/processVoluntaryExit.ts +++ b/packages/lodestar-beacon-state-transition/src/fast/block/processVoluntaryExit.ts @@ -1,16 +1,52 @@ import {Signature} from "@chainsafe/bls"; -import {BeaconState, SignedVoluntaryExit} from "@chainsafe/lodestar-types"; +import {BeaconState, SignedVoluntaryExit, ValidatorIndex} from "@chainsafe/lodestar-types"; +import {List, readOnlyForEach} from "@chainsafe/ssz"; import {DomainType, FAR_FUTURE_EPOCH} from "../../constants"; import {computeSigningRoot, getDomain, isActiveValidator} from "../../util"; import {EpochContext} from "../util"; -import {initiateValidatorExit} from "./initiateValidatorExit"; +import {initiateMultipleValidatorExits, initiateValidatorExit} from "./initiateValidatorExit"; +/** + * Process a single voluntary exit, for backward reference only. + */ export function processVoluntaryExit( epochCtx: EpochContext, state: BeaconState, signedVoluntaryExit: SignedVoluntaryExit, verifySignature = true +): void { + verifyVoluntaryExit(epochCtx, state, signedVoluntaryExit, verifySignature); + // initiate exit + const voluntaryExit = signedVoluntaryExit.message; + initiateValidatorExit(epochCtx, state, voluntaryExit.validatorIndex); +} + +/** + * Process voluntary exits in batch + */ +export function processVoluntaryExits( + epochCtx: EpochContext, + state: BeaconState, + signedVoluntaryExits: List, + verifySignature = true +): void { + const indexes = new Set(); + readOnlyForEach(signedVoluntaryExits, (signedVoluntaryExit) => { + verifyVoluntaryExit(epochCtx, state, signedVoluntaryExit, verifySignature); + indexes.add(signedVoluntaryExit.message.validatorIndex); + }); + initiateMultipleValidatorExits(epochCtx, state, Array.from(indexes)); +} + +/** + * The same to processVoluntaryExit without calling initiateValidatorExit. + */ +function verifyVoluntaryExit( + epochCtx: EpochContext, + state: BeaconState, + signedVoluntaryExit: SignedVoluntaryExit, + verifySignature = true ): void { const config = epochCtx.config; const voluntaryExit = signedVoluntaryExit.message; @@ -48,6 +84,4 @@ export function processVoluntaryExit( throw new Error("VoluntaryExit has an invalid signature"); } } - // initiate exit - initiateValidatorExit(epochCtx, state, voluntaryExit.validatorIndex); } diff --git a/packages/lodestar-beacon-state-transition/src/fast/block/slashValidator.ts b/packages/lodestar-beacon-state-transition/src/fast/block/slashValidator.ts index 42964e7a289a..78c6e48e07ca 100644 --- a/packages/lodestar-beacon-state-transition/src/fast/block/slashValidator.ts +++ b/packages/lodestar-beacon-state-transition/src/fast/block/slashValidator.ts @@ -2,7 +2,7 @@ import {BeaconState, ValidatorIndex} from "@chainsafe/lodestar-types"; import {decreaseBalance, increaseBalance} from "../../util"; import {EpochContext} from "../util"; -import {initiateValidatorExit} from "./initiateValidatorExit"; +import {initiateMultipleValidatorExits} from "./initiateValidatorExit"; export function slashValidator( epochCtx: EpochContext, @@ -17,7 +17,7 @@ export function slashValidator( PROPOSER_REWARD_QUOTIENT, } = epochCtx.config.params; const epoch = epochCtx.currentShuffling.epoch; - initiateValidatorExit(epochCtx, state, slashedIndex); + initiateMultipleValidatorExits(epochCtx, state, [slashedIndex]); const validator = state.validators[slashedIndex]; validator.slashed = true; validator.withdrawableEpoch = Math.max(validator.withdrawableEpoch, epoch + EPOCHS_PER_SLASHINGS_VECTOR); diff --git a/packages/spec-test-runner/test/spec/operations/voluntaryExit/voluntary_exit_fast.test.ts b/packages/spec-test-runner/test/spec/operations/voluntaryExit/voluntary_exit_fast.test.ts index d46b4fd94d56..0a19a9ede86f 100644 --- a/packages/spec-test-runner/test/spec/operations/voluntaryExit/voluntary_exit_fast.test.ts +++ b/packages/spec-test-runner/test/spec/operations/voluntaryExit/voluntary_exit_fast.test.ts @@ -1,12 +1,13 @@ import {join} from "path"; import {expect} from "chai"; -import {BeaconState} from "@chainsafe/lodestar-types"; +import {BeaconState, SignedVoluntaryExit} from "@chainsafe/lodestar-types"; import {config} from "@chainsafe/lodestar-config/lib/presets/mainnet"; import {EpochContext} from "@chainsafe/lodestar-beacon-state-transition"; -import {processVoluntaryExit} from "@chainsafe/lodestar-beacon-state-transition/lib/fast/block"; +import {processVoluntaryExits} from "@chainsafe/lodestar-beacon-state-transition/lib/fast/block"; import {describeDirectorySpecTest} from "@chainsafe/lodestar-spec-test-util/lib/single"; import {IProcessVoluntaryExitTestCase} from "./type"; import {SPEC_TEST_LOCATION} from "../../../utils/specTestCases"; +import {List} from "@chainsafe/ssz"; describeDirectorySpecTest( "process voluntary exit mainnet", @@ -15,7 +16,7 @@ describeDirectorySpecTest( const state = testcase.pre; const epochCtx = new EpochContext(config); epochCtx.loadState(state); - processVoluntaryExit(epochCtx, state, testcase.voluntary_exit); + processVoluntaryExits(epochCtx, state, [testcase.voluntary_exit] as List); return state; }, {