Skip to content
Draft
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
95 changes: 50 additions & 45 deletions packages/beacon-node/src/api/impl/validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,40 +133,49 @@ type BidCandidate = {
};

/**
* Return the best bid by boosted total payment. A candidate with the max boost factor is
* preferred over any other regardless of value, ties prefer a builder api bid over the
* p2p bid, and the earlier received bid between builder api bids.
* Ranking value of a bid, the boosted total payment scaled by 100 as `boostFactor` is a percentage.
* Kept scaled so bids that differ by less than a percent are not truncated into a tie.
*/
function selectBestBid(candidates: BidCandidate[]): BidCandidate | null {
const boostedValue = ({totalGwei, boostFactor}: BidCandidate): bigint => boostFactor * totalGwei;
let best: BidCandidate | null = null;
for (const candidate of candidates) {
if (best === null) {
best = candidate;
continue;
}
// Preserve max boost preference before comparing bid values
const candidateIsMaxBoost = candidate.boostFactor === MAX_BUILDER_BOOST_FACTOR;
const bestIsMaxBoost = best.boostFactor === MAX_BUILDER_BOOST_FACTOR;
if (candidateIsMaxBoost !== bestIsMaxBoost) {
if (candidateIsMaxBoost) {
best = candidate;
}
continue;
}
const candidateValue = boostedValue(candidate);
const bestValue = boostedValue(best);
if (candidateValue > bestValue) {
best = candidate;
} else if (
candidateValue === bestValue &&
// A tie prefers a builder api bid over the p2p bid, and the earlier received bid otherwise
(best.url === undefined || (candidate.url !== undefined && candidate.receivedMs < best.receivedMs))
) {
best = candidate;
}
function getBoostedTotalScaled({totalGwei, boostFactor}: BidCandidate): bigint {
return boostFactor * totalGwei;
}

/** Order bid candidates by preference, best first, used to select a bid and to log the ranking */
function compareBidCandidates(a: BidCandidate, b: BidCandidate): number {
// Preserve max boost preference before comparing bid values
const aIsMaxBoost = a.boostFactor === MAX_BUILDER_BOOST_FACTOR;
const bIsMaxBoost = b.boostFactor === MAX_BUILDER_BOOST_FACTOR;
if (aIsMaxBoost !== bIsMaxBoost) {
return aIsMaxBoost ? -1 : 1;
}

const aBoostedTotal = getBoostedTotalScaled(a);
const bBoostedTotal = getBoostedTotalScaled(b);
if (aBoostedTotal !== bBoostedTotal) {
return aBoostedTotal > bBoostedTotal ? -1 : 1;
}

// A tie prefers a builder api bid over the p2p bid
const aIsBuilderApi = a.url !== undefined;
const bIsBuilderApi = b.url !== undefined;
if (aIsBuilderApi !== bIsBuilderApi) {
return aIsBuilderApi ? -1 : 1;
}
return best;

// and the earlier received bid between builder api bids
return a.receivedMs - b.receivedMs;
}

/** Render a bid candidate for the ranking log */
function formatBidCandidate(candidate: BidCandidate): string {
return `{${[
`source=${candidate.url !== undefined ? toPrintableUrl(candidate.url) : "p2p"}`,
`builder=${candidate.signedBid.message.builderIndex}`,
`total=${prettyGweiToEth(candidate.totalGwei)}`,
`boost=${candidate.boostFactor}`,
`boosted=${prettyGweiToEth(getBoostedTotalScaled(candidate) / 100n)}`,
`received=${candidate.receivedMs}ms`,
].join(", ")}}`;
}

type ProduceBlockContentsRes = {executionPayloadValue: Wei; consensusBlockValue: Wei} & {
Expand Down Expand Up @@ -1049,20 +1058,16 @@ export function getValidatorApi(
});
}

const best = selectBestBid(candidates);
if (candidates.length > 0) {
logger.debug("Ranked builder bid candidates", {
slot,
candidates: candidates
.map(
(candidate) =>
`${candidate.url ?? "p2p"}:total=${prettyGweiToEth(candidate.totalGwei)}:boost=${candidate.boostFactor}:received=${candidate.receivedMs}ms`
)
.join(","),
bidSource: best?.url ?? "p2p",
});
if (candidates.length === 0) {
return null;
}
return best;

const rankedCandidates = candidates.toSorted(compareBidCandidates);
logger.debug("Ranked builder bid candidates", {
slot,
candidates: rankedCandidates.map(formatBidCandidate).join(", "),
});
return rankedCandidates[0];
})();

const commonBlockBodyPromise = chain.produceCommonBlockBody({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,8 +408,10 @@ describe("api/validator - produceBlockV4", () => {
};
const apiBid = ssz.gloas.SignedExecutionPayloadBid.defaultValue();
apiBid.message.value = 1;
apiBid.message.builderIndex = 1;
const p2pBid = ssz.gloas.SignedExecutionPayloadBid.defaultValue();
p2pBid.message.value = 1;
p2pBid.message.builderIndex = 2;

modules.chain.builderCircuitBreaker.isActive.mockReturnValue(false);
modules.chain.executionPayloadBidPool.getBestBid.mockReturnValue(toPooledBid(p2pBid));
Expand All @@ -428,6 +430,8 @@ describe("api/validator - produceBlockV4", () => {
builderConfig: {minBid: 0n, builderBoostFactor: 150n, builders: [entry]},
});

// The bids only differ by a boost factor of 150 vs 100, truncating the boosted total would
// tie them and hand it to the api bid
expect(modules.chain.produceBlock).toHaveBeenCalledWith(expect.objectContaining({builderBid: p2pBid}));
});

Expand Down Expand Up @@ -464,6 +468,11 @@ describe("api/validator - produceBlockV4", () => {
});

expect(modules.chain.produceBlock).toHaveBeenCalledWith(expect.objectContaining({builderBid: apiBid}));

// The ranking log must agree with the selection, ranking by boosted value alone would put the
// zero value max boost bid last even though it wins
const ranked = modules.chain.logger.debug.mock.calls.find(([msg]) => msg === "Ranked builder bid candidates");
expect((ranked?.[1] as {candidates: string}).candidates.split("}, {")[0]).toContain(builderUrl);
});

it("falls back to the p2p bid when the builder API bid fails validation", async () => {
Expand Down Expand Up @@ -729,6 +738,37 @@ describe("api/validator - produceBlockV4", () => {
engineValueGwei: 0,
expected: 1,
},
{
// ⏎
id: "max boost between builders compares value",
entries: [
{value: 1, boostFactor: maxBuilderBoostFactor},
{value: 2, boostFactor: maxBuilderBoostFactor},
],
p2pValue: null,
engineValueGwei: 0,
expected: 1,
},
{
// ⏎
id: "max boost tie prefers the earlier received api bid",
entries: [
{value: 2, boostFactor: maxBuilderBoostFactor, receivedMs: 2000},
{value: 2, boostFactor: maxBuilderBoostFactor, receivedMs: 1500},
],
p2pValue: null,
engineValueGwei: 0,
expected: 1,
},
{
// ⏎
id: "max p2p boost wins over a higher api bid",
entries: [{value: 5}],
p2pValue: 1,
builderBoostFactor: maxBuilderBoostFactor,
engineValueGwei: 0,
expected: "p2p",
},
{
// ⏎
id: "p2p bid below min bid is discarded",
Expand Down
Loading