Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 120 additions & 23 deletions packages/state-transition/src/block/processDepositRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {BeaconConfig} from "@lodestar/config";
import {FAR_FUTURE_EPOCH, ForkSeq, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params";
import {BLSPubkey, Bytes32, PubkeyHex, UintNum64, electra, ssz} from "@lodestar/types";
import {toPubkeyHex} from "@lodestar/utils";
import {type PendingDepositPubkeyIndex, type PendingValidatorVerifiedCount} from "../cache/epochCache.js";
import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
import {findBuilderIndexByPubkey, isBuilderWithdrawalCredential} from "../util/gloas.js";
import {computeEpochAtSlot, isValidatorKnown} from "../util/index.js";
Expand Down Expand Up @@ -76,49 +77,31 @@ function addBuilderToRegistry(
}
}

// TODO GLOAS: pendingValidatorPubkeys cache is currently naive and has room for improvement.
// Currently the cache lives in process_block, but we should put it in epochCache or elsewhere that has longer
// lifetime to avoid duplicated deposit signature computation
// See https://github.com/ChainSafe/lodestar/issues/9181
export function processDepositRequest(
fork: ForkSeq,
state: CachedBeaconStateElectra | CachedBeaconStateGloas,
depositRequest: electra.DepositRequest,
pendingValidatorPubkeysCache?: Set<PubkeyHex>
depositRequest: electra.DepositRequest
): void {
const {pubkey, withdrawalCredentials, amount, signature} = depositRequest;
const stateGloas = fork >= ForkSeq.gloas ? (state as CachedBeaconStateGloas) : null;
const pubkeyHex = stateGloas !== null ? toPubkeyHex(pubkey) : null;

// Check if this is a builder or validator deposit
if (fork >= ForkSeq.gloas) {
const stateGloas = state as CachedBeaconStateGloas;
const pendingValidatorPubkeys =
pendingValidatorPubkeysCache ?? getPendingValidatorPubkeys(state.config, stateGloas);
const pubkeyHex = toPubkeyHex(pubkey);
if (stateGloas !== null && pubkeyHex !== null) {
const builderIndex = findBuilderIndexByPubkey(stateGloas, pubkey);
const validatorIndex = state.epochCtx.getValidatorIndex(pubkey);

// Regardless of the withdrawal credentials prefix, if a builder/validator
// already exists with this pubkey, apply the deposit to their balance
const isBuilder = builderIndex !== null;
const isValidator = isValidatorKnown(state, validatorIndex);
const isPendingValidator = pendingValidatorPubkeys.has(pubkeyHex);
const isPendingValidator = getVerifiedPendingValidatorCountForPubkey(stateGloas, pubkeyHex) > 0;

if (isBuilder || (isBuilderWithdrawalCredential(withdrawalCredentials) && !isValidator && !isPendingValidator)) {
// Apply builder deposits immediately
applyDepositForBuilder(stateGloas, pubkey, withdrawalCredentials, amount, signature, state.slot);
return;
}

// Keep the shared cache in sync: if this deposit has a valid signature, subsequent
// deposit requests for the same pubkey in this envelope must see it as a pending validator
if (
pendingValidatorPubkeysCache &&
!isValidator &&
!isPendingValidator &&
isValidDepositSignature(state.config, pubkey, withdrawalCredentials, amount, signature)
) {
pendingValidatorPubkeys.add(pubkeyHex);
}
}

// Only set deposit_requests_start_index in Electra fork, not Gloas
Expand All @@ -135,6 +118,120 @@ export function processDepositRequest(
slot: state.slot,
});
state.pendingDeposits.push(pendingDeposit);

if (stateGloas !== null && pubkeyHex !== null) {
const pendingDepositPubkeyIndex = ensurePendingDepositPubkeyIndexWritable(stateGloas);
const newPendingDepositIndex = state.pendingDeposits.length - 1;
const bucket = pendingDepositPubkeyIndex.get(pubkeyHex);

if (bucket) {
pendingDepositPubkeyIndex.set(pubkeyHex, [...bucket, newPendingDepositIndex]);
} else {
pendingDepositPubkeyIndex.set(pubkeyHex, [newPendingDepositIndex]);
}

const verifiedPendingValidatorCount = getPendingValidatorVerifiedCount(stateGloas);
const cachedCount = verifiedPendingValidatorCount.get(pubkeyHex);
if (
cachedCount !== undefined &&
isValidDepositSignature(state.config, pubkey, withdrawalCredentials, amount, signature)
) {
ensurePendingValidatorVerifiedCountWritable(stateGloas).set(pubkeyHex, cachedCount + 1);
}
}
}

export function ensurePendingDepositPubkeyIndex(state: CachedBeaconStateGloas): PendingDepositPubkeyIndex {
if (state.epochCtx.pendingDepositPubkeyIndex !== null) {
return state.epochCtx.pendingDepositPubkeyIndex;
}

const pendingDepositPubkeyIndex: PendingDepositPubkeyIndex = new Map();
for (let i = 0; i < state.pendingDeposits.length; i++) {
const pubkeyHex = toPubkeyHex(state.pendingDeposits.getReadonly(i).pubkey);
Comment on lines +150 to +151
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Iterating over all pending deposits and converting every pubkey to hex can be a performance bottleneck if the queue is large. While this is only done when the cache is initialized, consider if the hex representation can be cached or if PendingDeposit can store it to avoid repeated toPubkeyHex calls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think that will be a problem since I got 70.94 ms on pubkey-only scan

const bucket = pendingDepositPubkeyIndex.get(pubkeyHex);

if (bucket) {
bucket.push(i);
} else {
pendingDepositPubkeyIndex.set(pubkeyHex, [i]);
}
}

state.epochCtx.pendingDepositPubkeyIndex = pendingDepositPubkeyIndex;
state.epochCtx.pendingDepositPubkeyIndexShared = false;
if (state.epochCtx.pendingValidatorVerifiedCount === null) {
state.epochCtx.pendingValidatorVerifiedCount = new Map();
state.epochCtx.pendingValidatorVerifiedCountShared = false;
}

return pendingDepositPubkeyIndex;
}

function ensurePendingDepositPubkeyIndexWritable(state: CachedBeaconStateGloas): PendingDepositPubkeyIndex {
let pendingDepositPubkeyIndex = ensurePendingDepositPubkeyIndex(state);

if (state.epochCtx.pendingDepositPubkeyIndexShared) {
pendingDepositPubkeyIndex = new Map(pendingDepositPubkeyIndex);
state.epochCtx.pendingDepositPubkeyIndex = pendingDepositPubkeyIndex;
state.epochCtx.pendingDepositPubkeyIndexShared = false;
}

return pendingDepositPubkeyIndex;
}

function getPendingValidatorVerifiedCount(state: CachedBeaconStateGloas): PendingValidatorVerifiedCount {
if (state.epochCtx.pendingValidatorVerifiedCount === null) {
state.epochCtx.pendingValidatorVerifiedCount = new Map();
state.epochCtx.pendingValidatorVerifiedCountShared = false;
}

return state.epochCtx.pendingValidatorVerifiedCount;
}

function ensurePendingValidatorVerifiedCountWritable(state: CachedBeaconStateGloas): PendingValidatorVerifiedCount {
let verifiedPendingValidatorCount = getPendingValidatorVerifiedCount(state);

if (state.epochCtx.pendingValidatorVerifiedCountShared) {
verifiedPendingValidatorCount = new Map(verifiedPendingValidatorCount);
state.epochCtx.pendingValidatorVerifiedCount = verifiedPendingValidatorCount;
state.epochCtx.pendingValidatorVerifiedCountShared = false;
}

return verifiedPendingValidatorCount;
}

function getVerifiedPendingValidatorCountForPubkey(state: CachedBeaconStateGloas, pubkeyHex: PubkeyHex): number {
const verifiedPendingValidatorCount = getPendingValidatorVerifiedCount(state);
const cachedCount = verifiedPendingValidatorCount.get(pubkeyHex);
if (cachedCount !== undefined) {
return cachedCount;
}

const bucket = ensurePendingDepositPubkeyIndex(state).get(pubkeyHex);
if (!bucket) {
ensurePendingValidatorVerifiedCountWritable(state).set(pubkeyHex, 0);
return 0;
}

let validCount = 0;
for (const pendingDepositIndex of bucket) {
const pendingDeposit = state.pendingDeposits.getReadonly(pendingDepositIndex);
if (
isValidDepositSignature(
state.config,
pendingDeposit.pubkey,
pendingDeposit.withdrawalCredentials,
pendingDeposit.amount,
pendingDeposit.signature
)
) {
validCount++;
}
}

ensurePendingValidatorVerifiedCountWritable(state).set(pubkeyHex, validCount);
return validCount;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {CachedBeaconStateGloas} from "../types.js";
import {computeTimeAtSlot} from "../util/index.js";
import {verifySignatureSet} from "../util/signatureSets.js";
import {processConsolidationRequest} from "./processConsolidationRequest.js";
import {getPendingValidatorPubkeys, processDepositRequest} from "./processDepositRequest.js";
import {ensurePendingDepositPubkeyIndex, processDepositRequest} from "./processDepositRequest.js";
import {processWithdrawalRequest} from "./processWithdrawalRequest.js";

export type ProcessExecutionPayloadEnvelopeOpts = {
Expand All @@ -33,19 +33,20 @@ export function processExecutionPayloadEnvelope(
throw Error(`Execution payload envelope has invalid signature builderIndex=${envelope.builderIndex}`);
}

const requests = envelope.executionRequests;

if (requests.deposits.length > 0) {
ensurePendingDepositPubkeyIndex(state);
}

// .clone() before mutating state, similar to stateTransition()
const postState = state.clone(opts?.dontTransferCache) as CachedBeaconStateGloas;

validateExecutionPayloadEnvelope(postState, envelope);

const requests = envelope.executionRequests;

if (requests.deposits.length > 0) {
// Build cache of pending validator pubkeys once, shared across all deposit requests
const pendingValidatorPubkeys = getPendingValidatorPubkeys(postState.config, postState);

for (const deposit of requests.deposits) {
processDepositRequest(fork, postState, deposit, pendingValidatorPubkeys);
processDepositRequest(fork, postState, deposit);
}
}

Expand Down
50 changes: 50 additions & 0 deletions packages/state-transition/src/cache/epochCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
CommitteeIndex,
Epoch,
IndexedAttestation,
PubkeyHex,
RootHex,
Slot,
SubnetID,
Expand Down Expand Up @@ -79,6 +80,9 @@ export type EpochCacheOpts = {
shufflingGetter?: ShufflingGetter;
};

export type PendingDepositPubkeyIndex = Map<PubkeyHex, number[]>;
export type PendingValidatorVerifiedCount = Map<PubkeyHex, number>;

/** Defers computing proposers by persisting only the seed, and dropping it once indexes are computed */
type ProposersDeferred = {computed: false; seed: Uint8Array} | {computed: true; indexes: ValidatorIndex[]};

Expand Down Expand Up @@ -226,6 +230,29 @@ export class EpochCache {
/** TODO: Indexed SyncCommitteeCache */
nextSyncCommitteeIndexed: SyncCommitteeCache;

/**
* Maps each pending-deposit pubkey to the indices where it appears in `state.pendingDeposits`.
* Starts as `null` on states created from bytes and is built when needed.
*/
pendingDepositPubkeyIndex: PendingDepositPubkeyIndex | null;
/**
* True when `pendingDepositPubkeyIndex` is shared with another cloned state and must be detached
* before mutating the map itself.
*/
pendingDepositPubkeyIndexShared: boolean;

/**
* Caches the number of signature-valid pending deposits for each pubkey.
* Starts as `null` on states created from bytes and is built when needed. a missing map entry means
* that pubkey has not been checked for the current state yet.
*/
pendingValidatorVerifiedCount: PendingValidatorVerifiedCount | null;
/**
* True when `pendingValidatorVerifiedCount` is shared with another cloned state and must be detached
* before mutating the map itself.
*/
pendingValidatorVerifiedCountShared: boolean;

// TODO GLOAS: See if we need to cache PTC for next epoch
// PTC for previous epoch, required for slot N block validating slot N-1 attestations
previousPayloadTimelinessCommittees: Uint32Array[];
Expand Down Expand Up @@ -268,6 +295,10 @@ export class EpochCache {
previousTargetUnslashedBalanceIncrements: number;
currentSyncCommitteeIndexed: SyncCommitteeCache;
nextSyncCommitteeIndexed: SyncCommitteeCache;
pendingDepositPubkeyIndex: PendingDepositPubkeyIndex | null;
pendingDepositPubkeyIndexShared: boolean;
pendingValidatorVerifiedCount: PendingValidatorVerifiedCount | null;
pendingValidatorVerifiedCountShared: boolean;
previousPayloadTimelinessCommittees: Uint32Array[];
payloadTimelinessCommittees: Uint32Array[];
epoch: Epoch;
Expand Down Expand Up @@ -299,6 +330,10 @@ export class EpochCache {
this.previousTargetUnslashedBalanceIncrements = data.previousTargetUnslashedBalanceIncrements;
this.currentSyncCommitteeIndexed = data.currentSyncCommitteeIndexed;
this.nextSyncCommitteeIndexed = data.nextSyncCommitteeIndexed;
this.pendingDepositPubkeyIndex = data.pendingDepositPubkeyIndex;
this.pendingDepositPubkeyIndexShared = data.pendingDepositPubkeyIndexShared;
this.pendingValidatorVerifiedCount = data.pendingValidatorVerifiedCount;
this.pendingValidatorVerifiedCountShared = data.pendingValidatorVerifiedCountShared;
this.previousPayloadTimelinessCommittees = data.previousPayloadTimelinessCommittees;
this.payloadTimelinessCommittees = data.payloadTimelinessCommittees;
this.epoch = data.epoch;
Expand Down Expand Up @@ -544,6 +579,10 @@ export class EpochCache {
currentTargetUnslashedBalanceIncrements,
currentSyncCommitteeIndexed,
nextSyncCommitteeIndexed,
pendingDepositPubkeyIndex: null,
pendingDepositPubkeyIndexShared: false,
pendingValidatorVerifiedCount: null,
pendingValidatorVerifiedCountShared: false,
previousPayloadTimelinessCommittees,
payloadTimelinessCommittees,
epoch: currentEpoch,
Expand All @@ -558,6 +597,13 @@ export class EpochCache {
// warning: pubkey cache is not copied, it is shared, as eth1 is not expected to reorder validators.
// Shallow copy all data from current epoch context to the next
// All data is completely replaced, or only-appended
if (this.pendingDepositPubkeyIndex !== null) {
this.pendingDepositPubkeyIndexShared = true;
}
if (this.pendingValidatorVerifiedCount !== null) {
this.pendingValidatorVerifiedCountShared = true;
}

return new EpochCache({
config: this.config,
// Common append-only structures shared with all states, no need to clone
Expand Down Expand Up @@ -590,6 +636,10 @@ export class EpochCache {
currentTargetUnslashedBalanceIncrements: this.currentTargetUnslashedBalanceIncrements,
currentSyncCommitteeIndexed: this.currentSyncCommitteeIndexed,
nextSyncCommitteeIndexed: this.nextSyncCommitteeIndexed,
pendingDepositPubkeyIndex: this.pendingDepositPubkeyIndex,
pendingDepositPubkeyIndexShared: this.pendingDepositPubkeyIndex !== null,
pendingValidatorVerifiedCount: this.pendingValidatorVerifiedCount,
pendingValidatorVerifiedCountShared: this.pendingValidatorVerifiedCount !== null,
previousPayloadTimelinessCommittees: this.previousPayloadTimelinessCommittees,
payloadTimelinessCommittees: this.payloadTimelinessCommittees,
epoch: this.epoch,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ export function processPendingDeposits(state: CachedBeaconStateElectra, cache: E
state.pendingDeposits.push(deposit);
}

state.epochCtx.pendingDepositPubkeyIndex = null;
state.epochCtx.pendingDepositPubkeyIndexShared = false;
state.epochCtx.pendingValidatorVerifiedCount = null;
state.epochCtx.pendingValidatorVerifiedCountShared = false;

// Accumulate churn only if the churn limit has been hit.
if (isChurnLimitReached) {
state.depositBalanceToConsume = availableForProcessing - BigInt(processedAmount);
Expand Down