Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
123 changes: 111 additions & 12 deletions packages/state-transition/src/block/processDepositRequest.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {FAR_FUTURE_EPOCH, ForkSeq, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params";
import {BLSPubkey, Bytes32, UintNum64, electra, ssz} from "@lodestar/types";
import {PublicKey, Signature, verify, verifyMultipleAggregateSignatures} from "@chainsafe/blst";
import {BeaconConfig} from "@lodestar/config";
import {DOMAIN_DEPOSIT, FAR_FUTURE_EPOCH, ForkSeq, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params";
import {BLSPubkey, BuilderIndex, Bytes32, Epoch, UintNum64, electra, ssz} from "@lodestar/types";
import {toPubkeyHex} from "@lodestar/utils";
import {ZERO_HASH} from "../constants/index.js";
import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
import {findBuilderIndexByPubkey, isBuilderWithdrawalCredential} from "../util/gloas.js";
import {computeEpochAtSlot, isValidatorKnown} from "../util/index.js";
import {computeDomain, computeEpochAtSlot, computeSigningRoot, isValidatorKnown} from "../util/index.js";
import {PendingDepositsLookup} from "../util/pendingDepositsLookup.js";
import {isValidDepositSignature} from "./processDeposit.js";

Expand Down Expand Up @@ -33,6 +36,20 @@ export function applyDepositForBuilder(
}
}

/**
* Create a new builder registry entry (a `Builder` view) from a deposit.
*/
function buildNewBuilder(pubkey: BLSPubkey, withdrawalCredentials: Bytes32, amount: UintNum64, depositEpoch: Epoch) {
return ssz.gloas.Builder.toViewDU({
pubkey,
version: withdrawalCredentials[0],
executionAddress: withdrawalCredentials.subarray(12),
balance: amount,
depositEpoch,
withdrawableEpoch: FAR_FUTURE_EPOCH,
});
}

/**
* Add a new builder to the builders registry.
* Reuses slots from exited and fully withdrawn builders if available.
Expand All @@ -57,15 +74,7 @@ function addBuilderToRegistry(
}
}

// Create new builder
const newBuilder = ssz.gloas.Builder.toViewDU({
pubkey,
version: withdrawalCredentials[0],
executionAddress: withdrawalCredentials.subarray(12),
balance: amount,
depositEpoch: depositEpoch,
withdrawableEpoch: FAR_FUTURE_EPOCH,
});
const newBuilder = buildNewBuilder(pubkey, withdrawalCredentials, amount, depositEpoch);

if (builderIndex < state.builders.length) {
// Reuse existing slot
Expand All @@ -76,6 +85,96 @@ function addBuilderToRegistry(
}
}

/**
* Apply a deposit for a builder whose registry index is already known by the caller.
*
* This is called at gloas fork transition only. The signature is NOT verified here — the
* caller (`onboardBuildersFromPendingDeposits`) batch-verifies new-builder signatures
* before calling this. A new builder is appended directly, since the slot-reuse scan in
* `addBuilderToRegistry` is unnecessary at the fork (`state.builders` starts empty).
*/
export function applyDepositForBuilderIndex(
state: CachedBeaconStateGloas,
builderIndex: BuilderIndex | null,
pubkey: BLSPubkey,
withdrawalCredentials: Bytes32,
amount: UintNum64,
slot: UintNum64
): void {
if (builderIndex !== null) {
// Existing builder - increase balance
state.builders.get(builderIndex).balance += amount;
return;
}

// New builder - signature already verified by the caller; append directly
state.builders.push(buildNewBuilder(pubkey, withdrawalCredentials, amount, computeEpochAtSlot(slot)));
}

/**
* Verify a batch of deposit signatures. Tries batch verification first; on failure falls
* back to verifying each deposit individually so the valid deposits in a batch that
* contains an invalid one are still identified. Returns a boolean per input deposit.
*/
export function verifyDepositSignatures(config: BeaconConfig, deposits: electra.PendingDeposit[]): boolean[] {
const results = new Array<boolean>(deposits.length).fill(false);
// Deposit signatures use a fork-agnostic domain, see `isValidDepositSignature`
const domain = computeDomain(DOMAIN_DEPOSIT, config.GENESIS_FORK_VERSION, ZERO_HASH);

const signatureSets: {publicKey: PublicKey; message: Uint8Array; signature: Uint8Array}[] = [];
const signatureSetDepositIndices: number[] = [];
for (let i = 0; i < deposits.length; i++) {
const {pubkey, withdrawalCredentials, amount, signature} = deposits[i];
let publicKey: PublicKey;
try {
// Deposit pubkeys are untrusted: must be group + infinity checked
publicKey = PublicKey.fromBytes(pubkey, true);
} catch (_) {
// Malformed pubkey - invalid deposit, results[i] stays false
continue;
}
const message = computeSigningRoot(ssz.phase0.DepositMessage, {pubkey, withdrawalCredentials, amount}, domain);
signatureSets.push({publicKey, message, signature});
signatureSetDepositIndices.push(i);
}

if (signatureSets.length === 0) {
return results;
}

let batchValid: boolean;
try {
batchValid =
signatureSets.length >= 2
? verifyMultipleAggregateSignatures(
signatureSets.map((s) => ({pk: s.publicKey, msg: s.message, sig: Signature.fromBytes(s.signature, true)}))
)
: verify(
signatureSets[0].message,
signatureSets[0].publicKey,
Signature.fromBytes(signatureSets[0].signature, true)
);
} catch (_) {
batchValid = false;
}
Comment thread
twoeths marked this conversation as resolved.
Outdated

if (batchValid) {
// Batch passed - every deposit with a well-formed pubkey is valid
for (const depositIndex of signatureSetDepositIndices) {
results[depositIndex] = true;
}
} else {
// Batch failed: at least one signature is invalid - verify each individually
for (let s = 0; s < signatureSets.length; s++) {
const depositIndex = signatureSetDepositIndices[s];
const {pubkey, withdrawalCredentials, amount, signature} = deposits[depositIndex];
results[depositIndex] = isValidDepositSignature(config, pubkey, withdrawalCredentials, amount, signature);
}
}

return results;
}

// TODO GLOAS: the PendingDepositsLookup is currently scoped to a single envelope of
// deposit-requests. We can track it as ephemeral within EpochCache and transfer to the next block
// transition to reuse cached signature verifications.
Expand Down
105 changes: 81 additions & 24 deletions packages/state-transition/src/slot/upgradeStateToGloas.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
import {ssz} from "@lodestar/types";
import {PubkeyHex, electra, ssz} from "@lodestar/types";
import {toPubkeyHex} from "@lodestar/utils";
import {applyDepositForBuilder} from "../block/processDepositRequest.js";
import {applyDepositForBuilderIndex, verifyDepositSignatures} from "../block/processDepositRequest.js";
import {getCachedBeaconState} from "../cache/stateCache.js";
import {CachedBeaconStateFulu, CachedBeaconStateGloas} from "../types.js";
import {initializePtcWindow, isBuilderWithdrawalCredential} from "../util/gloas.js";
Expand Down Expand Up @@ -85,17 +85,56 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea
return stateGloas;
}

/** Verify queued builder deposit signatures in batches of this size. */
const BUILDER_DEPOSIT_BATCH_SIZE = 32;

/**
* Applies any pending deposits for builders to onboard builders during the fork transition
* Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.8/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits
*
* New-builder deposits are verified lazily: signatures are queued and batch-verified
* `BUILDER_DEPOSIT_BATCH_SIZE` at a time.
*/
function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void {
// Track pubkeys of new builders added when applying deposits
const builderPubkeys = new Set<string>();
export function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void {
// Map of builder pubkey -> index in `state.builders` for builders already applied in this loop.
const builderIndexByPubkey = new Map<PubkeyHex, number>();
// FIFO queue of new-builder deposits awaiting batch signature verification. Holds
// distinct pubkeys, a reappearing queued pubkey force-flushes the queue first.
const queuedBuilderDeposits = new Map<PubkeyHex, electra.PendingDeposit>();

const pendingDeposits = ssz.electra.PendingDeposits.defaultViewDU();
const pendingDepositsLookup = PendingDepositsLookup.buildEmpty();

// Batch-verify the queued deposits and apply the ones with valid signatures.
const flushQueue = (): void => {
if (queuedBuilderDeposits.size === 0) {
return;
}
const entries = Array.from(queuedBuilderDeposits);
const validResults = verifyDepositSignatures(
state.config,
entries.map(([, deposit]) => deposit)
);
for (let j = 0; j < entries.length; j++) {
if (!validResults[j]) {
continue;
}
const [pubkeyHex, deposit] = entries[j];
// With direct push (no slot reuse at the fork) the builder lands at the current length
const builderIndex = state.builders.length;
applyDepositForBuilderIndex(
state,
null,
deposit.pubkey,
deposit.withdrawalCredentials,
deposit.amount,
deposit.slot
);
builderIndexByPubkey.set(pubkeyHex, builderIndex);
}
queuedBuilderDeposits.clear();
};

for (let i = 0; i < state.pendingDeposits.length; i++) {
const deposit = state.pendingDeposits.getReadonly(i);

Expand All @@ -109,11 +148,19 @@ function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void
continue;
}

// `applyDepositForBuilder` can mutate the state and add a builder to the registry, so
// the set of builder pubkeys must be recomputed each iteration. `builderPubkeys` stands
// in for the spec's `[b.pubkey for b in state.builders]`: `state.builders` starts empty
// at the fork, so every builder is one added in a previous iteration of this loop.
if (!builderPubkeys.has(pubkeyHex)) {
// after the flush it is either an applied builder (-> top-up below) or absent (its
// queued deposit had an invalid signature -> re-evaluated as a fresh candidate).
// this ensures the functions work the same way to the spec
if (queuedBuilderDeposits.has(pubkeyHex)) {
flushQueue();
}

// `?? null` is required to keep builder index 0. A known index means a top-up to an
// already-onboarded builder, applied regardless of withdrawal credential; otherwise
// this is a candidate new builder and the credential checks below apply.
const builderIndex = builderIndexByPubkey.get(pubkeyHex) ?? null;

if (builderIndex === null) {
// Deposits for non-builders stay in the pending queue. If there is a valid pending
// deposit for a new validator with this pubkey, keep this deposit in the pending
// queue to be applied to that validator later.
Expand All @@ -127,23 +174,33 @@ function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void
pendingDepositsLookup.add(deposit, pubkeyHex);
continue;
}
}

const buildersLenBefore = state.builders.length;
// TODO GLOAS: handle 20k 1ETH deposits on time
// there is a note in the spec https://github.com/ethereum/consensus-specs/pull/5227
applyDepositForBuilder(
state,
deposit.pubkey,
deposit.withdrawalCredentials,
deposit.amount,
deposit.signature,
deposit.slot
);
if (state.builders.length > buildersLenBefore) {
builderPubkeys.add(pubkeyHex);
// New builder candidate: queue it for lazy batch signature verification
queuedBuilderDeposits.set(pubkeyHex, {
pubkey: deposit.pubkey,
withdrawalCredentials: deposit.withdrawalCredentials,
amount: deposit.amount,
signature: deposit.signature,
slot: deposit.slot,
});
if (queuedBuilderDeposits.size >= BUILDER_DEPOSIT_BATCH_SIZE) {
flushQueue();
}
} else {
// Top-up of an already-onboarded builder; no signature verification needed
applyDepositForBuilderIndex(
state,
builderIndex,
deposit.pubkey,
deposit.withdrawalCredentials,
deposit.amount,
deposit.slot
);
}
}

// Verify and apply any remaining queued builder deposits
flushQueue();

state.pendingDeposits = pendingDeposits;
}
Loading
Loading