mirror of
https://github.com/ChainSafe/lodestar.git
synced 2026-01-08 23:28:10 -05:00
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:
@@ -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 =
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,8 @@ function translateParticipation(
|
||||
data,
|
||||
attestation.inclusionDelay,
|
||||
epochCtx.epoch,
|
||||
rootCache
|
||||
rootCache,
|
||||
null
|
||||
);
|
||||
|
||||
const committeeIndices = epochCtx.getBeaconCommittee(data.slot, data.index);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
58
packages/state-transition/src/util/gloas.ts
Normal file
58
packages/state-transition/src/util/gloas.ts
Normal 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);
|
||||
}
|
||||
@@ -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``).
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user