diff --git a/packages/state-transition/src/block/processAttestationPhase0.ts b/packages/state-transition/src/block/processAttestationPhase0.ts index ab57bd27c80d..3a44d11e7afc 100644 --- a/packages/state-transition/src/block/processAttestationPhase0.ts +++ b/packages/state-transition/src/block/processAttestationPhase0.ts @@ -100,15 +100,30 @@ export function validateAttestation(fork: ForkSeq, state: CachedBeaconStateAllFo ); } - // Get total number of attestation participant of every committee specified - const participantCount = committeeIndices - .map((committeeIndex) => epochCtx.getBeaconCommittee(data.slot, committeeIndex).length) - .reduce((acc, committeeSize) => acc + committeeSize, 0); + const validatorsByCommittee = epochCtx.getBeaconCommittees(data.slot, committeeIndices); + const aggregationBitsArray = attestationElectra.aggregationBits.toBoolArray(); + + // Total number of attestation participants of every committee specified + let committeeOffset = 0; + for (const committeeValidators of validatorsByCommittee) { + const committeeAggregationBits = aggregationBitsArray.slice( + committeeOffset, + committeeOffset + committeeValidators.length + ); + + // Assert aggregation bits in this committee have at least one true bit + if (committeeAggregationBits.every((bit) => !bit)) { + throw new Error("Every committee in aggregation bits must have at least one attester"); + } + + committeeOffset += committeeValidators.length; + } + // Bitfield length matches total number of participants assert.equal( attestationElectra.aggregationBits.bitLen, - participantCount, - `Attestation aggregation bits length does not match total number of committee participant aggregationBitsLength=${attestation.aggregationBits.bitLen} participantCount=${participantCount}` + committeeOffset, + `Attestation aggregation bits length does not match total number of committee participants aggregationBitsLength=${attestation.aggregationBits.bitLen} participantCount=${committeeOffset}` ); } else { if (!(data.index < committeeCount)) { diff --git a/packages/state-transition/src/block/processConsolidationRequest.ts b/packages/state-transition/src/block/processConsolidationRequest.ts index 1a1d83eee0be..ade22c7454db 100644 --- a/packages/state-transition/src/block/processConsolidationRequest.ts +++ b/packages/state-transition/src/block/processConsolidationRequest.ts @@ -5,7 +5,7 @@ import {CachedBeaconStateElectra} from "../types.js"; import {hasEth1WithdrawalCredential} from "../util/capella.js"; import {hasExecutionWithdrawalCredential, isPubkeyKnown, switchToCompoundingValidator} from "../util/electra.js"; import {computeConsolidationEpochAndUpdateChurn} from "../util/epoch.js"; -import {getConsolidationChurnLimit, isActiveValidator} from "../util/validator.js"; +import {getConsolidationChurnLimit, getPendingBalanceToWithdraw, isActiveValidator} from "../util/validator.js"; // TODO Electra: Clean up necessary as there is a lot of overlap with isValidSwitchToCompoundRequest export function processConsolidationRequest( @@ -71,6 +71,16 @@ export function processConsolidationRequest( return; } + // Verify the source has been active long enough + if (currentEpoch < sourceValidator.activationEpoch + state.config.SHARD_COMMITTEE_PERIOD) { + return; + } + + // Verify the source has no pending withdrawals in the queue + if (getPendingBalanceToWithdraw(state, sourceIndex) > 0) { + return; + } + // TODO Electra: See if we can get rid of big int const exitEpoch = computeConsolidationEpochAndUpdateChurn(state, BigInt(sourceValidator.effectiveBalance)); sourceValidator.exitEpoch = exitEpoch; diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index ab1df570eb30..7f9ab6aa53f1 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -25,9 +25,8 @@ export function processWithdrawals( state: CachedBeaconStateCapella | CachedBeaconStateElectra, payload: capella.FullOrBlindedExecutionPayload ): void { - // partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) - // TODO - electra: may switch to executionWithdrawalsCount - const {withdrawals: expectedWithdrawals, partialWithdrawalsCount} = getExpectedWithdrawals(fork, state); + // processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) + const {withdrawals: expectedWithdrawals, processedPartialWithdrawalsCount} = getExpectedWithdrawals(fork, state); const numWithdrawals = expectedWithdrawals.length; if (isCapellaPayloadHeader(payload)) { @@ -59,7 +58,9 @@ export function processWithdrawals( if (fork >= ForkSeq.electra) { const stateElectra = state as CachedBeaconStateElectra; - stateElectra.pendingPartialWithdrawals = stateElectra.pendingPartialWithdrawals.sliceFrom(partialWithdrawalsCount); + stateElectra.pendingPartialWithdrawals = stateElectra.pendingPartialWithdrawals.sliceFrom( + processedPartialWithdrawalsCount + ); } // Update the nextWithdrawalIndex @@ -87,7 +88,7 @@ export function getExpectedWithdrawals( ): { withdrawals: capella.Withdrawal[]; sampledValidators: number; - partialWithdrawalsCount: number; + processedPartialWithdrawalsCount: number; } { if (fork < ForkSeq.capella) { throw new Error(`getExpectedWithdrawals not supported at forkSeq=${fork} < ForkSeq.capella`); @@ -100,7 +101,7 @@ export function getExpectedWithdrawals( const withdrawals: capella.Withdrawal[] = []; const isPostElectra = fork >= ForkSeq.electra; // partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002) - let partialWithdrawalsCount = 0; + let processedPartialWithdrawalsCount = 0; if (isPostElectra) { const stateElectra = state as CachedBeaconStateElectra; @@ -140,7 +141,7 @@ export function getExpectedWithdrawals( }); withdrawalIndex++; } - partialWithdrawalsCount++; + processedPartialWithdrawalsCount++; } } @@ -151,9 +152,14 @@ export function getExpectedWithdrawals( for (n = 0; n < bound; n++) { // Get next validator in turn const validatorIndex = (nextWithdrawalValidatorIndex + n) % validators.length; + const partiallyWithdrawnBalance = withdrawals + .filter((withdrawal) => withdrawal.validatorIndex === validatorIndex) + .reduce((acc, withdrawal) => acc + Number(withdrawal.amount), 0); const validator = validators.getReadonly(validatorIndex); - const balance = balances.get(validatorIndex); + const balance = isPostElectra + ? balances.get(validatorIndex) - partiallyWithdrawnBalance + : balances.get(validatorIndex); const {withdrawableEpoch, withdrawalCredentials, effectiveBalance} = validator; const hasWithdrawableCredentials = isPostElectra ? hasExecutionWithdrawalCredential(withdrawalCredentials) @@ -193,5 +199,5 @@ export function getExpectedWithdrawals( } } - return {withdrawals, sampledValidators: n, partialWithdrawalsCount}; + return {withdrawals, sampledValidators: n, processedPartialWithdrawalsCount}; } diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index af9e79bc831c..c975331c7dfb 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -748,13 +748,13 @@ export class EpochCache { * Return the beacon committee at slot for index. */ getBeaconCommittee(slot: Slot, index: CommitteeIndex): Uint32Array { - return this.getBeaconCommittees(slot, [index]); + return this.getBeaconCommittees(slot, [index])[0]; } /** - * Return a single Uint32Array representing concatted committees of indices + * Return a Uint32Array[] representing committees of indices */ - getBeaconCommittees(slot: Slot, indices: CommitteeIndex[]): Uint32Array { + getBeaconCommittees(slot: Slot, indices: CommitteeIndex[]): Uint32Array[] { if (indices.length === 0) { throw new Error("Attempt to get committees without providing CommitteeIndex"); } @@ -773,22 +773,7 @@ export class EpochCache { committees.push(slotCommittees[index]); } - // Early return if only one index - if (committees.length === 1) { - return committees[0]; - } - - // Create a new Uint32Array to flatten `committees` - const totalLength = committees.reduce((acc, curr) => acc + curr.length, 0); - const result = new Uint32Array(totalLength); - - let offset = 0; - for (const committee of committees) { - result.set(committee, offset); - offset += committee.length; - } - - return result; + return committees; } getCommitteeCountPerSlot(epoch: Epoch): number { @@ -911,9 +896,19 @@ export class EpochCache { // TODO Electra: resolve the naming conflicts const committeeIndices = committeeBits.getTrueBitIndexes(); - const validatorIndices = this.getBeaconCommittees(data.slot, committeeIndices); + const validatorsByCommittee = this.getBeaconCommittees(data.slot, committeeIndices); + + // Create a new Uint32Array to flatten `validatorsByCommittee` + const totalLength = validatorsByCommittee.reduce((acc, curr) => acc + curr.length, 0); + const committeeValidators = new Uint32Array(totalLength); + + let offset = 0; + for (const committee of validatorsByCommittee) { + committeeValidators.set(committee, offset); + offset += committee.length; + } - return aggregationBits.intersectValues(validatorIndices); + return aggregationBits.intersectValues(committeeValidators); } getCommitteeAssignments( diff --git a/packages/state-transition/src/slot/upgradeStateToElectra.ts b/packages/state-transition/src/slot/upgradeStateToElectra.ts index f030f9d572fe..3aae0f5b487e 100644 --- a/packages/state-transition/src/slot/upgradeStateToElectra.ts +++ b/packages/state-transition/src/slot/upgradeStateToElectra.ts @@ -56,7 +56,8 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache stateElectraView.exitBalanceToConsume = BigInt(0); const validatorsArr = stateElectraView.validators.getAllReadonly(); - const exitEpochs: Epoch[] = []; + const currentEpochPre = stateDeneb.epochCtx.epoch; + let earliestExitEpoch = computeActivationExitEpoch(currentEpochPre); // [EIP-7251]: add validators that are not yet active to pending balance deposits const preActivation: ValidatorIndex[] = []; @@ -65,17 +66,12 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache if (activationEpoch === FAR_FUTURE_EPOCH) { preActivation.push(validatorIndex); } - if (exitEpoch !== FAR_FUTURE_EPOCH) { - exitEpochs.push(exitEpoch); + if (exitEpoch !== FAR_FUTURE_EPOCH && exitEpoch > earliestExitEpoch) { + earliestExitEpoch = exitEpoch; } } - const currentEpochPre = stateDeneb.epochCtx.epoch; - - if (exitEpochs.length === 0) { - exitEpochs.push(currentEpochPre); - } - stateElectraView.earliestExitEpoch = Math.max(...exitEpochs) + 1; + stateElectraView.earliestExitEpoch = earliestExitEpoch + 1; stateElectraView.consolidationBalanceToConsume = BigInt(0); stateElectraView.earliestConsolidationEpoch = computeActivationExitEpoch(currentEpochPre); // TODO-electra: can we improve this?