feat: implement epbs state transition (#8507)

Implement epbs state transition function.

Passes all operations, epoch_transition and rewards spec tests on v1.6.1

Part of #8439

---------

Co-authored-by: Nico Flaig <nflaig@protonmail.com>
This commit is contained in:
NC
2025-12-04 08:35:47 -08:00
committed by GitHub
parent f61b8d9dad
commit 1f2a3a4524
38 changed files with 1026 additions and 110 deletions

View File

@@ -22,6 +22,7 @@ import {
import {
CachedBeaconStateAllForks,
CachedBeaconStateAltair,
CachedBeaconStateGloas,
CachedBeaconStatePhase0,
EffectiveBalanceIncrements,
RootCache,
@@ -486,7 +487,10 @@ export class AggregatedAttestationPool {
consolidation.attData,
inclusionDistance,
stateEpoch,
rootCache
rootCache,
ForkSeq[fork] >= ForkSeq.gloas
? (state as CachedBeaconStateGloas).executionPayloadAvailability.toBoolArray()
: null
);
const weight =

View File

@@ -51,6 +51,7 @@ const epochTransitionFns: Record<string, EpochTransitionFn> = {
const fork = state.config.getForkSeq(state.slot);
epochFns.processProposerLookahead(fork, state as CachedBeaconStateFulu, epochTransitionCache);
},
builder_pending_payments: epochFns.processBuilderPendingPayments as EpochTransitionFn,
};
/**

View File

@@ -1,5 +1,5 @@
import path from "node:path";
import {ACTIVE_PRESET, ForkName} from "@lodestar/params";
import {ACTIVE_PRESET, ForkName, ForkSeq} from "@lodestar/params";
import {InputType} from "@lodestar/spec-test-util";
import {
BeaconStateAllForks,
@@ -7,11 +7,12 @@ import {
CachedBeaconStateBellatrix,
CachedBeaconStateCapella,
CachedBeaconStateElectra,
CachedBeaconStateGloas,
ExecutionPayloadStatus,
getBlockRootAtSlot,
} from "@lodestar/state-transition";
import * as blockFns from "@lodestar/state-transition/block";
import {AttesterSlashing, altair, bellatrix, capella, electra, phase0, ssz, sszTypesFor} from "@lodestar/types";
import {AttesterSlashing, altair, bellatrix, capella, electra, gloas, phase0, ssz, sszTypesFor} from "@lodestar/types";
import {createCachedBeaconStateTest} from "../../utils/cachedBeaconState.js";
import {getConfig} from "../../utils/config.js";
import {ethereumConsensusSpecsTests} from "../specTestVersioning.js";
@@ -67,13 +68,24 @@ const operationFns: Record<string, BlockProcessFn<CachedBeaconStateAllForks>> =
blockFns.processVoluntaryExit(fork, state, testCase.voluntary_exit);
},
execution_payload: (state, testCase: {body: bellatrix.BeaconBlockBody; execution: {execution_valid: boolean}}) => {
execution_payload: (
state,
testCase: {
body: bellatrix.BeaconBlockBody | gloas.BeaconBlockBody;
signed_envelope: gloas.SignedExecutionPayloadEnvelope;
execution: {execution_valid: boolean};
}
) => {
const fork = state.config.getForkSeq(state.slot);
blockFns.processExecutionPayload(fork, state as CachedBeaconStateBellatrix, testCase.body, {
executionPayloadStatus: testCase.execution.execution_valid
? ExecutionPayloadStatus.valid
: ExecutionPayloadStatus.invalid,
});
if (fork >= ForkSeq.gloas) {
blockFns.processExecutionPayloadEnvelope(state as CachedBeaconStateGloas, testCase.signed_envelope, true);
} else {
blockFns.processExecutionPayload(fork, state as CachedBeaconStateBellatrix, testCase.body, {
executionPayloadStatus: testCase.execution.execution_valid
? ExecutionPayloadStatus.valid
: ExecutionPayloadStatus.invalid,
});
}
},
bls_to_execution_change: (state, testCase: {address_change: capella.SignedBLSToExecutionChange}) => {
@@ -95,7 +107,16 @@ const operationFns: Record<string, BlockProcessFn<CachedBeaconStateAllForks>> =
},
consolidation_request: (state, testCase: {consolidation_request: electra.ConsolidationRequest}) => {
blockFns.processConsolidationRequest(state as CachedBeaconStateElectra, testCase.consolidation_request);
const fork = state.config.getForkSeq(state.slot);
blockFns.processConsolidationRequest(fork, state as CachedBeaconStateElectra, testCase.consolidation_request);
},
execution_payload_bid: (state, testCase: {block: gloas.BeaconBlock}) => {
blockFns.processExecutionPayloadBid(state as CachedBeaconStateGloas, testCase.block);
},
payload_attestation: (state, testCase: {payload_attestation: gloas.PayloadAttestation}) => {
blockFns.processPayloadAttestation(state as CachedBeaconStateGloas, testCase.payload_attestation);
},
};
@@ -149,6 +170,8 @@ const operations: TestRunnerFn<OperationsTestCase, BeaconStateAllForks> = (fork,
withdrawal_request: ssz.electra.WithdrawalRequest,
deposit_request: ssz.electra.DepositRequest,
consolidation_request: ssz.electra.ConsolidationRequest,
payload_attestation: ssz.gloas.PayloadAttestation,
signed_envelope: ssz.gloas.SignedExecutionPayloadEnvelope,
},
shouldError: (testCase) => testCase.post === undefined,
getExpected: (testCase) => testCase.post,

View File

@@ -14,7 +14,7 @@ import {DownloadTestsOptions} from "@lodestar/spec-test-util/downloadTests";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const ethereumConsensusSpecsTests: DownloadTestsOptions = {
specVersion: "v1.6.0-beta.2",
specVersion: "v1.6.1",
// Target directory is the host package root: 'packages/*/spec-tests'
outputDir: path.join(__dirname, "../../spec-tests"),
specTestsRepoUrl: "https://github.com/ethereum/consensus-specs",

View File

@@ -69,7 +69,7 @@ export const defaultSkipOpts: SkipOpts = {
/^electra\/light_client\/single_merkle_proof\/BeaconBlockBody.*/,
/^fulu\/light_client\/single_merkle_proof\/BeaconBlockBody.*/,
/^.+\/light_client\/data_collection\/.*/,
/^gloas\/(?!.*ssz_static).*$/,
/^gloas\/(finality|fork_choice|networking|sanity|transition)\/.*$/,
/^gloas\/ssz_static\/ForkChoiceNode.*$/,
],
skippedTests: [],

View File

@@ -8,7 +8,7 @@ import {loadConfigYaml} from "../yaml.js";
// Not e2e, but slow. Run with e2e tests
/** https://github.com/ethereum/consensus-specs/releases */
const specConfigCommit = "v1.6.0-beta.2";
const specConfigCommit = "v1.6.1";
/**
* Fields that we filter from local config when doing comparison.
* Ideally this should be empty as it is not spec compliant

View File

@@ -1,14 +1,22 @@
import {ForkSeq} from "@lodestar/params";
import {ForkPostGloas, ForkSeq} from "@lodestar/params";
import {BeaconBlock, BlindedBeaconBlock, altair, capella} from "@lodestar/types";
import {BeaconStateTransitionMetrics} from "../metrics.js";
import {CachedBeaconStateAllForks, CachedBeaconStateBellatrix, CachedBeaconStateCapella} from "../types.js";
import {
CachedBeaconStateAllForks,
CachedBeaconStateBellatrix,
CachedBeaconStateCapella,
CachedBeaconStateGloas,
} from "../types.js";
import {getFullOrBlindedPayload, isExecutionEnabled} from "../util/execution.js";
import {BlockExternalData, DataAvailabilityStatus} from "./externalData.js";
import {processBlobKzgCommitments} from "./processBlobKzgCommitments.js";
import {processBlockHeader} from "./processBlockHeader.js";
import {processEth1Data} from "./processEth1Data.js";
import {processExecutionPayload} from "./processExecutionPayload.js";
import {processExecutionPayloadBid} from "./processExecutionPayloadBid.ts";
import {processExecutionPayloadEnvelope} from "./processExecutionPayloadEnvelope.ts";
import {processOperations} from "./processOperations.js";
import {processPayloadAttestation} from "./processPayloadAttestation.ts";
import {processRandao} from "./processRandao.js";
import {processSyncAggregate} from "./processSyncCommittee.js";
import {processWithdrawals} from "./processWithdrawals.js";
@@ -22,6 +30,9 @@ export {
processEth1Data,
processSyncAggregate,
processWithdrawals,
processExecutionPayloadBid,
processPayloadAttestation,
processExecutionPayloadEnvelope,
};
export * from "./externalData.js";
@@ -41,23 +52,33 @@ export function processBlock(
processBlockHeader(state, block);
if (fork >= ForkSeq.gloas) {
// After gloas, processWithdrawals does not take a payload parameter
processWithdrawals(fork, state as CachedBeaconStateGloas);
} else if (fork >= ForkSeq.capella) {
const fullOrBlindedPayload = getFullOrBlindedPayload(block);
processWithdrawals(
fork,
state as CachedBeaconStateCapella,
fullOrBlindedPayload as capella.FullOrBlindedExecutionPayload
);
}
// The call to the process_execution_payload must happen before the call to the process_randao as the former depends
// on the randao_mix computed with the reveal of the previous block.
if (fork >= ForkSeq.bellatrix && isExecutionEnabled(state as CachedBeaconStateBellatrix, block)) {
const fullOrBlindedPayload = getFullOrBlindedPayload(block);
// TODO Deneb: Allow to disable withdrawals for interop testing
// https://github.com/ethereum/consensus-specs/blob/b62c9e877990242d63aa17a2a59a49bc649a2f2e/specs/eip4844/beacon-chain.md#disabling-withdrawals
if (fork >= ForkSeq.capella) {
processWithdrawals(
fork,
state as CachedBeaconStateCapella,
fullOrBlindedPayload as capella.FullOrBlindedExecutionPayload
);
}
// TODO GLOAS: We call processExecutionPayload somewhere else post-gloas
if (
fork >= ForkSeq.bellatrix &&
fork < ForkSeq.gloas &&
isExecutionEnabled(state as CachedBeaconStateBellatrix, block)
) {
processExecutionPayload(fork, state as CachedBeaconStateBellatrix, block.body, externalData);
}
if (fork >= ForkSeq.gloas) {
processExecutionPayloadBid(state as CachedBeaconStateGloas, block as BeaconBlock<ForkPostGloas>);
}
processRandao(state, block, verifySignatures);
processEth1Data(state, block.body.eth1Data);
processOperations(fork, state, block.body, opts, metrics);

View File

@@ -0,0 +1,23 @@
import {gloas} from "@lodestar/types";
import {getIndexedPayloadAttestationSignatureSet} from "../signatureSets/index.ts";
import {CachedBeaconStateGloas} from "../types.js";
import {verifySignatureSet} from "../util/index.ts";
export function isValidIndexedPayloadAttestation(
state: CachedBeaconStateGloas,
indexedPayloadAttestation: gloas.IndexedPayloadAttestation,
verifySignature: boolean
): boolean {
const indices = indexedPayloadAttestation.attestingIndices;
const isSorted = indices.every((val, i, arr) => i === 0 || arr[i - 1] <= val);
if (indices.length === 0 || !isSorted) {
return false;
}
if (verifySignature) {
return verifySignatureSet(getIndexedPayloadAttestationSignatureSet(state, indexedPayloadAttestation));
}
return true;
}

View File

@@ -86,7 +86,11 @@ export function validateAttestation(fork: ForkSeq, state: CachedBeaconStateAllFo
}
if (fork >= ForkSeq.electra) {
assert.equal(data.index, 0, `AttestationData.index must be zero: index=${data.index}`);
if (fork >= ForkSeq.gloas) {
assert.lt(data.index, 2, `AttestationData.index must be 0 or 1: index=${data.index}`);
} else {
assert.equal(data.index, 0, `AttestationData.index must be 0: index=${data.index}`);
}
const attestationElectra = attestation as electra.Attestation;
const committeeIndices = attestationElectra.committeeBits.getTrueBitIndexes();

View File

@@ -1,9 +1,11 @@
import {byteArrayEquals} from "@chainsafe/ssz";
import {
EFFECTIVE_BALANCE_INCREMENT,
ForkSeq,
MIN_ATTESTATION_INCLUSION_DELAY,
PROPOSER_WEIGHT,
SLOTS_PER_EPOCH,
SLOTS_PER_HISTORICAL_ROOT,
TIMELY_HEAD_FLAG_INDEX,
TIMELY_HEAD_WEIGHT,
TIMELY_SOURCE_FLAG_INDEX,
@@ -16,7 +18,8 @@ import {Attestation, Epoch, phase0} from "@lodestar/types";
import {intSqrt} from "@lodestar/utils";
import {BeaconStateTransitionMetrics} from "../metrics.js";
import {getAttestationWithIndicesSignatureSet} from "../signatureSets/indexedAttestation.js";
import {CachedBeaconStateAltair} from "../types.js";
import {CachedBeaconStateAltair, CachedBeaconStateGloas} from "../types.js";
import {isAttestationSameSlot, isAttestationSameSlotRootCache} from "../util/gloas.ts";
import {increaseBalance, verifySignatureSet} from "../util/index.js";
import {RootCache} from "../util/rootCache.js";
import {checkpointToStr, isTimelyTarget, validateAttestation} from "./processAttestationPhase0.js";
@@ -31,7 +34,7 @@ const SLOTS_PER_EPOCH_SQRT = intSqrt(SLOTS_PER_EPOCH);
export function processAttestationsAltair(
fork: ForkSeq,
state: CachedBeaconStateAltair,
state: CachedBeaconStateAltair | CachedBeaconStateGloas,
attestations: Attestation[],
verifySignature = true,
metrics?: BeaconStateTransitionMetrics | null
@@ -46,6 +49,9 @@ export function processAttestationsAltair(
let proposerReward = 0;
let newSeenAttesters = 0;
let newSeenAttestersEffectiveBalance = 0;
const builderWeightMap: Map<number, number> = new Map();
for (const attestation of attestations) {
const data = attestation.data;
@@ -66,13 +72,16 @@ export function processAttestationsAltair(
const inCurrentEpoch = data.target.epoch === currentEpoch;
const epochParticipation = inCurrentEpoch ? state.currentEpochParticipation : state.previousEpochParticipation;
// Count how much additional weight added to current or previous epoch's builder pending payment (in ETH increment)
let paymentWeightToAdd = 0;
const flagsAttestation = getAttestationParticipationStatus(
fork,
data,
stateSlot - data.slot,
epochCtx.epoch,
rootCache
rootCache,
fork >= ForkSeq.gloas ? (state as CachedBeaconStateGloas).executionPayloadAvailability.toBoolArray() : null
);
// For each participant, update their participation
@@ -121,12 +130,35 @@ export function processAttestationsAltair(
}
}
}
if (fork >= ForkSeq.gloas && flagsNewSet !== 0 && isAttestationSameSlot(state as CachedBeaconStateGloas, data)) {
paymentWeightToAdd += effectiveBalanceIncrements[validatorIndex];
}
}
// Do the discrete math inside the loop to ensure a deterministic result
const totalIncrements = totalBalanceIncrementsWithWeight;
const proposerRewardNumerator = totalIncrements * state.epochCtx.baseRewardPerIncrement;
proposerReward += Math.floor(proposerRewardNumerator / PROPOSER_REWARD_DOMINATOR);
if (fork >= ForkSeq.gloas) {
const builderPendingPaymentIndex = inCurrentEpoch
? SLOTS_PER_EPOCH + (data.slot % SLOTS_PER_EPOCH)
: data.slot % SLOTS_PER_EPOCH;
const existingWeight =
builderWeightMap.get(builderPendingPaymentIndex) ??
(state as CachedBeaconStateGloas).builderPendingPayments.get(builderPendingPaymentIndex).weight;
const updatedWeight = existingWeight + paymentWeightToAdd * EFFECTIVE_BALANCE_INCREMENT;
builderWeightMap.set(builderPendingPaymentIndex, updatedWeight);
}
}
for (const [index, weight] of builderWeightMap) {
const payment = (state as CachedBeaconStateGloas).builderPendingPayments.get(index);
if (payment.withdrawal.amount > 0) {
payment.weight = weight;
}
}
metrics?.newSeenAttestersPerBlock.set(newSeenAttesters);
@@ -145,7 +177,8 @@ export function getAttestationParticipationStatus(
data: phase0.AttestationData,
inclusionDelay: number,
currentEpoch: Epoch,
rootCache: RootCache
rootCache: RootCache,
executionPayloadAvailability: boolean[] | null
): number {
const justifiedCheckpoint =
data.target.epoch === currentEpoch ? rootCache.currentJustifiedCheckpoint : rootCache.previousJustifiedCheckpoint;
@@ -168,9 +201,33 @@ export function getAttestationParticipationStatus(
const isMatchingTarget = byteArrayEquals(data.target.root, rootCache.getBlockRoot(data.target.epoch));
// a timely head is only be set if the target is _also_ matching
const isMatchingHead =
// In gloas, this is called `head_root_matches`
let isMatchingHead =
isMatchingTarget && byteArrayEquals(data.beaconBlockRoot, rootCache.getBlockRootAtSlot(data.slot));
if (fork >= ForkSeq.gloas) {
let isMatchingPayload = false;
if (isAttestationSameSlotRootCache(rootCache, data)) {
if (data.index !== 0) {
throw new Error("Attesting same slot must indicate empty payload");
}
isMatchingPayload = true;
} else {
if (executionPayloadAvailability === null) {
throw new Error("Must supply executionPayloadAvailability post-gloas");
}
if (data.index !== 0 && data.index !== 1) {
throw new Error(`data index must be 0 or 1 index=${data.index}`);
}
isMatchingPayload = Boolean(data.index) === executionPayloadAvailability[data.slot % SLOTS_PER_HISTORICAL_ROOT];
}
isMatchingHead = isMatchingHead && isMatchingPayload;
}
let flags = 0;
if (isMatchingSource && inclusionDelay <= SLOTS_PER_EPOCH_SQRT) flags |= TIMELY_SOURCE;
if (isMatchingTarget && isTimelyTarget(fork, inclusionDelay)) flags |= TIMELY_TARGET;

View File

@@ -1,6 +1,6 @@
import {FAR_FUTURE_EPOCH, MIN_ACTIVATION_BALANCE, PENDING_CONSOLIDATIONS_LIMIT} from "@lodestar/params";
import {FAR_FUTURE_EPOCH, ForkSeq, MIN_ACTIVATION_BALANCE, PENDING_CONSOLIDATIONS_LIMIT} from "@lodestar/params";
import {electra, ssz} from "@lodestar/types";
import {CachedBeaconStateElectra} from "../types.js";
import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
import {hasEth1WithdrawalCredential} from "../util/capella.js";
import {
hasCompoundingWithdrawalCredential,
@@ -13,7 +13,8 @@ import {getConsolidationChurnLimit, getPendingBalanceToWithdraw, isActiveValidat
// TODO Electra: Clean up necessary as there is a lot of overlap with isValidSwitchToCompoundRequest
export function processConsolidationRequest(
state: CachedBeaconStateElectra,
fork: ForkSeq,
state: CachedBeaconStateElectra | CachedBeaconStateGloas,
consolidationRequest: electra.ConsolidationRequest
): void {
const {sourcePubkey, targetPubkey, sourceAddress} = consolidationRequest;
@@ -82,7 +83,7 @@ export function processConsolidationRequest(
}
// Verify the source has no pending withdrawals in the queue
if (getPendingBalanceToWithdraw(state, sourceIndex) > 0) {
if (getPendingBalanceToWithdraw(fork, state, sourceIndex) > 0) {
return;
}
@@ -103,7 +104,7 @@ export function processConsolidationRequest(
* Determine if we should set consolidation target validator to compounding credential
*/
function isValidSwitchToCompoundRequest(
state: CachedBeaconStateElectra,
state: CachedBeaconStateElectra | CachedBeaconStateGloas,
consolidationRequest: electra.ConsolidationRequest
): boolean {
const {sourcePubkey, targetPubkey, sourceAddress} = consolidationRequest;

View File

@@ -1,8 +1,11 @@
import {UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params";
import {electra, ssz} from "@lodestar/types";
import {CachedBeaconStateElectra} from "../types.js";
import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
export function processDepositRequest(state: CachedBeaconStateElectra, depositRequest: electra.DepositRequest): void {
export function processDepositRequest(
state: CachedBeaconStateElectra | CachedBeaconStateGloas,
depositRequest: electra.DepositRequest
): void {
if (state.depositRequestsStartIndex === UNSET_DEPOSIT_REQUESTS_START_INDEX) {
state.depositRequestsStartIndex = depositRequest.index;
}

View File

@@ -0,0 +1,120 @@
import {PublicKey, Signature, verify} from "@chainsafe/blst";
import {byteArrayEquals} from "@chainsafe/ssz";
import {
DOMAIN_BEACON_BUILDER,
FAR_FUTURE_EPOCH,
ForkPostGloas,
MIN_ACTIVATION_BALANCE,
SLOTS_PER_EPOCH,
} from "@lodestar/params";
import {BeaconBlock, gloas, ssz} from "@lodestar/types";
import {toHex, toRootHex} from "@lodestar/utils";
import {G2_POINT_AT_INFINITY} from "../constants/constants.ts";
import {CachedBeaconStateGloas} from "../types.ts";
import {hasBuilderWithdrawalCredential} from "../util/gloas.ts";
import {computeSigningRoot, getCurrentEpoch, getRandaoMix, isActiveValidator} from "../util/index.ts";
export function processExecutionPayloadBid(state: CachedBeaconStateGloas, block: BeaconBlock<ForkPostGloas>): void {
const signedBid = block.body.signedExecutionPayloadBid;
const bid = signedBid.message;
const {builderIndex, value: amount} = bid;
const builder = state.validators.getReadonly(builderIndex);
// For self-builds, amount must be zero regardless of withdrawal credential prefix
if (builderIndex === block.proposerIndex) {
if (amount !== 0) {
throw Error(`Invalid execution payload bid: self-build with non-zero amount ${amount}`);
}
if (!byteArrayEquals(signedBid.signature, G2_POINT_AT_INFINITY)) {
throw Error("Invalid execution payload bid: self-build with non-zero signature");
}
// Non-self builds require builder withdrawal credential
} else {
if (!hasBuilderWithdrawalCredential(builder.withdrawalCredentials)) {
throw Error(`Invalid execution payload bid: builder ${builderIndex} does not have builder withdrawal credential`);
}
if (!verifyExecutionPayloadBidSignature(state, builder.pubkey, signedBid)) {
throw Error(`Invalid execution payload bid: invalid signature for builder ${builderIndex}`);
}
}
if (!isActiveValidator(builder, getCurrentEpoch(state))) {
throw Error(`Invalid execution payload bid: builder ${builderIndex} is not active`);
}
if (builder.slashed) {
throw Error(`Invalid execution payload bid: builder ${builderIndex} is slashed`);
}
const pendingPayments = state.builderPendingPayments
.getAllReadonly()
.filter((payment) => payment.withdrawal.builderIndex === builderIndex)
.reduce((acc, payment) => acc + payment.withdrawal.amount, 0);
const pendingWithdrawals = state.builderPendingWithdrawals
.getAllReadonly()
.filter((withdrawal) => withdrawal.builderIndex === builderIndex)
.reduce((acc, withdrawal) => acc + withdrawal.amount, 0);
if (
amount !== 0 &&
state.balances.get(builderIndex) < amount + pendingPayments + pendingWithdrawals + MIN_ACTIVATION_BALANCE
) {
throw Error("Insufficient builder balance");
}
if (bid.slot !== block.slot) {
throw Error(`Bid slot ${bid.slot} does not match block slot ${block.slot}`);
}
if (!byteArrayEquals(bid.parentBlockHash, state.latestBlockHash)) {
throw Error(
`Parent block hash ${toRootHex(bid.parentBlockHash)} of bid does not match state's latest block hash ${toRootHex(state.latestBlockHash)}`
);
}
if (!byteArrayEquals(bid.parentBlockRoot, block.parentRoot)) {
throw Error(
`Parent block root ${toRootHex(bid.parentBlockRoot)} of bid does not match block's parent root ${toRootHex(block.parentRoot)}`
);
}
const stateRandao = getRandaoMix(state, getCurrentEpoch(state));
if (!byteArrayEquals(bid.prevRandao, stateRandao)) {
throw Error(`Prev randao ${toHex(bid.prevRandao)} of bid does not match state's randao mix ${toHex(stateRandao)}`);
}
if (amount > 0) {
const pendingPaymentView = ssz.gloas.BuilderPendingPayment.toViewDU({
weight: 0,
withdrawal: ssz.gloas.BuilderPendingWithdrawal.toViewDU({
feeRecipient: bid.feeRecipient,
amount,
builderIndex,
withdrawableEpoch: FAR_FUTURE_EPOCH,
}),
});
state.builderPendingPayments.set(SLOTS_PER_EPOCH + (bid.slot % SLOTS_PER_EPOCH), pendingPaymentView);
}
state.latestExecutionPayloadBid = ssz.gloas.ExecutionPayloadBid.toViewDU(bid);
}
function verifyExecutionPayloadBidSignature(
state: CachedBeaconStateGloas,
pubkey: Uint8Array,
signedBid: gloas.SignedExecutionPayloadBid
): boolean {
const domain = state.config.getDomain(state.slot, DOMAIN_BEACON_BUILDER);
const signingRoot = computeSigningRoot(ssz.gloas.ExecutionPayloadBid, signedBid.message, domain);
try {
const publicKey = PublicKey.fromBytes(pubkey);
const signature = Signature.fromBytes(signedBid.signature, true);
return verify(signingRoot, publicKey, signature);
} catch (_e) {
return false; // Catch all BLS errors: failed key validation, failed signature validation, invalid signature
}
}

View File

@@ -0,0 +1,181 @@
import {PublicKey, Signature, verify} from "@chainsafe/blst";
import {byteArrayEquals} from "@chainsafe/ssz";
import {DOMAIN_BEACON_BUILDER, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
import {gloas, ssz} from "@lodestar/types";
import {toHex, toRootHex} from "@lodestar/utils";
import {CachedBeaconStateGloas} from "../types.ts";
import {computeExitEpochAndUpdateChurn, computeSigningRoot, computeTimeAtSlot} from "../util/index.ts";
import {processConsolidationRequest} from "./processConsolidationRequest.ts";
import {processDepositRequest} from "./processDepositRequest.ts";
import {processWithdrawalRequest} from "./processWithdrawalRequest.ts";
// This function does not call execution engine to verify payload. Need to call it from other place
export function processExecutionPayloadEnvelope(
state: CachedBeaconStateGloas,
signedEnvelope: gloas.SignedExecutionPayloadEnvelope,
verify: boolean
): void {
const envelope = signedEnvelope.message;
const payload = envelope.payload;
const fork = state.config.getForkSeq(envelope.slot);
if (verify) {
const builderIndex = envelope.builderIndex;
const pubkey = state.validators.getReadonly(builderIndex).pubkey;
if (!verifyExecutionPayloadEnvelopeSignature(state, pubkey, signedEnvelope)) {
throw new Error("Payload Envelope has invalid signature");
}
}
validateExecutionPayloadEnvelope(state, envelope);
const requests = envelope.executionRequests;
for (const deposit of requests.deposits) {
processDepositRequest(state, deposit);
}
for (const withdrawal of requests.withdrawals) {
processWithdrawalRequest(fork, state, withdrawal);
}
for (const consolidation of requests.consolidations) {
processConsolidationRequest(fork, state, consolidation);
}
// Queue the builder payment
const paymentIndex = SLOTS_PER_EPOCH + (state.slot % SLOTS_PER_EPOCH);
const payment = state.builderPendingPayments.get(paymentIndex).clone();
const amount = payment.withdrawal.amount;
if (amount > 0) {
const exitQueueEpoch = computeExitEpochAndUpdateChurn(state, BigInt(amount));
payment.withdrawal.withdrawableEpoch = exitQueueEpoch + state.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY;
state.builderPendingWithdrawals.push(payment.withdrawal);
}
state.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU());
// Cache the execution payload hash
state.executionPayloadAvailability.set(state.slot % SLOTS_PER_HISTORICAL_ROOT, true);
state.latestBlockHash = payload.blockHash;
if (verify && !byteArrayEquals(envelope.stateRoot, state.hashTreeRoot())) {
throw new Error(
`Envelope's state root does not match state envelope=${toRootHex(envelope.stateRoot)} state=${toRootHex(state.hashTreeRoot())}`
);
}
}
function validateExecutionPayloadEnvelope(
state: CachedBeaconStateGloas,
envelope: gloas.ExecutionPayloadEnvelope
): void {
const payload = envelope.payload;
if (byteArrayEquals(state.latestBlockHeader.stateRoot, ssz.Root.defaultValue())) {
const previousStateRoot = state.hashTreeRoot();
state.latestBlockHeader.stateRoot = previousStateRoot;
}
// Verify consistency with the beacon block
if (!byteArrayEquals(envelope.beaconBlockRoot, state.latestBlockHeader.hashTreeRoot())) {
throw new Error(
`Envelope's block is not the latest block header envelope=${toRootHex(envelope.beaconBlockRoot)} latestBlockHeader=${toRootHex(state.latestBlockHeader.hashTreeRoot())}`
);
}
// Verify consistency with the beacon block
if (envelope.slot !== state.slot) {
throw new Error(`Slot mismatch between envelope and state envelope=${envelope.slot} state=${state.slot}`);
}
const committedBid = state.latestExecutionPayloadBid;
// Verify consistency with the committed bid
if (envelope.builderIndex !== committedBid.builderIndex) {
throw new Error(
`Builder index mismatch between envelope and committed bid envelope=${envelope.builderIndex} committedBid=${committedBid.builderIndex}`
);
}
// Verify consistency with the committed bid
const envelopeKzgRoot = ssz.deneb.BlobKzgCommitments.hashTreeRoot(envelope.blobKzgCommitments);
if (!byteArrayEquals(committedBid.blobKzgCommitmentsRoot, envelopeKzgRoot)) {
throw new Error(
`Kzg commitment root mismatch between envelope and committed bid envelope=${toRootHex(envelopeKzgRoot)} committedBid=${toRootHex(committedBid.blobKzgCommitmentsRoot)}`
);
}
// Verify the withdrawals root
const envelopeWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(envelope.payload.withdrawals);
if (!byteArrayEquals(state.latestWithdrawalsRoot, envelopeWithdrawalsRoot)) {
throw new Error(
`Withdrawals root mismatch between envelope and latest withdrawals root envelope=${toRootHex(envelopeWithdrawalsRoot)} latestWithdrawalRoot=${toRootHex(state.latestWithdrawalsRoot)}`
);
}
// Verify the gas_limit
if (Number(committedBid.gasLimit) !== payload.gasLimit) {
throw new Error(
`Gas limit mismatch between envelope's payload and committed bid envelope=${payload.gasLimit} committedBid=${Number(committedBid.gasLimit)}`
);
}
// Verify the block hash
if (!byteArrayEquals(committedBid.blockHash, payload.blockHash)) {
throw new Error(
`Block hash mismatch between envelope's payload and committed bid envelope=${toRootHex(payload.blockHash)} committedBid=${toRootHex(committedBid.blockHash)}`
);
}
// Verify consistency of the parent hash with respect to the previous execution payload
if (!byteArrayEquals(payload.parentHash, state.latestBlockHash)) {
throw new Error(
`Parent hash mismatch between envelope's payload and state envelope=${toRootHex(payload.parentHash)} state=${toRootHex(state.latestBlockHash)}`
);
}
// Verify prev_randao matches committed bid
if (!byteArrayEquals(committedBid.prevRandao, payload.prevRandao)) {
throw new Error(
`Prev randao mismatch between committed bid and payload committedBid=${toHex(committedBid.prevRandao)} payload=${toHex(payload.prevRandao)}`
);
}
// Verify timestamp
if (payload.timestamp !== computeTimeAtSlot(state.config, state.slot, state.genesisTime)) {
throw new Error(
`Timestamp mismatch between envelope's payload and state envelope=${payload.timestamp} state=${computeTimeAtSlot(state.config, state.slot, state.genesisTime)}`
);
}
// Verify commitments are under limit
const maxBlobsPerBlock = state.config.getMaxBlobsPerBlock(state.epochCtx.epoch);
if (envelope.blobKzgCommitments.length > maxBlobsPerBlock) {
throw new Error(
`Kzg commitments exceed limit commitment.length=${envelope.blobKzgCommitments.length} limit=${maxBlobsPerBlock}`
);
}
// Skipped: Verify the execution payload is valid
}
function verifyExecutionPayloadEnvelopeSignature(
state: CachedBeaconStateGloas,
pubkey: Uint8Array,
signedEnvelope: gloas.SignedExecutionPayloadEnvelope
): boolean {
const domain = state.config.getDomain(state.slot, DOMAIN_BEACON_BUILDER);
const signingRoot = computeSigningRoot(ssz.gloas.ExecutionPayloadEnvelope, signedEnvelope.message, domain);
try {
const publicKey = PublicKey.fromBytes(pubkey);
const signature = Signature.fromBytes(signedEnvelope.signature, true);
return verify(signingRoot, publicKey, signature);
} catch (_e) {
return false; // Catch all BLS errors: failed key validation, failed signature validation, invalid signature
}
}

View File

@@ -1,7 +1,12 @@
import {ForkSeq} from "@lodestar/params";
import {BeaconBlockBody, capella, electra} from "@lodestar/types";
import {BeaconBlockBody, capella, electra, gloas} from "@lodestar/types";
import {BeaconStateTransitionMetrics} from "../metrics.js";
import {CachedBeaconStateAllForks, CachedBeaconStateCapella, CachedBeaconStateElectra} from "../types.js";
import {
CachedBeaconStateAllForks,
CachedBeaconStateCapella,
CachedBeaconStateElectra,
CachedBeaconStateGloas,
} from "../types.js";
import {getEth1DepositCount} from "../util/deposit.js";
import {processAttestations} from "./processAttestations.js";
import {processAttesterSlashing} from "./processAttesterSlashing.js";
@@ -9,6 +14,7 @@ import {processBlsToExecutionChange} from "./processBlsToExecutionChange.js";
import {processConsolidationRequest} from "./processConsolidationRequest.js";
import {processDeposit} from "./processDeposit.js";
import {processDepositRequest} from "./processDepositRequest.js";
import {processPayloadAttestation} from "./processPayloadAttestation.ts";
import {processProposerSlashing} from "./processProposerSlashing.js";
import {processVoluntaryExit} from "./processVoluntaryExit.js";
import {processWithdrawalRequest} from "./processWithdrawalRequest.js";
@@ -64,7 +70,7 @@ export function processOperations(
}
}
if (fork >= ForkSeq.electra) {
if (fork >= ForkSeq.electra && fork < ForkSeq.gloas) {
const stateElectra = state as CachedBeaconStateElectra;
const bodyElectra = body as electra.BeaconBlockBody;
@@ -77,7 +83,13 @@ export function processOperations(
}
for (const elConsolidationRequest of bodyElectra.executionRequests.consolidations) {
processConsolidationRequest(stateElectra, elConsolidationRequest);
processConsolidationRequest(fork, stateElectra, elConsolidationRequest);
}
}
if (fork >= ForkSeq.gloas) {
for (const payloadAttestation of (body as gloas.BeaconBlockBody).payloadAttestations) {
processPayloadAttestation(state as CachedBeaconStateGloas, payloadAttestation);
}
}
}

View File

@@ -0,0 +1,25 @@
import {byteArrayEquals} from "@chainsafe/ssz";
import {gloas} from "@lodestar/types";
import {CachedBeaconStateGloas} from "../types.ts";
import {isValidIndexedPayloadAttestation} from "./isValidIndexedPayloadAttestation.ts";
export function processPayloadAttestation(
state: CachedBeaconStateGloas,
payloadAttestation: gloas.PayloadAttestation
): void {
const data = payloadAttestation.data;
if (!byteArrayEquals(data.beaconBlockRoot, state.latestBlockHeader.parentRoot)) {
throw Error("Payload attestation is referring to the wrong block");
}
if (data.slot + 1 !== state.slot) {
throw Error("Payload attestation is not from previous slot");
}
const indexedPayloadAttestation = state.epochCtx.getIndexedPayloadAttestation(data.slot, payloadAttestation);
if (!isValidIndexedPayloadAttestation(state, indexedPayloadAttestation, true)) {
throw Error("Invalid payload attestation");
}
}

View File

@@ -1,8 +1,8 @@
import {ForkSeq} from "@lodestar/params";
import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params";
import {phase0, ssz} from "@lodestar/types";
import {getProposerSlashingSignatureSets} from "../signatureSets/index.js";
import {CachedBeaconStateAllForks} from "../types.js";
import {isSlashableValidator} from "../util/index.js";
import {CachedBeaconStateAllForks, CachedBeaconStateGloas} from "../types.js";
import {computeEpochAtSlot, isSlashableValidator} from "../util/index.js";
import {verifySignatureSet} from "../util/signatureSets.js";
import {slashValidator} from "./slashValidator.js";
@@ -20,6 +20,27 @@ export function processProposerSlashing(
): void {
assertValidProposerSlashing(state, proposerSlashing, verifySignatures);
if (fork >= ForkSeq.gloas) {
const slot = Number(proposerSlashing.signedHeader1.message.slot);
const proposalEpoch = computeEpochAtSlot(slot);
const currentEpoch = state.epochCtx.epoch;
const previousEpoch = currentEpoch - 1;
const paymentIndex =
proposalEpoch === currentEpoch
? SLOTS_PER_EPOCH + (slot % SLOTS_PER_EPOCH)
: proposalEpoch === previousEpoch
? slot % SLOTS_PER_EPOCH
: undefined;
if (paymentIndex !== undefined) {
(state as CachedBeaconStateGloas).builderPendingPayments.set(
paymentIndex,
ssz.gloas.BuilderPendingPayment.defaultViewDU()
);
}
}
slashValidator(fork, state, proposerSlashing.signedHeader1.message.proposerIndex);
}

View File

@@ -69,7 +69,7 @@ export function getVoluntaryExitValidity(
// only exit validator if it has no pending withdrawals in the queue
if (
fork >= ForkSeq.electra &&
getPendingBalanceToWithdraw(state as CachedBeaconStateElectra, voluntaryExit.validatorIndex) !== 0
getPendingBalanceToWithdraw(fork, state as CachedBeaconStateElectra, voluntaryExit.validatorIndex) !== 0
) {
return VoluntaryExitValidity.pendingWithdrawals;
}

View File

@@ -7,7 +7,7 @@ import {
} from "@lodestar/params";
import {electra, phase0, ssz} from "@lodestar/types";
import {toHex} from "@lodestar/utils";
import {CachedBeaconStateElectra} from "../types.js";
import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
import {hasCompoundingWithdrawalCredential, hasExecutionWithdrawalCredential} from "../util/electra.js";
import {computeExitEpochAndUpdateChurn} from "../util/epoch.js";
import {getPendingBalanceToWithdraw, isActiveValidator} from "../util/validator.js";
@@ -15,7 +15,7 @@ import {initiateValidatorExit} from "./initiateValidatorExit.js";
export function processWithdrawalRequest(
fork: ForkSeq,
state: CachedBeaconStateElectra,
state: CachedBeaconStateElectra | CachedBeaconStateGloas,
withdrawalRequest: electra.WithdrawalRequest
): void {
const amount = Number(withdrawalRequest.amount);
@@ -42,7 +42,7 @@ export function processWithdrawalRequest(
}
// TODO Electra: Consider caching pendingPartialWithdrawals
const pendingBalanceToWithdraw = getPendingBalanceToWithdraw(state, validatorIndex);
const pendingBalanceToWithdraw = getPendingBalanceToWithdraw(fork, state, validatorIndex);
const validatorBalance = state.balances.get(validatorIndex);
if (isFullExitRequest) {
@@ -81,7 +81,7 @@ export function processWithdrawalRequest(
function isValidatorEligibleForWithdrawOrExit(
validator: phase0.Validator,
sourceAddress: Uint8Array,
state: CachedBeaconStateElectra
state: CachedBeaconStateElectra | CachedBeaconStateGloas
): boolean {
const {withdrawalCredentials} = validator;
const addressStr = toHex(withdrawalCredentials.subarray(12));

View File

@@ -10,7 +10,8 @@ import {
} from "@lodestar/params";
import {ValidatorIndex, capella, ssz} from "@lodestar/types";
import {MapDef, toRootHex} from "@lodestar/utils";
import {CachedBeaconStateCapella, CachedBeaconStateElectra} from "../types.js";
import {CachedBeaconStateCapella, CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
import {isBuilderPaymentWithdrawable, isParentBlockFull} from "../util/gloas.ts";
import {
decreaseBalance,
getMaxEffectiveBalance,
@@ -21,31 +22,48 @@ import {
export function processWithdrawals(
fork: ForkSeq,
state: CachedBeaconStateCapella | CachedBeaconStateElectra,
payload: capella.FullOrBlindedExecutionPayload
state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas,
payload?: capella.FullOrBlindedExecutionPayload
): void {
// Return early if the parent block is empty
if (fork >= ForkSeq.gloas && !isParentBlockFull(state as CachedBeaconStateGloas)) {
return;
}
// processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002)
const {withdrawals: expectedWithdrawals, processedPartialWithdrawalsCount} = getExpectedWithdrawals(fork, state);
// processedBuilderWithdrawalsCount is withdrawals coming from builder payment since gloas (EIP-7732)
const {
withdrawals: expectedWithdrawals,
processedPartialWithdrawalsCount,
processedBuilderWithdrawalsCount,
} = getExpectedWithdrawals(fork, state);
const numWithdrawals = expectedWithdrawals.length;
if (isCapellaPayloadHeader(payload)) {
const expectedWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(expectedWithdrawals);
const actualWithdrawalsRoot = payload.withdrawalsRoot;
if (!byteArrayEquals(expectedWithdrawalsRoot, actualWithdrawalsRoot)) {
throw Error(
`Invalid withdrawalsRoot of executionPayloadHeader, expected=${toRootHex(
expectedWithdrawalsRoot
)}, actual=${toRootHex(actualWithdrawalsRoot)}`
);
// After gloas, withdrawals are verified later in processExecutionPayloadEnvelope
if (fork < ForkSeq.gloas) {
if (payload === undefined) {
throw Error("payload is required for pre-gloas processWithdrawals");
}
} else {
if (expectedWithdrawals.length !== payload.withdrawals.length) {
throw Error(`Invalid withdrawals length expected=${numWithdrawals} actual=${payload.withdrawals.length}`);
}
for (let i = 0; i < numWithdrawals; i++) {
const withdrawal = expectedWithdrawals[i];
if (!ssz.capella.Withdrawal.equals(withdrawal, payload.withdrawals[i])) {
throw Error(`Withdrawal mismatch at index=${i}`);
if (isCapellaPayloadHeader(payload)) {
const expectedWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(expectedWithdrawals);
const actualWithdrawalsRoot = payload.withdrawalsRoot;
if (!byteArrayEquals(expectedWithdrawalsRoot, actualWithdrawalsRoot)) {
throw Error(
`Invalid withdrawalsRoot of executionPayloadHeader, expected=${toRootHex(
expectedWithdrawalsRoot
)}, actual=${toRootHex(actualWithdrawalsRoot)}`
);
}
} else {
if (expectedWithdrawals.length !== payload.withdrawals.length) {
throw Error(`Invalid withdrawals length expected=${numWithdrawals} actual=${payload.withdrawals.length}`);
}
for (let i = 0; i < numWithdrawals; i++) {
const withdrawal = expectedWithdrawals[i];
if (!ssz.capella.Withdrawal.equals(withdrawal, payload.withdrawals[i])) {
throw Error(`Withdrawal mismatch at index=${i}`);
}
}
}
}
@@ -62,6 +80,24 @@ export function processWithdrawals(
);
}
if (fork >= ForkSeq.gloas) {
const stateGloas = state as CachedBeaconStateGloas;
stateGloas.latestWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(expectedWithdrawals);
const unprocessedWithdrawals = stateGloas.builderPendingWithdrawals
.getAllReadonly()
.slice(0, processedBuilderWithdrawalsCount)
.filter((w) => !isBuilderPaymentWithdrawable(stateGloas, w));
const remainingWithdrawals = stateGloas.builderPendingWithdrawals
.sliceFrom(processedBuilderWithdrawalsCount)
.getAllReadonly();
stateGloas.builderPendingWithdrawals = ssz.gloas.BeaconState.fields.builderPendingWithdrawals.toViewDU([
...unprocessedWithdrawals,
...remainingWithdrawals,
]);
}
// Update the nextWithdrawalIndex
const latestWithdrawal = expectedWithdrawals.at(-1);
if (latestWithdrawal) {
@@ -82,11 +118,12 @@ export function processWithdrawals(
export function getExpectedWithdrawals(
fork: ForkSeq,
state: CachedBeaconStateCapella | CachedBeaconStateElectra
state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas
): {
withdrawals: capella.Withdrawal[];
sampledValidators: number;
processedPartialWithdrawalsCount: number;
processedBuilderWithdrawalsCount: number;
} {
if (fork < ForkSeq.capella) {
throw new Error(`getExpectedWithdrawals not supported at forkSeq=${fork} < ForkSeq.capella`);
@@ -99,17 +136,71 @@ export function getExpectedWithdrawals(
const withdrawals: capella.Withdrawal[] = [];
const withdrawnBalances = new MapDef<ValidatorIndex, number>(() => 0);
const isPostElectra = fork >= ForkSeq.electra;
const isPostGloas = fork >= ForkSeq.gloas;
// partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002)
let processedPartialWithdrawalsCount = 0;
// builderWithdrawalsCount is withdrawals coming from builder payments since Gloas (EIP-7732)
let processedBuilderWithdrawalsCount = 0;
if (isPostGloas) {
const stateGloas = state as CachedBeaconStateGloas;
const allBuilderPendingWithdrawals =
stateGloas.builderPendingWithdrawals.length <= MAX_WITHDRAWALS_PER_PAYLOAD
? stateGloas.builderPendingWithdrawals.getAllReadonly()
: null;
for (let i = 0; i < stateGloas.builderPendingWithdrawals.length; i++) {
const withdrawal = allBuilderPendingWithdrawals
? allBuilderPendingWithdrawals[i]
: stateGloas.builderPendingWithdrawals.getReadonly(i);
if (withdrawal.withdrawableEpoch > epoch || withdrawals.length + 1 === MAX_WITHDRAWALS_PER_PAYLOAD) {
break;
}
if (isBuilderPaymentWithdrawable(stateGloas, withdrawal)) {
const totalWithdrawn = withdrawnBalances.getOrDefault(withdrawal.builderIndex);
const balance = state.balances.get(withdrawal.builderIndex) - totalWithdrawn;
const builder = state.validators.get(withdrawal.builderIndex);
let withdrawableBalance = 0;
if (builder.slashed) {
withdrawableBalance = balance < withdrawal.amount ? balance : withdrawal.amount;
} else if (balance > MIN_ACTIVATION_BALANCE) {
withdrawableBalance =
balance - MIN_ACTIVATION_BALANCE < withdrawal.amount ? balance - MIN_ACTIVATION_BALANCE : withdrawal.amount;
}
if (withdrawableBalance > 0) {
withdrawals.push({
index: withdrawalIndex,
validatorIndex: withdrawal.builderIndex,
address: withdrawal.feeRecipient,
amount: BigInt(withdrawableBalance),
});
withdrawalIndex++;
withdrawnBalances.set(withdrawal.builderIndex, totalWithdrawn + withdrawableBalance);
}
}
processedBuilderWithdrawalsCount++;
}
}
if (isPostElectra) {
// In pre-gloas, partialWithdrawalBound == MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP
const partialWithdrawalBound = Math.min(
withdrawals.length + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP,
MAX_WITHDRAWALS_PER_PAYLOAD - 1
);
const stateElectra = state as CachedBeaconStateElectra;
// MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP = 8, PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728 so we should only call getAllReadonly() if it makes sense
// pendingPartialWithdrawals comes from EIP-7002 smart contract where it takes fee so it's more likely than not validator is in correct condition to withdraw
// also we may break early if withdrawableEpoch > epoch
const allPendingPartialWithdrawals =
stateElectra.pendingPartialWithdrawals.length <= MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP
stateElectra.pendingPartialWithdrawals.length <= partialWithdrawalBound
? stateElectra.pendingPartialWithdrawals.getAllReadonly()
: null;
@@ -118,7 +209,7 @@ export function getExpectedWithdrawals(
const withdrawal = allPendingPartialWithdrawals
? allPendingPartialWithdrawals[i]
: stateElectra.pendingPartialWithdrawals.getReadonly(i);
if (withdrawal.withdrawableEpoch > epoch || withdrawals.length === MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP) {
if (withdrawal.withdrawableEpoch > epoch || withdrawals.length === partialWithdrawalBound) {
break;
}
@@ -147,11 +238,11 @@ export function getExpectedWithdrawals(
}
}
const bound = Math.min(validators.length, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP);
const withdrawalBound = Math.min(validators.length, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP);
let n = 0;
// Just run a bounded loop max iterating over all withdrawals
// however breaks out once we have MAX_WITHDRAWALS_PER_PAYLOAD
for (n = 0; n < bound; n++) {
for (n = 0; n < withdrawalBound; n++) {
// Get next validator in turn
const validatorIndex = (nextWithdrawalValidatorIndex + n) % validators.length;
@@ -203,5 +294,5 @@ export function getExpectedWithdrawals(
}
}
return {withdrawals, sampledValidators: n, processedPartialWithdrawalsCount};
return {withdrawals, sampledValidators: n, processedPartialWithdrawalsCount, processedBuilderWithdrawalsCount};
}

View File

@@ -24,6 +24,7 @@ import {
SyncPeriod,
ValidatorIndex,
electra,
gloas,
phase0,
} from "@lodestar/types";
import {LodestarError} from "@lodestar/utils";
@@ -46,6 +47,7 @@ import {
getSeed,
isActiveValidator,
isAggregatorFromCommitteeLength,
naiveGetPayloadTimlinessCommitteeIndices,
} from "../util/index.js";
import {computeBaseRewardPerIncrement, computeSyncParticipantReward} from "../util/syncCommittee.js";
import {sumTargetUnslashedBalanceIncrements} from "../util/targetUnslashedBalance.js";
@@ -59,7 +61,7 @@ import {
computeSyncCommitteeCache,
getSyncCommitteeCache,
} from "./syncCommitteeCache.js";
import {BeaconStateAllForks, BeaconStateAltair} from "./types.js";
import {BeaconStateAllForks, BeaconStateAltair, BeaconStateGloas} from "./types.js";
/** `= PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT)` */
export const PROPOSER_WEIGHT_FACTOR = PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT);
@@ -238,6 +240,10 @@ export class EpochCache {
/** TODO: Indexed SyncCommitteeCache */
nextSyncCommitteeIndexed: SyncCommitteeCache;
// TODO GLOAS: See if we need to cached PTC for prev/next epoch
// PTC for current epoch
payloadTimelinessCommittee: ValidatorIndex[][];
// TODO: Helper stats
syncPeriod: SyncPeriod;
@@ -276,6 +282,7 @@ export class EpochCache {
previousTargetUnslashedBalanceIncrements: number;
currentSyncCommitteeIndexed: SyncCommitteeCache;
nextSyncCommitteeIndexed: SyncCommitteeCache;
payloadTimelinessCommittee: ValidatorIndex[][];
epoch: Epoch;
syncPeriod: SyncPeriod;
}) {
@@ -307,6 +314,7 @@ export class EpochCache {
this.previousTargetUnslashedBalanceIncrements = data.previousTargetUnslashedBalanceIncrements;
this.currentSyncCommitteeIndexed = data.currentSyncCommitteeIndexed;
this.nextSyncCommitteeIndexed = data.nextSyncCommitteeIndexed;
this.payloadTimelinessCommittee = data.payloadTimelinessCommittee;
this.epoch = data.epoch;
this.syncPeriod = data.syncPeriod;
}
@@ -485,6 +493,17 @@ export class EpochCache {
nextSyncCommitteeIndexed = new SyncCommitteeCacheEmpty();
}
// Compute PTC for this epoch
let payloadTimelinessCommittee: ValidatorIndex[][] = [];
if (currentEpoch >= config.GLOAS_FORK_EPOCH) {
payloadTimelinessCommittee = naiveGetPayloadTimlinessCommitteeIndices(
state as BeaconStateGloas,
currentShuffling,
effectiveBalanceIncrements,
currentEpoch
);
}
// Precompute churnLimit for efficient initiateValidatorExit() during block proposing MUST be recompute everytime the
// active validator indices set changes in size. Validators change active status only when:
// - validator.activation_epoch is set. Only changes in process_registry_updates() if validator can be activated. If
@@ -559,6 +578,7 @@ export class EpochCache {
currentTargetUnslashedBalanceIncrements,
currentSyncCommitteeIndexed,
nextSyncCommitteeIndexed,
payloadTimelinessCommittee: payloadTimelinessCommittee,
epoch: currentEpoch,
syncPeriod: computeSyncPeriodAtEpoch(currentEpoch),
});
@@ -605,6 +625,7 @@ export class EpochCache {
currentTargetUnslashedBalanceIncrements: this.currentTargetUnslashedBalanceIncrements,
currentSyncCommitteeIndexed: this.currentSyncCommitteeIndexed,
nextSyncCommitteeIndexed: this.nextSyncCommitteeIndexed,
payloadTimelinessCommittee: this.payloadTimelinessCommittee,
epoch: this.epoch,
syncPeriod: this.syncPeriod,
});
@@ -750,6 +771,14 @@ export class EpochCache {
const epochAfterUpcoming = upcomingEpoch + 1;
this.proposersPrevEpoch = this.proposers;
if (upcomingEpoch >= this.config.GLOAS_FORK_EPOCH) {
this.payloadTimelinessCommittee = naiveGetPayloadTimlinessCommitteeIndices(
state as BeaconStateGloas,
this.currentShuffling,
this.effectiveBalanceIncrements,
upcomingEpoch
);
}
if (upcomingEpoch >= this.config.FULU_FORK_EPOCH) {
// Populate proposer cache with lookahead from state
const proposerLookahead = (state as CachedBeaconStateFulu).proposerLookahead.getAll();
@@ -1151,6 +1180,34 @@ export class EpochCache {
isPostElectra(): boolean {
return this.epoch >= this.config.ELECTRA_FORK_EPOCH;
}
getPayloadTimelinessCommittee(slot: Slot): ValidatorIndex[] {
const epoch = computeEpochAtSlot(slot);
if (epoch < this.config.GLOAS_FORK_EPOCH) {
throw new Error("Payload Timeliness Committee is not available before gloas fork");
}
if (epoch === this.epoch) {
return this.payloadTimelinessCommittee[slot % SLOTS_PER_EPOCH];
}
throw new Error(`Payload Timeliness Committee is not available for slot=${slot}`);
}
getIndexedPayloadAttestation(
slot: Slot,
payloadAttestation: gloas.PayloadAttestation
): gloas.IndexedPayloadAttestation {
const payloadTimelinessCommittee = this.getPayloadTimelinessCommittee(slot);
const attestingIndices = payloadAttestation.aggregationBits.intersectValues(payloadTimelinessCommittee);
return {
attestingIndices: attestingIndices.sort((a, b) => a - b),
data: payloadAttestation.data,
signature: payloadAttestation.signature,
};
}
}
function getEffectiveBalanceIncrementsByteLen(validatorCount: number): number {

View File

@@ -12,9 +12,11 @@ import {
CachedBeaconStateCapella,
CachedBeaconStateElectra,
CachedBeaconStateFulu,
CachedBeaconStateGloas,
CachedBeaconStatePhase0,
EpochTransitionCache,
} from "../types.js";
import {processBuilderPendingPayments} from "./processBuilderPendingPayments.ts";
import {processEffectiveBalanceUpdates} from "./processEffectiveBalanceUpdates.js";
import {processEth1DataReset} from "./processEth1DataReset.js";
import {processHistoricalRootsUpdate} from "./processHistoricalRootsUpdate.js";
@@ -53,6 +55,7 @@ export {
processPendingDeposits,
processPendingConsolidations,
processProposerLookahead,
processBuilderPendingPayments,
};
export {computeUnrealizedCheckpoints} from "./computeUnrealizedCheckpoints.js";
@@ -78,6 +81,7 @@ export enum EpochTransitionStep {
processPendingDeposits = "processPendingDeposits",
processPendingConsolidations = "processPendingConsolidations",
processProposerLookahead = "processProposerLookahead",
processBuilderPendingPayments = "processBuilderPendingPayments",
}
export function processEpoch(
@@ -154,6 +158,14 @@ export function processEpoch(
}
}
if (fork >= ForkSeq.gloas) {
const timer = metrics?.epochTransitionStepTime.startTimer({
step: EpochTransitionStep.processBuilderPendingPayments,
});
processBuilderPendingPayments(state as CachedBeaconStateGloas);
timer?.();
}
{
const timer = metrics?.epochTransitionStepTime.startTimer({
step: EpochTransitionStep.processEffectiveBalanceUpdates,

View File

@@ -0,0 +1,31 @@
import {SLOTS_PER_EPOCH} from "@lodestar/params";
import {ssz} from "@lodestar/types";
import {CachedBeaconStateGloas} from "../types.ts";
import {computeExitEpochAndUpdateChurn} from "../util/epoch.ts";
import {getBuilderPaymentQuorumThreshold} from "../util/gloas.ts";
/**
* Processes the builder pending payments from the previous epoch.
*/
export function processBuilderPendingPayments(state: CachedBeaconStateGloas): void {
const quorum = getBuilderPaymentQuorumThreshold(state);
for (let i = 0; i < SLOTS_PER_EPOCH; i++) {
const payment = state.builderPendingPayments.get(i);
if (payment.weight > quorum) {
const exitQueueEpoch = computeExitEpochAndUpdateChurn(state, BigInt(payment.withdrawal.amount));
payment.withdrawal.withdrawableEpoch = exitQueueEpoch + state.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY;
state.builderPendingWithdrawals.push(payment.withdrawal);
}
}
// TODO GLOAS: Optimize this
for (let i = 0; i < state.builderPendingPayments.length; i++) {
if (i < SLOTS_PER_EPOCH) {
state.builderPendingPayments.set(i, state.builderPendingPayments.get(i + SLOTS_PER_EPOCH).clone());
} else {
state.builderPendingPayments.set(i, ssz.gloas.BuilderPendingPayment.defaultViewDU());
}
}
}

View File

@@ -52,6 +52,7 @@ export type {
BeaconStateElectra,
BeaconStateExecutions,
BeaconStateFulu,
BeaconStateGloas,
// Non-cached states
BeaconStatePhase0,
CachedBeaconStateAllForks,
@@ -62,6 +63,7 @@ export type {
CachedBeaconStateElectra,
CachedBeaconStateExecutions,
CachedBeaconStateFulu,
CachedBeaconStateGloas,
CachedBeaconStatePhase0,
} from "./types.js";
export * from "./util/index.js";

View File

@@ -14,6 +14,7 @@ import {getVoluntaryExitsSignatureSets} from "./voluntaryExits.js";
export * from "./attesterSlashings.js";
export * from "./blsToExecutionChange.js";
export * from "./indexedAttestation.js";
export * from "./indexedPayloadAttestation.ts";
export * from "./proposer.js";
export * from "./proposerSlashings.js";
export * from "./randao.js";

View File

@@ -0,0 +1,24 @@
import {DOMAIN_PTC_ATTESTER} from "@lodestar/params";
import {gloas, ssz} from "@lodestar/types";
import {CachedBeaconStateGloas} from "../types.ts";
import {ISignatureSet, computeSigningRoot, createAggregateSignatureSetFromComponents} from "../util/index.ts";
export function getIndexedPayloadAttestationSignatureSet(
state: CachedBeaconStateGloas,
indexedPayloadAttestation: gloas.IndexedPayloadAttestation
): ISignatureSet {
return createAggregateSignatureSetFromComponents(
indexedPayloadAttestation.attestingIndices.map((i) => state.epochCtx.index2pubkey[i]),
getPayloadAttestationDataSigningRoot(state, indexedPayloadAttestation.data),
indexedPayloadAttestation.signature
);
}
export function getPayloadAttestationDataSigningRoot(
state: CachedBeaconStateGloas,
data: gloas.PayloadAttestationData
): Uint8Array {
const domain = state.config.getDomain(state.slot, DOMAIN_PTC_ATTESTER);
return computeSigningRoot(ssz.gloas.PayloadAttestationData, data, domain);
}

View File

@@ -1,7 +1,7 @@
import {byteArrayEquals} from "@chainsafe/ssz";
import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
import {ForkSeq, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
import {ZERO_HASH} from "../constants/index.js";
import {CachedBeaconStateAllForks} from "../types.js";
import {CachedBeaconStateAllForks, CachedBeaconStateGloas} from "../types.js";
export {upgradeStateToAltair} from "./upgradeStateToAltair.js";
export {upgradeStateToBellatrix} from "./upgradeStateToBellatrix.js";
@@ -14,7 +14,7 @@ export {upgradeStateToGloas} from "./upgradeStateToGloas.js";
/**
* Dial state to next slot. Common for all forks
*/
export function processSlot(state: CachedBeaconStateAllForks): void {
export function processSlot(fork: ForkSeq, state: CachedBeaconStateAllForks): void {
// Cache state root
// Note: .hashTreeRoot() automatically commits() pending changes
const previousStateRoot = state.hashTreeRoot();
@@ -29,4 +29,12 @@ export function processSlot(state: CachedBeaconStateAllForks): void {
// Note: .hashTreeRoot() automatically commits() pending changes
const previousBlockRoot = state.latestBlockHeader.hashTreeRoot();
state.blockRoots.set(state.slot % SLOTS_PER_HISTORICAL_ROOT, previousBlockRoot);
if (fork >= ForkSeq.gloas) {
// Unset the next payload availability
(state as CachedBeaconStateGloas).executionPayloadAvailability.set(
(state.slot + 1) % SLOTS_PER_HISTORICAL_ROOT,
false
);
}
}

View File

@@ -135,7 +135,8 @@ function translateParticipation(
data,
attestation.inclusionDelay,
epochCtx.epoch,
rootCache
rootCache,
null
);
const committeeIndices = epochCtx.getBeaconCommittee(data.slot, data.index);

View File

@@ -1,24 +1,68 @@
import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
import {ssz} from "@lodestar/types";
import {getCachedBeaconState} from "../cache/stateCache.js";
import {CachedBeaconStateFulu, CachedBeaconStateGloas} from "../types.js";
/**
* Upgrade a state from Fulu to Gloas.
* TODO GLOAS: Implement this
*/
export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBeaconStateGloas {
const {config} = stateFulu;
const stateFuluNode = ssz.fulu.BeaconState.commitViewDU(stateFulu);
const stateGloasView = ssz.gloas.BeaconState.getViewDU(stateFuluNode);
ssz.fulu.BeaconState.commitViewDU(stateFulu);
const stateGloasCloned = stateFulu;
const stateGloas = getCachedBeaconState(stateGloasView, stateFulu);
const stateGloasView = ssz.gloas.BeaconState.defaultViewDU();
stateGloas.fork = ssz.phase0.Fork.toViewDU({
stateGloasView.genesisTime = stateGloasCloned.genesisTime;
stateGloasView.genesisValidatorsRoot = stateGloasCloned.genesisValidatorsRoot;
stateGloasView.slot = stateGloasCloned.slot;
stateGloasView.fork = ssz.phase0.Fork.toViewDU({
previousVersion: stateFulu.fork.currentVersion,
currentVersion: config.GLOAS_FORK_VERSION,
epoch: stateFulu.epochCtx.epoch,
});
stateGloasView.latestBlockHeader = stateGloasCloned.latestBlockHeader;
stateGloasView.blockRoots = stateGloasCloned.blockRoots;
stateGloasView.stateRoots = stateGloasCloned.stateRoots;
stateGloasView.historicalRoots = stateGloasCloned.historicalRoots;
stateGloasView.eth1Data = stateGloasCloned.eth1Data;
stateGloasView.eth1DataVotes = stateGloasCloned.eth1DataVotes;
stateGloasView.eth1DepositIndex = stateGloasCloned.eth1DepositIndex;
stateGloasView.validators = stateGloasCloned.validators;
stateGloasView.balances = stateGloasCloned.balances;
stateGloasView.randaoMixes = stateGloasCloned.randaoMixes;
stateGloasView.slashings = stateGloasCloned.slashings;
stateGloasView.previousEpochParticipation = stateGloasCloned.previousEpochParticipation;
stateGloasView.currentEpochParticipation = stateGloasCloned.currentEpochParticipation;
stateGloasView.justificationBits = stateGloasCloned.justificationBits;
stateGloasView.previousJustifiedCheckpoint = stateGloasCloned.previousJustifiedCheckpoint;
stateGloasView.currentJustifiedCheckpoint = stateGloasCloned.currentJustifiedCheckpoint;
stateGloasView.finalizedCheckpoint = stateGloasCloned.finalizedCheckpoint;
stateGloasView.inactivityScores = stateGloasCloned.inactivityScores;
stateGloasView.currentSyncCommittee = stateGloasCloned.currentSyncCommittee;
stateGloasView.nextSyncCommittee = stateGloasCloned.nextSyncCommittee;
stateGloasView.latestExecutionPayloadBid.blockHash = stateFulu.latestExecutionPayloadHeader.blockHash;
stateGloasView.nextWithdrawalIndex = stateGloasCloned.nextWithdrawalIndex;
stateGloasView.nextWithdrawalValidatorIndex = stateGloasCloned.nextWithdrawalValidatorIndex;
stateGloasView.historicalSummaries = stateGloasCloned.historicalSummaries;
stateGloasView.depositRequestsStartIndex = stateGloasCloned.depositRequestsStartIndex;
stateGloasView.depositBalanceToConsume = stateGloasCloned.depositBalanceToConsume;
stateGloasView.exitBalanceToConsume = stateGloasCloned.exitBalanceToConsume;
stateGloasView.earliestExitEpoch = stateGloasCloned.earliestExitEpoch;
stateGloasView.consolidationBalanceToConsume = stateGloasCloned.consolidationBalanceToConsume;
stateGloasView.earliestConsolidationEpoch = stateGloasCloned.earliestConsolidationEpoch;
stateGloasView.pendingDeposits = stateGloasCloned.pendingDeposits;
stateGloasView.pendingPartialWithdrawals = stateGloasCloned.pendingPartialWithdrawals;
stateGloasView.pendingConsolidations = stateGloasCloned.pendingConsolidations;
stateGloasView.proposerLookahead = stateGloasCloned.proposerLookahead;
for (let i = 0; i < SLOTS_PER_HISTORICAL_ROOT; i++) {
stateGloasView.executionPayloadAvailability.set(i, true);
}
stateGloasView.latestBlockHash = stateFulu.latestExecutionPayloadHeader.blockHash;
const stateGloas = getCachedBeaconState(stateGloasView, stateFulu);
stateGloas.commit();
// Clear cache to ensure the cache of fulu fields is not used by new gloas fields

View File

@@ -217,13 +217,13 @@ function processSlotsWithTransientCache(
}
while (postState.slot < slot) {
processSlot(postState);
const fork = postState.config.getForkSeq(postState.slot);
processSlot(fork, postState);
// Process epoch on the first slot of the next epoch
// We use `fork` because at fork boundary we don't want to process
// "next fork" epoch before upgrading state
if ((postState.slot + 1) % SLOTS_PER_EPOCH === 0) {
// At fork boundary we don't want to process "next fork" epoch before upgrading state
const fork = postState.config.getForkSeq(postState.slot);
const epochTransitionTimer = metrics?.epochTransitionTime.startTimer();
let epochTransitionCache: EpochTransitionCache;

View File

@@ -1,11 +1,14 @@
import {COMPOUNDING_WITHDRAWAL_PREFIX, GENESIS_SLOT, MIN_ACTIVATION_BALANCE} from "@lodestar/params";
import {ValidatorIndex, ssz} from "@lodestar/types";
import {G2_POINT_AT_INFINITY} from "../constants/constants.js";
import {CachedBeaconStateElectra} from "../types.js";
import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
import {hasEth1WithdrawalCredential} from "./capella.js";
import {hasBuilderWithdrawalCredential} from "./gloas.ts";
export function hasCompoundingWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean {
return withdrawalCredentials[0] === COMPOUNDING_WITHDRAWAL_PREFIX;
return (
withdrawalCredentials[0] === COMPOUNDING_WITHDRAWAL_PREFIX || hasBuilderWithdrawalCredential(withdrawalCredentials)
);
}
export function hasExecutionWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean {
@@ -14,7 +17,10 @@ export function hasExecutionWithdrawalCredential(withdrawalCredentials: Uint8Arr
);
}
export function switchToCompoundingValidator(state: CachedBeaconStateElectra, index: ValidatorIndex): void {
export function switchToCompoundingValidator(
state: CachedBeaconStateElectra | CachedBeaconStateGloas,
index: ValidatorIndex
): void {
const validator = state.validators.get(index);
// directly modifying the byte leads to ssz missing the modification resulting into
@@ -30,7 +36,10 @@ export function switchToCompoundingValidator(state: CachedBeaconStateElectra, in
queueExcessActiveBalance(state, index);
}
export function queueExcessActiveBalance(state: CachedBeaconStateElectra, index: ValidatorIndex): void {
export function queueExcessActiveBalance(
state: CachedBeaconStateElectra | CachedBeaconStateGloas,
index: ValidatorIndex
): void {
const balance = state.balances.get(index);
if (balance > MIN_ACTIVATION_BALANCE) {
const validator = state.validators.getReadonly(index);
@@ -53,7 +62,7 @@ export function queueExcessActiveBalance(state: CachedBeaconStateElectra, index:
/**
* Since we share pubkey2index, pubkey maybe added by other epoch transition but we don't have that validator in this state
*/
export function isPubkeyKnown(state: CachedBeaconStateElectra, pubkey: Uint8Array): boolean {
export function isPubkeyKnown(state: CachedBeaconStateElectra | CachedBeaconStateGloas, pubkey: Uint8Array): boolean {
return isValidatorKnown(state, state.epochCtx.getValidatorIndex(pubkey));
}
@@ -61,7 +70,7 @@ export function isPubkeyKnown(state: CachedBeaconStateElectra, pubkey: Uint8Arra
* Since we share pubkey2index, validatorIndex maybe not null but we don't have that validator in this state
*/
export function isValidatorKnown(
state: CachedBeaconStateElectra,
state: CachedBeaconStateElectra | CachedBeaconStateGloas,
index: ValidatorIndex | null
): index is ValidatorIndex {
return index !== null && index < state.validators.length;

View File

@@ -1,6 +1,6 @@
import {EPOCHS_PER_SYNC_COMMITTEE_PERIOD, GENESIS_EPOCH, MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
import {BeaconState, Epoch, Gwei, Slot, SyncPeriod} from "@lodestar/types";
import {CachedBeaconStateElectra} from "../types.js";
import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
import {getActivationExitChurnLimit, getConsolidationChurnLimit} from "./validator.js";
/**
@@ -41,7 +41,10 @@ export function computeActivationExitEpoch(epoch: Epoch): Epoch {
return epoch + 1 + MAX_SEED_LOOKAHEAD;
}
export function computeExitEpochAndUpdateChurn(state: CachedBeaconStateElectra, exitBalance: Gwei): number {
export function computeExitEpochAndUpdateChurn(
state: CachedBeaconStateElectra | CachedBeaconStateGloas,
exitBalance: Gwei
): number {
let earliestExitEpoch = Math.max(state.earliestExitEpoch, computeActivationExitEpoch(state.epochCtx.epoch));
const perEpochChurn = getActivationExitChurnLimit(state.epochCtx);
@@ -65,7 +68,7 @@ export function computeExitEpochAndUpdateChurn(state: CachedBeaconStateElectra,
}
export function computeConsolidationEpochAndUpdateChurn(
state: CachedBeaconStateElectra,
state: CachedBeaconStateElectra | CachedBeaconStateGloas,
consolidationBalance: Gwei
): number {
let earliestConsolidationEpoch = Math.max(

View File

@@ -0,0 +1,58 @@
import {byteArrayEquals} from "@chainsafe/ssz";
import {
BUILDER_PAYMENT_THRESHOLD_DENOMINATOR,
BUILDER_PAYMENT_THRESHOLD_NUMERATOR,
BUILDER_WITHDRAWAL_PREFIX,
EFFECTIVE_BALANCE_INCREMENT,
SLOTS_PER_EPOCH,
} from "@lodestar/params";
import {gloas} from "@lodestar/types";
import {AttestationData} from "@lodestar/types/phase0";
import {CachedBeaconStateGloas} from "../types.ts";
import {getBlockRootAtSlot} from "./blockRoot.ts";
import {computeEpochAtSlot} from "./epoch.ts";
import {RootCache} from "./rootCache.ts";
export function hasBuilderWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean {
return withdrawalCredentials[0] === BUILDER_WITHDRAWAL_PREFIX;
}
export function getBuilderPaymentQuorumThreshold(state: CachedBeaconStateGloas): number {
const quorum =
Math.floor((state.epochCtx.totalActiveBalanceIncrements * EFFECTIVE_BALANCE_INCREMENT) / SLOTS_PER_EPOCH) *
BUILDER_PAYMENT_THRESHOLD_NUMERATOR;
return Math.floor(quorum / BUILDER_PAYMENT_THRESHOLD_DENOMINATOR);
}
export function isBuilderPaymentWithdrawable(
state: CachedBeaconStateGloas,
withdrawal: gloas.BuilderPendingWithdrawal
): boolean {
const builder = state.validators.getReadonly(withdrawal.builderIndex);
const currentEpoch = computeEpochAtSlot(state.slot);
return builder.withdrawableEpoch >= currentEpoch || !builder.slashed;
}
export function isAttestationSameSlot(state: CachedBeaconStateGloas, data: AttestationData): boolean {
if (data.slot === 0) return true;
const isMatchingBlockRoot = byteArrayEquals(data.beaconBlockRoot, getBlockRootAtSlot(state, data.slot));
const isCurrentBlockRoot = !byteArrayEquals(data.beaconBlockRoot, getBlockRootAtSlot(state, data.slot - 1));
return isMatchingBlockRoot && isCurrentBlockRoot;
}
export function isAttestationSameSlotRootCache(rootCache: RootCache, data: AttestationData): boolean {
if (data.slot === 0) return true;
const isMatchingBlockRoot = byteArrayEquals(data.beaconBlockRoot, rootCache.getBlockRootAtSlot(data.slot));
const isCurrentBlockRoot = !byteArrayEquals(data.beaconBlockRoot, rootCache.getBlockRootAtSlot(data.slot - 1));
return isMatchingBlockRoot && isCurrentBlockRoot;
}
export function isParentBlockFull(state: CachedBeaconStateGloas): boolean {
return byteArrayEquals(state.latestExecutionPayloadBid.blockHash, state.latestBlockHash);
}

View File

@@ -5,6 +5,7 @@ import {
} from "@chainsafe/swap-or-not-shuffle";
import {
DOMAIN_BEACON_PROPOSER,
DOMAIN_PTC_ATTESTER,
DOMAIN_SYNC_COMMITTEE,
EFFECTIVE_BALANCE_INCREMENT,
EPOCHS_PER_HISTORICAL_VECTOR,
@@ -12,6 +13,7 @@ import {
MAX_EFFECTIVE_BALANCE,
MAX_EFFECTIVE_BALANCE_ELECTRA,
MIN_SEED_LOOKAHEAD,
PTC_SIZE,
SHUFFLE_ROUND_COUNT,
SLOTS_PER_EPOCH,
SYNC_COMMITTEE_SIZE,
@@ -19,7 +21,7 @@ import {
import {Bytes32, DomainType, Epoch, ValidatorIndex} from "@lodestar/types";
import {assert, bytesToBigInt, bytesToInt, intToBytes} from "@lodestar/utils";
import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js";
import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js";
import {BeaconStateAllForks, BeaconStateGloas, CachedBeaconStateAllForks} from "../types.js";
import {computeEpochAtSlot, computeStartSlotAtEpoch} from "./epoch.js";
/**
@@ -266,6 +268,60 @@ export function getNextSyncCommitteeIndices(
);
}
export function naiveGetPayloadTimlinessCommitteeIndices(
state: BeaconStateGloas,
shuffling: {committees: Uint32Array[][]},
effectiveBalanceIncrements: EffectiveBalanceIncrements,
epoch: Epoch
): ValidatorIndex[][] {
const epochSeed = getSeed(state, epoch, DOMAIN_PTC_ATTESTER);
const startSlot = computeStartSlotAtEpoch(epoch);
const committeeIndices = [];
for (let slot = startSlot; slot < startSlot + SLOTS_PER_EPOCH; slot++) {
const slotCommittees = shuffling.committees[slot % SLOTS_PER_EPOCH];
const indices = naiveComputePayloadTimelinessCommitteeIndices(
effectiveBalanceIncrements,
slotCommittees.flatMap((c) => Array.from(c)),
digest(Buffer.concat([epochSeed, intToBytes(slot, 8)]))
);
committeeIndices.push(indices);
}
return committeeIndices;
}
export function naiveComputePayloadTimelinessCommitteeIndices(
effectiveBalanceIncrements: EffectiveBalanceIncrements,
indices: ArrayLike<ValidatorIndex>,
seed: Uint8Array
): ValidatorIndex[] {
if (indices.length === 0) {
throw Error("Validator indices must not be empty");
}
const result = [];
const MAX_RANDOM_VALUE = 2 ** 16 - 1;
const MAX_EFFECTIVE_BALANCE_INCREMENT = MAX_EFFECTIVE_BALANCE_ELECTRA / EFFECTIVE_BALANCE_INCREMENT;
let i = 0;
while (result.length < PTC_SIZE) {
const candidateIndex = indices[i % indices.length];
const randomBytes = digest(Buffer.concat([seed, intToBytes(Math.floor(i / 16), 8, "le")]));
const offset = (i % 16) * 2;
const randomValue = bytesToInt(randomBytes.subarray(offset, offset + 2));
const effectiveBalanceIncrement = effectiveBalanceIncrements[candidateIndex];
if (effectiveBalanceIncrement * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_INCREMENT * randomValue) {
result.push(candidateIndex);
}
i += 1;
}
return result;
}
/**
* Return the shuffled validator index corresponding to ``seed`` (and ``index_count``).
*

View File

@@ -7,7 +7,7 @@ import {
} from "@lodestar/params";
import {Epoch, ValidatorIndex, phase0} from "@lodestar/types";
import {intDiv} from "@lodestar/utils";
import {BeaconStateAllForks, CachedBeaconStateElectra, EpochCache} from "../types.js";
import {BeaconStateAllForks, CachedBeaconStateElectra, CachedBeaconStateGloas, EpochCache} from "../types.js";
import {hasCompoundingWithdrawalCredential} from "./electra.js";
/**
@@ -94,12 +94,31 @@ export function getMaxEffectiveBalance(withdrawalCredentials: Uint8Array): numbe
return MIN_ACTIVATION_BALANCE;
}
export function getPendingBalanceToWithdraw(state: CachedBeaconStateElectra, validatorIndex: ValidatorIndex): number {
export function getPendingBalanceToWithdraw(
fork: ForkSeq,
state: CachedBeaconStateElectra | CachedBeaconStateGloas,
validatorIndex: ValidatorIndex
): number {
let total = 0;
for (const item of state.pendingPartialWithdrawals.getAllReadonly()) {
if (item.validatorIndex === validatorIndex) {
total += Number(item.amount);
}
}
if (fork >= ForkSeq.gloas) {
const stateGloas = state as CachedBeaconStateGloas;
for (const item of stateGloas.builderPendingWithdrawals.getAllReadonly()) {
if (item.builderIndex === validatorIndex) {
total += item.amount;
}
}
for (const item of stateGloas.builderPendingPayments.getAllReadonly()) {
if (item.withdrawal.builderIndex === validatorIndex) {
total += item.withdrawal.amount;
}
}
}
return total;
}

View File

@@ -1,4 +1,5 @@
import {bench, describe} from "@chainsafe/benchmark";
import {ForkSeq} from "@lodestar/params";
import {processSlot} from "../../../src/slot/index.js";
import {State} from "../types.js";
import {generatePerfTestCachedStatePhase0} from "../util.js";
@@ -14,7 +15,7 @@ describe("processSlot", () => {
fn: (state) => {
for (let i = 0; i < slotCount; i++) {
state.slot++;
processSlot(state);
processSlot(ForkSeq.phase0, state);
}
},
});

View File

@@ -5,6 +5,7 @@ import {
BLS_WITHDRAWAL_PREFIX,
COMPOUNDING_WITHDRAWAL_PREFIX,
FAR_FUTURE_EPOCH,
ForkSeq,
SLOTS_PER_EPOCH,
} from "@lodestar/params";
import {ssz} from "@lodestar/types";
@@ -51,7 +52,7 @@ describe("processConsolidationRequest", () => {
expect(state.pendingConsolidations.length).eq(0);
processConsolidationRequest(state, request);
processConsolidationRequest(ForkSeq.electra, state, request);
expect(state.pendingConsolidations.length).eq(0);
});

View File

@@ -23,7 +23,7 @@ const {Gwei, ExecutionAddress, ValidatorIndex, Epoch, BLSSignature, Bytes32, Roo
export const BuilderPendingWithdrawal = new ContainerType(
{
feeRecipient: ExecutionAddress,
amount: Gwei,
amount: UintNum64,
builderIndex: ValidatorIndex,
withdrawableEpoch: Epoch,
},
@@ -32,7 +32,7 @@ export const BuilderPendingWithdrawal = new ContainerType(
export const BuilderPendingPayment = new ContainerType(
{
weight: Gwei,
weight: UintNum64,
withdrawal: BuilderPendingWithdrawal,
},
{typeName: "BuilderPendingPayment", jsonCase: "eth2"}
@@ -80,11 +80,13 @@ export const ExecutionPayloadBid = new ContainerType(
parentBlockHash: Bytes32,
parentBlockRoot: Root,
blockHash: Bytes32,
prevRandao: Bytes32,
feeRecipient: ExecutionAddress,
gasLimit: UintBn64,
builderIndex: ValidatorIndex,
slot: Slot,
value: Gwei,
value: UintNum64,
executionPayment: UintNum64,
blobKzgCommitmentsRoot: Root,
},
{typeName: "ExecutionPayloadBid", jsonCase: "eth2"}