Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import {FAR_FUTURE_EPOCH} from "@lodestar/params";
import {FAR_FUTURE_EPOCH, PAYLOAD_BUILDER_VERSION} from "@lodestar/params";
import {gloas} from "@lodestar/types";
import {CachedBeaconStateGloas} from "../types.js";
import {computeEpochAtSlot} from "../util/epoch.js";
import {addBuilderToRegistry, findBuilderIndexByPubkey, isValidBuilderDepositSignature} from "../util/gloas.js";
import {
addBuilderToRegistry,
findBuilderIndexByPubkey,
isBuilderWithdrawalCredential,
isValidBuilderDepositSignature,
} from "../util/gloas.js";

/**
* Process a builder deposit request from the execution layer: register a new builder
Expand All @@ -15,14 +20,20 @@ export function processBuilderDepositRequest(
request: gloas.BuilderDepositRequest
): void {
const {pubkey, withdrawalCredentials, amount, signature} = request;

// Ignore deposits with unexpected withdrawal credential prefixes.
if (!isBuilderWithdrawalCredential(withdrawalCredentials)) {
return;
}

const builderIndex = findBuilderIndexByPubkey(state, pubkey);

if (builderIndex === null) {
if (isValidBuilderDepositSignature(state.config, pubkey, withdrawalCredentials, amount, signature)) {
addBuilderToRegistry(
state,
pubkey,
withdrawalCredentials[0],
PAYLOAD_BUILDER_VERSION,
withdrawalCredentials.subarray(12),
amount,
state.slot
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import {beforeEach, describe, expect, it, vi} from "vitest";
import {createBeaconConfig} from "@lodestar/config";
import {getConfig} from "@lodestar/config/test-utils";
import {BUILDER_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH, ForkName, SLOTS_PER_EPOCH} from "@lodestar/params";
import {
BUILDER_WITHDRAWAL_PREFIX,
FAR_FUTURE_EPOCH,
ForkName,
PAYLOAD_BUILDER_VERSION,
SLOTS_PER_EPOCH,
} from "@lodestar/params";
import {ssz} from "@lodestar/types";

const isValidBuilderDepositSignatureMock = vi.hoisted(() =>
Expand Down Expand Up @@ -49,9 +55,12 @@ function buildGloasState(slot = 0) {
);
}

function makeBuilderWithdrawalCredentials(executionAddress: Uint8Array): Uint8Array {
function makeBuilderWithdrawalCredentials(
executionAddress: Uint8Array,
prefix = BUILDER_WITHDRAWAL_PREFIX
): Uint8Array {
const creds = new Uint8Array(32);
creds[0] = BUILDER_WITHDRAWAL_PREFIX;
creds[0] = prefix;
creds.set(executionAddress, 12);
return creds;
}
Expand All @@ -61,17 +70,21 @@ function makeBuilderDepositRequest({
executionAddress = Uint8Array.from({length: 20}, (_, i) => i + 1),
amount = 1_000_000_000,
signatureFirstByte = 1, // 1 => valid via mock, anything else => invalid
withdrawalCredentials,
prefix = BUILDER_WITHDRAWAL_PREFIX,
}: {
pubkey?: Uint8Array;
executionAddress?: Uint8Array;
amount?: number;
signatureFirstByte?: number;
withdrawalCredentials?: Uint8Array;
prefix?: number;
} = {}) {
const signature = new Uint8Array(96);
signature[0] = signatureFirstByte;
return {
pubkey,
withdrawalCredentials: makeBuilderWithdrawalCredentials(executionAddress),
withdrawalCredentials: withdrawalCredentials ?? makeBuilderWithdrawalCredentials(executionAddress, prefix),
amount,
signature,
};
Expand All @@ -95,10 +108,20 @@ describe("processBuilderDepositRequest", () => {
const builder = state.builders.get(0);
expect(builder.balance).toBe(32_000_000_000);
expect(builder.executionAddress).toEqual(request.withdrawalCredentials.subarray(12));
expect(builder.version).toBe(BUILDER_WITHDRAWAL_PREFIX);
expect(builder.version).toBe(PAYLOAD_BUILDER_VERSION);
expect(builder.withdrawableEpoch).toBe(FAR_FUTURE_EPOCH);
});

it("drops a new builder request when the withdrawal credentials prefix is not the builder prefix", () => {
const state = buildGloasState(1);
const request = makeBuilderDepositRequest({prefix: 0x01});

processBuilderDepositRequest(state, request);

expect(isValidBuilderDepositSignatureMock).not.toHaveBeenCalled();
expect(state.builders.length).toBe(0);
});

it("drops the request when PoP is invalid", () => {
const state = buildGloasState(1);
const request = makeBuilderDepositRequest({signatureFirstByte: 0});
Expand All @@ -113,21 +136,20 @@ describe("processBuilderDepositRequest", () => {
const state = buildGloasState(SLOTS_PER_EPOCH);
const pubkey = Uint8Array.from({length: 48}, (_, i) => i + 1);
const originalAddress = Uint8Array.from({length: 20}, (_, i) => i + 1);
const originalCreds = makeBuilderWithdrawalCredentials(originalAddress);

state.builders.push(
ssz.gloas.Builder.toViewDU({
pubkey,
version: originalCreds[0],
version: PAYLOAD_BUILDER_VERSION,
executionAddress: originalAddress,
balance: 32_000_000_000,
depositEpoch: 0,
withdrawableEpoch: FAR_FUTURE_EPOCH,
})
);

// Attacker-shaped top-up: same pubkey but different (would-be) execution address. Top-ups
// must ignore the request's withdrawal credentials and signature entirely.
// Attacker-shaped top-up: same pubkey but different (would-be) execution address. Valid
// top-ups must ignore the request's execution address and signature.
const attackerAddress = Uint8Array.from({length: 20}, () => 0xff);
const request = makeBuilderDepositRequest({
pubkey,
Expand All @@ -143,7 +165,39 @@ describe("processBuilderDepositRequest", () => {
const builder = state.builders.get(0);
expect(builder.balance).toBe(33_000_000_000);
expect(builder.executionAddress).toEqual(originalAddress);
expect(builder.version).toBe(BUILDER_WITHDRAWAL_PREFIX);
expect(builder.version).toBe(PAYLOAD_BUILDER_VERSION);
});

it("drops a top-up when the withdrawal credentials prefix is not the builder prefix", () => {
const state = buildGloasState(SLOTS_PER_EPOCH);
const pubkey = Uint8Array.from({length: 48}, (_, i) => i + 1);
const executionAddress = Uint8Array.from({length: 20}, (_, i) => i + 1);

state.builders.push(
ssz.gloas.Builder.toViewDU({
pubkey,
version: PAYLOAD_BUILDER_VERSION,
executionAddress,
balance: 32_000_000_000,
depositEpoch: 0,
withdrawableEpoch: FAR_FUTURE_EPOCH,
})
);

const request = makeBuilderDepositRequest({
pubkey,
amount: 1_000_000_000,
prefix: 0x01,
});

processBuilderDepositRequest(state, request);

expect(isValidBuilderDepositSignatureMock).not.toHaveBeenCalled();
expect(state.builders.length).toBe(1);
const builder = state.builders.get(0);
expect(builder.balance).toBe(32_000_000_000);
expect(builder.executionAddress).toEqual(executionAddress);
expect(builder.version).toBe(PAYLOAD_BUILDER_VERSION);
});

it("resets the withdrawable epoch when topping up an exited, fully-swept builder", () => {
Expand All @@ -156,7 +210,7 @@ describe("processBuilderDepositRequest", () => {
state.builders.push(
ssz.gloas.Builder.toViewDU({
pubkey,
version: BUILDER_WITHDRAWAL_PREFIX,
version: PAYLOAD_BUILDER_VERSION,
executionAddress,
balance: 0,
depositEpoch: 0,
Expand Down Expand Up @@ -185,7 +239,7 @@ describe("processBuilderDepositRequest", () => {
state.builders.push(
ssz.gloas.Builder.toViewDU({
pubkey,
version: BUILDER_WITHDRAWAL_PREFIX,
version: PAYLOAD_BUILDER_VERSION,
executionAddress,
balance: 1_000_000_000,
depositEpoch: 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {describe, expect, it} from "vitest";
import {createBeaconConfig} from "@lodestar/config";
import {getConfig} from "@lodestar/config/test-utils";
import {BUILDER_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH, ForkName, SLOTS_PER_EPOCH} from "@lodestar/params";
import {FAR_FUTURE_EPOCH, ForkName, PAYLOAD_BUILDER_VERSION, SLOTS_PER_EPOCH} from "@lodestar/params";
import {ssz} from "@lodestar/types";
import {processBuilderExitRequest} from "../../../src/block/processBuilderExitRequest.js";
import {createCachedBeaconState, createPubkeyCache} from "../../../src/index.js";
Expand Down Expand Up @@ -53,7 +53,7 @@ function pushBuilder(
state.builders.push(
ssz.gloas.Builder.toViewDU({
pubkey,
version: BUILDER_WITHDRAWAL_PREFIX,
version: PAYLOAD_BUILDER_VERSION,
executionAddress,
balance,
depositEpoch,
Expand Down
Loading