Skip to content

Commit

Permalink
Merge 9a291fc into 00dfa63
Browse files Browse the repository at this point in the history
  • Loading branch information
nazarhussain committed Feb 5, 2024
2 parents 00dfa63 + 9a291fc commit 26d4cf6
Show file tree
Hide file tree
Showing 12 changed files with 480 additions and 358 deletions.
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({
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 {
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;
}
if (err !== resolveTimeoutError) {
throw err;
}
} else {
eventCb(RaceEvent.precutoff, Date.now() - startTime);
return mapStatusesToResponses(promisesStates);
}

// 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),
sleep(raceTimeoutMs - resolveTimeoutMs, signal).then(() => {
throw raceTimeoutError;
}),
]);

return promiseResults;
} catch (err) {
if (err instanceof ErrorAborted) {
return promiseResults;
}
if (err !== raceTimeoutError && !(err instanceof AggregateError)) {
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>> = {
[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);
});
}
});
});

0 comments on commit 26d4cf6

Please sign in to comment.