Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: update the block production race #6241

Merged
merged 41 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
31e489f
Update the promise race implementation
nazarhussain Dec 29, 2023
fdbc32c
Update block production race
nazarhussain Jan 19, 2024
e5dcc96
Fix spelling
nazarhussain Jan 19, 2024
95f088c
Work on feedback
nazarhussain Jan 22, 2024
c0334e7
Update the test file name
nazarhussain Jan 22, 2024
74e6d1e
Update the promise tests
nazarhussain Jan 22, 2024
8149719
Update util to switch
nazarhussain Jan 23, 2024
2c60bf9
chore: add early return on censoring builder or 0 builder boost
wemeetagain Jan 24, 2024
aab884f
Update the promise to extended promise
nazarhussain Jan 26, 2024
70a35b7
Update the builder flow
nazarhussain Jan 26, 2024
535fb23
Merge branch 'nh/6159-block-prod-race' of github.com:ChainSafe/lodest…
nazarhussain Jan 26, 2024
a1e8a0c
Fix the types
nazarhussain Jan 26, 2024
8698e46
Fix lint errors
nazarhussain Jan 26, 2024
c6150bb
Simplify logging
nazarhussain Jan 26, 2024
efd684c
Improve log messages for block values
nazarhussain Jan 26, 2024
6865972
Merge branch 'unstable' into nh/6159-block-prod-race
nazarhussain Jan 26, 2024
9c7942d
Update the promise to be typesafe
nazarhussain Jan 29, 2024
4597d06
Update the validator implementation
nazarhussain Jan 29, 2024
17686b9
Restructure test file for better review
nazarhussain Jan 29, 2024
f4c93e7
Fix lint errors
nazarhussain Jan 29, 2024
3391626
Fix lint error
nazarhussain Jan 29, 2024
73943ee
Make the tyep more flexible
nazarhussain Jan 29, 2024
374aeff
Fix flaky tests
nazarhussain Jan 29, 2024
6196428
Improve log message
nazarhussain Jan 29, 2024
7e6e91c
Simplify implementation
nazarhussain Jan 29, 2024
ddad503
Update log message function
nazarhussain Jan 29, 2024
7a2027a
chore: add review feedback
wemeetagain Jan 29, 2024
806c88f
Merge branch 'unstable' into nh/6159-block-prod-race
wemeetagain Jan 29, 2024
a0dc19f
chore: fix linter error
wemeetagain Jan 30, 2024
ba444cc
chore: address PR comments
wemeetagain Feb 1, 2024
f6e6619
Update packages/beacon-node/src/api/impl/validator/index.ts
wemeetagain Feb 1, 2024
e14889b
chore: address PR comments
wemeetagain Feb 1, 2024
15c7210
Merge branch 'unstable' into nh/6159-block-prod-race
wemeetagain Feb 1, 2024
689e087
chore: clean up selectBlockProductionSource
wemeetagain Feb 1, 2024
72e4a2b
Fix unit tests
nazarhussain Feb 5, 2024
76768b6
Add support for routes.validator.BuilderSelection.ExecutionOnly
nazarhussain Feb 5, 2024
f44fd8a
Fix unit tests
nazarhussain Feb 5, 2024
74fdad3
Increase the timeout for e2e env
nazarhussain Feb 5, 2024
64e6605
Apply suggestions from code review
nazarhussain Feb 5, 2024
9a291fc
Increase the timeout for e2e env
nazarhussain Feb 5, 2024
cb022e4
Revert e2e timeout
nazarhussain Feb 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
346 changes: 173 additions & 173 deletions packages/beacon-node/src/api/impl/validator/index.ts

Large diffs are not rendered by default.

32 changes: 31 additions & 1 deletion packages/beacon-node/src/api/impl/validator/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {BeaconStateAllForks, computeSlotsSinceEpochStart} from "@lodestar/state-transition";
import {ATTESTATION_SUBNET_COUNT} from "@lodestar/params";
import {BLSPubkey, CommitteeIndex, Slot, ValidatorIndex} from "@lodestar/types";
import {routes} from "@lodestar/api";
import {BLSPubkey, CommitteeIndex, ProducedBlockSource, Slot, ValidatorIndex} from "@lodestar/types";
import {MAX_BUILDER_BOOST_FACTOR} from "@lodestar/validator";

export function computeSubnetForCommitteesAtSlot(
slot: Slot,
Expand Down Expand Up @@ -41,3 +43,31 @@ export function getPubkeysForIndices(

return pubkeys;
}

export function selectBlockProductionSource({
nflaig marked this conversation as resolved.
Show resolved Hide resolved
builderSelection,
engineBlockValue,
builderBlockValue,
builderBoostFactor,
}: {
builderSelection: routes.validator.BuilderSelection;
engineBlockValue: bigint;
builderBlockValue: bigint;
builderBoostFactor: bigint;
}): ProducedBlockSource {
switch (builderSelection) {
case routes.validator.BuilderSelection.ExecutionAlways:
case routes.validator.BuilderSelection.ExecutionOnly:
return ProducedBlockSource.engine;

case routes.validator.BuilderSelection.MaxProfit:
return builderBoostFactor !== MAX_BUILDER_BOOST_FACTOR &&
(builderBoostFactor === BigInt(0) || engineBlockValue >= (builderBlockValue * builderBoostFactor) / BigInt(100))
? ProducedBlockSource.engine
: ProducedBlockSource.builder;

case routes.validator.BuilderSelection.BuilderAlways:
case routes.validator.BuilderSelection.BuilderOnly:
return ProducedBlockSource.builder;
}
}
27 changes: 27 additions & 0 deletions packages/utils/src/format.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {toHexString} from "./bytes.js";
import {ETH_TO_WEI} from "./ethConversion.js";

/**
* Format bytes as `0x1234…1234`
Expand Down Expand Up @@ -27,3 +28,29 @@ export function truncBytes(root: Uint8Array | string): string {
const str = typeof root === "string" ? root : toHexString(root);
return str.slice(0, 14);
}

/**
* Format a bigint value as a decimal string
*/
export function formatBigDecimal(numerator: bigint, denominator: bigint, maxDecimalFactor: bigint): string {
const full = numerator / denominator;
const fraction = ((numerator - full * denominator) * maxDecimalFactor) / denominator;

// zeros to be added post decimal are number of zeros in maxDecimalFactor - number of digits in fraction
const zerosPostDecimal = String(maxDecimalFactor).length - 1 - String(fraction).length;
return `${full}.${"0".repeat(zerosPostDecimal)}${fraction}`;
}

// display upto 5 decimal places
const MAX_DECIMAL_FACTOR = BigInt("100000");

/**
* Format wei as ETH, with up to 5 decimals
*
* if suffix is true, append ' ETH'
*/
export function prettyWeiToEth(wei: bigint, suffix = false): string {
nflaig marked this conversation as resolved.
Show resolved Hide resolved
let eth = formatBigDecimal(wei, ETH_TO_WEI, MAX_DECIMAL_FACTOR);
if (suffix) eth += " ETH";
return eth;
}
212 changes: 114 additions & 98 deletions packages/utils/src/promise.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {ErrorAborted, TimeoutError} from "./errors.js";
import {sleep} from "./sleep.js";
import {ArrayToTuple, NonEmptyArray} from "./types.js";

/**
* While promise t is not finished, call function `fn` per `interval`
Expand All @@ -25,115 +27,129 @@ export async function callFnWhenAwait<T>(
return t;
}

enum PromiseStatus {
resolved,
rejected,
pending,
}

type PromiseState<T> =
| {status: PromiseStatus.resolved; value: T}
| {status: PromiseStatus.rejected; value: Error}
| {status: PromiseStatus.pending; value: null};

function mapStatusesToResponses<T>(promisesStates: PromiseState<T>[]): (Error | T)[] {
return promisesStates.map((pmStatus) => {
switch (pmStatus.status) {
case PromiseStatus.resolved:
return pmStatus.value;
case PromiseStatus.rejected:
return pmStatus.value;
case PromiseStatus.pending:
return Error("pending");
export type PromiseResult<T> = {
promise: Promise<T>;
} & (
| {
status: "pending";
}
});
}
| {
status: "fulfilled";
value: T;
durationMs: number;
}
| {
status: "rejected";
reason: Error;
durationMs: number;
}
);
export type PromiseFulfilledResult<T> = PromiseResult<T> & {status: "fulfilled"};
export type PromiseRejectedResult<T> = PromiseResult<T> & {status: "rejected"};

export enum RaceEvent {
/** all reject/resolve before cutoff */
precutoff = "precutoff-return",
/** cutoff reached as some were pending till cutoff **/
cutoff = "cutoff-reached",
/** atleast one resolved till cutoff so no race required */
resolvedatcutoff = "resolved-at-cutoff",
/** if none reject/resolve before cutoff but one resolves or all reject before timeout */
pretimeout = "pretimeout-return",
/** timeout reached as none resolved and some were pending till timeout*/
timeout = "timeout-reached",
/**
* Wrap a promise to an object to track the status and value of the promise
*/
export function wrapPromise<T>(promise: PromiseLike<T>): PromiseResult<T> {
const startedAt = Date.now();

const result = {
promise: promise.then(
(value) => {
result.status = "fulfilled";
(result as PromiseFulfilledResult<T>).value = value;
(result as PromiseFulfilledResult<T>).durationMs = Date.now() - startedAt;
return value;
},
(reason: unknown) => {
result.status = "rejected";
(result as PromiseRejectedResult<T>).reason = reason as Error;
(result as PromiseRejectedResult<T>).durationMs = Date.now() - startedAt;
throw reason;
}
),
status: "pending",
} as PromiseResult<T>;

// events for the promises for better tracking
/** promise resolved */
resolved = "resolved",
/** promise rejected */
rejected = "rejected",
return result;
}

/**
* Wait for promises to resolve till cutoff and then race them beyond the cutoff with an overall timeout
* @return resolved values or rejections or still pending errors corresponding to input promises
* ArrayToTuple converts an `Array<T>` to `[T, ...T]`
*
* eg: `[1, 2, 3]` from type `number[]` to `[number, number, number]`
*/
export async function racePromisesWithCutoff<T>(
promises: Promise<T>[],
cutoffMs: number,
timeoutMs: number,
eventCb: (event: RaceEvent, delayMs: number, index?: number) => void
): Promise<(Error | T)[]> {
// start the cutoff and timeout timers
let cutoffObserved = false;
const cutoffPromise = sleep(cutoffMs).then(() => {
cutoffObserved = true;
});
let timeoutObserved = false;
const timeoutPromise = sleep(timeoutMs).then(() => {
timeoutObserved = true;
});
const startTime = Date.now();
type ReturnPromiseWithTuple<Tuple extends NonEmptyArray<PromiseLike<unknown>>> = {
[Index in keyof ArrayToTuple<Tuple>]: PromiseResult<Awaited<Tuple[Index]>>;
};

// Track promises status and resolved values/rejected errors
// Even if the promises reject with the following decoration promises will not throw
const promisesStates = [] as PromiseState<T>[];
promises.forEach((promise, index) => {
promisesStates[index] = {status: PromiseStatus.pending, value: null};
promise
.then((value) => {
eventCb(RaceEvent.resolved, Date.now() - startTime, index);
promisesStates[index] = {status: PromiseStatus.resolved, value};
})
.catch((e: Error) => {
eventCb(RaceEvent.rejected, Date.now() - startTime, index);
promisesStates[index] = {status: PromiseStatus.rejected, value: e};
});
});
/**
* Two phased approach for resolving promises:
* - first wait `resolveTimeoutMs` or until all promises settle
* - then wait `raceTimeoutMs - resolveTimeoutMs` or until at least a single promise resolves
*
* Returns a list of promise results, see `PromiseResult`
*/
export async function resolveOrRacePromises<T extends NonEmptyArray<PromiseLike<unknown>>>(
promises: T,
{
resolveTimeoutMs,
raceTimeoutMs,
signal,
}: {
resolveTimeoutMs: number;
raceTimeoutMs: number;
signal?: AbortSignal;
}
): Promise<ReturnPromiseWithTuple<T>> | never {
if (raceTimeoutMs <= resolveTimeoutMs) {
throw new Error("Race time must be greater than resolve time");
}
const resolveTimeoutError = new TimeoutError(
`Given promises can't be resolved within resolveTimeoutMs=${resolveTimeoutMs}`
);
const raceTimeoutError = new TimeoutError(
`Not a any single promise be resolved in given raceTimeoutMs=${raceTimeoutMs}`
);

// Wait till cutoff time unless all original promises resolve/reject early
await Promise.allSettled(promises.map((promise) => Promise.race([promise, cutoffPromise])));
if (cutoffObserved) {
// If any is resolved, then just simply return as we are post cutoff
const anyResolved = promisesStates.reduce(
(acc, pmState) => acc || pmState.status === PromiseStatus.resolved,
false
);
if (anyResolved) {
eventCb(RaceEvent.resolvedatcutoff, Date.now() - startTime);
return mapStatusesToResponses(promisesStates);
} else {
eventCb(RaceEvent.cutoff, Date.now() - startTime);
const promiseResults = promises.map((p) => wrapPromise(p)) as ReturnPromiseWithTuple<T>;
promises = (promiseResults as PromiseResult<T>[]).map((p) => p.promise) as unknown as T;

try {
await Promise.race([
Promise.allSettled(promises),
sleep(resolveTimeoutMs, signal).then(() => {
throw resolveTimeoutError;
}),
]);

return promiseResults;
} catch (err) {
if (err instanceof ErrorAborted) {
return promiseResults;
}
wemeetagain marked this conversation as resolved.
Show resolved Hide resolved
if (err !== resolveTimeoutError) {
throw err;
}
} else {
eventCb(RaceEvent.precutoff, Date.now() - startTime);
return mapStatusesToResponses(promisesStates);
}

nazarhussain marked this conversation as resolved.
Show resolved Hide resolved
// Post deadline resolve with any of the promise or all rejected before timeout
await Promise.any(promises.map((promise) => Promise.race([promise, timeoutPromise]))).catch(
// just ignore if all reject as we will returned mapped rejections
// eslint-disable-next-line @typescript-eslint/no-empty-function
(_e) => {}
);
if (timeoutObserved) {
eventCb(RaceEvent.timeout, Date.now() - startTime);
} else {
eventCb(RaceEvent.pretimeout, Date.now() - startTime);
try {
await Promise.race([
Promise.any(promises),
wemeetagain marked this conversation as resolved.
Show resolved Hide resolved
sleep(raceTimeoutMs - resolveTimeoutMs, signal).then(() => {
throw raceTimeoutError;
}),
]);

return promiseResults;
} catch (err) {
if (err instanceof ErrorAborted) {
return promiseResults;
}
if (err !== raceTimeoutError && !(err instanceof AggregateError)) {
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved
throw err;
}
}
return mapStatusesToResponses(promisesStates);

return promiseResults;
}
6 changes: 6 additions & 0 deletions packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ export type RecursivePartial<T> = {
export function bnToNum(bn: bigint): number {
return Number(bn);
}

export type NonEmptyArray<T> = [T, ...T[]];

export type ArrayToTuple<Tuple extends NonEmptyArray<unknown>> = {
nflaig marked this conversation as resolved.
Show resolved Hide resolved
[Index in keyof Tuple]: Tuple[Index];
};
23 changes: 23 additions & 0 deletions packages/utils/test/unit/format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {describe, it, expect} from "vitest";
import {formatBigDecimal} from "../../src/format.js";

describe("format", () => {
describe("formatBigDecimal", () => {
const testCases: [bigint, bigint, bigint, string][] = [
[BigInt("103797739275696858"), BigInt("1000000000000000000"), BigInt("100000"), "0.10379"],
[BigInt("103797739275696858"), BigInt("1000000000000000000"), BigInt("1000"), "0.103"],
[BigInt("10379773927569685"), BigInt("1000000000000000000"), BigInt("1000"), "0.010"],
[BigInt("1037977392756968"), BigInt("1000000000000000000"), BigInt("1000"), "0.001"],
[BigInt("1037977392756968"), BigInt("1000000000000000000"), BigInt("100000"), "0.00103"],
[BigInt("58200000000000000"), BigInt("1000000000000000000"), BigInt("100000"), "0.05820"],
[BigInt("111103797739275696858"), BigInt("1000000000000000000"), BigInt("100000"), "111.10379"],
[BigInt("111103797739275696858"), BigInt("1000000000000000000"), BigInt("1000"), "111.103"],
[BigInt("1037977392756"), BigInt("1000000000000000000"), BigInt("100000"), "0.00000"],
];
for (const [numerator, denominator, decimalFactor, expectedString] of testCases) {
it(`format ${numerator} / ${denominator} correctly to ${expectedString}`, () => {
expect(formatBigDecimal(numerator, denominator, decimalFactor)).toBe(expectedString);
});
}
});
});