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
1 change: 1 addition & 0 deletions .wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ heapdump
heaptrack
holesky
hoodi
hostname
interop
js
keymanager
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/run/validator-management/vc-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Example 3: Setting a `--builder.boostFactor=100` is the same as signaling `--bui

Starting with Gloas, external builders are configured on the validator client and the beacon node requests bids on its behalf. Use [`--builder.urls`](./validator-cli.md#--builderurls) to name the builders to request bids from. Viable bids received over p2p are considered alongside them unless the builder circuit breaker is active, governed by the same selection settings. These are additional settings, the pre-Gloas builder flags still apply.

Every bid request is authenticated with data the builder expects, by default the UTF-8 bytes of the builder URL exactly as configured. If a builder requires different auth data agreed out of band, append it as a hex fragment to its URL, e.g. `--builder.urls https://builder.example.com#0x0123`. The fragment is stripped before the URL is used and never sent to the builder. Auth data that must stay secret is better kept in the [proposer configuration file](./proposer-config.md), as command line arguments are visible to other processes.
Every bid request is authenticated with data the builder expects, by default the hostname of the builder URL. If a builder requires different auth data agreed out of band, append it as a hex fragment to its URL, e.g. `--builder.urls https://builder.example.com#0x0123`. The fragment is stripped before the URL is used and never sent to the builder. Auth data that must stay secret is better kept in the [proposer configuration file](./proposer-config.md), as command line arguments are visible to other processes.

- [`--builder.minBid`](./validator-cli.md#--builderminbid): minimum counted total payment in Gwei accepted from a builder bid. The total is `value + min(execution_payment, max_execution_payment)`, capped at max uint64. Bids below the floor are discarded.
- [`--builder.maxExecutionPayment`](./validator-cli.md#--buildermaxexecutionpayment): maximum execution layer payment in Gwei counted from a builder bid, any payment above it adds nothing to the bid when comparing it with other bids. The default of `0` only counts trustless payments backed by the builder's staked collateral. An execution layer payment is only a promise by the builder to pay as part of the block, so values above `0` require the explicit [`--allowDangerousTrustedPayments`](./validator-cli.md#--allowdangeroustrustedpayments) opt-in.
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/keymanager/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export type BuilderBoostFactorData = ValueOf<typeof BuilderBoostFactorDataType>;
export type BuilderEntryConfig = {
/** URL the bid requests for this entry are sent to */
url: string;
/** Auth data hex string as agreed with the builder, derived from the UTF-8 bytes of the builder url when omitted */
/** Auth data hex string as agreed with the builder, derived from the builder url hostname when omitted */
authData?: string;
/** Builder BLS pubkeys this entry accepts bids from, empty or omitted accepts any builder */
builderPubkeys?: string[];
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/cmds/validator/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export const validatorOptions: CliCommandOptions<IValidatorCliArgs> = {

"builder.urls": {
description:
"URL(s) of external builders to request execution payload bids from. Auth data agreed with a builder may be appended as a hex fragment, e.g. https://builder.example.com#0x0123, otherwise the UTF-8 bytes of the URL are used. Only used post-Gloas",
"URL(s) of external builders to request execution payload bids from. Auth data agreed with a builder may be appended as a hex fragment, e.g. https://builder.example.com#0x0123, otherwise the hostname of the URL is used. Only used post-Gloas",
type: "array",
string: true,
coerce: (urls: string[]): string[] => urls.flatMap((url) => url.split(",")),
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/util/proposerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ export function parseBuilderEntries(builders?: unknown): BuilderEntryConfig[] |
if (!isValidAsciiHttpUrl(entry.url)) {
throw Error(`Invalid builder url: ${entry.url}`);
}
const authData = entry.authData !== undefined ? toHex(fromHex(entry.authData)) : toHex(Buffer.from(entry.url));
const authData =
entry.authData !== undefined ? toHex(fromHex(entry.authData)) : toHex(Buffer.from(new URL(entry.url).hostname));
const entryKey = `${entry.url}|${authData}`;
if (seenEntries.has(entryKey)) {
throw Error(`Duplicate builder entry url=${entry.url}`);
Expand All @@ -239,7 +240,7 @@ export function parseBuilderEntries(builders?: unknown): BuilderEntryConfig[] |
/**
* Parse builder urls into builder entries. Auth data agreed with a builder out of band may be
* appended as a hex fragment (`https://builder.example.com#0x0123`), it is stripped from the url
* and never sent on the wire. Without a fragment the auth data derives from the url.
* and never sent on the wire. Without a fragment the auth data derives from the url hostname.
*/
export function parseBuilderUrls(urls?: string[]): BuilderEntryConfig[] | undefined {
if (urls === undefined) return undefined;
Expand All @@ -261,7 +262,7 @@ export function parseBuilderUrls(urls?: string[]): BuilderEntryConfig[] | undefi
`Invalid builder url auth data, must be a 0x-prefixed hex string of 1 to ${MAX_BUILDER_AUTH_DATA_SIZE} bytes: ${url}`
);
}
const entryKey = `${url}|${authData !== undefined ? toHex(fromHex(authData)) : toHex(Buffer.from(url))}`;
const entryKey = `${url}|${authData !== undefined ? toHex(fromHex(authData)) : toHex(Buffer.from(new URL(url).hostname))}`;
if (seenEntries.has(entryKey)) {
throw Error(`Duplicate builder url: ${url}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe("validator / parseBuilderUrls", () => {

it("rejects duplicate entries, comparing an omitted auth data as derived from the url", () => {
const url = "https://builder.example.com";
const derived = `0x${Buffer.from(url).toString("hex")}`;
const derived = `0x${Buffer.from("builder.example.com").toString("hex")}`;
expect(() => parseBuilderUrls([url, url])).toThrow(/Duplicate builder url/);
expect(() => parseBuilderUrls([url, `${url}#${derived}`])).toThrow(/Duplicate builder url/);
});
Expand Down
9 changes: 6 additions & 3 deletions packages/validator/src/services/validatorStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ export class ValidatorStore {
/**
* Resolve the builder entries for this key. Per-key entries replace the validator client's
* builders. A value omitted on an entry takes this key's default, then the validator
* client's configuration, while omitted auth data is derived from the entry url instead.
* client's configuration, while omitted auth data is derived from the entry url hostname instead.
*/
getResolvedBuilderEntries(pubkeyHex: PubkeyHex, boostFactor?: bigint): ResolvedBuilderEntry[] {
const validatorData = this.validators.get(pubkeyHex);
Expand All @@ -493,7 +493,8 @@ export class ValidatorStore {
const builders = validatorData.builder?.builders ?? this.defaultProposerConfig.builder.builders ?? [];
return builders.map((entry) => ({
url: entry.url,
authData: entry.authData !== undefined ? fromHex(entry.authData) : new TextEncoder().encode(entry.url),
authData:
entry.authData !== undefined ? fromHex(entry.authData) : new TextEncoder().encode(new URL(entry.url).hostname),
builderPubkeys: (entry.builderPubkeys ?? []).map(fromHex),
maxExecutionPayment: entry.maxExecutionPayment ?? keyMaxExecutionPayment,
minBid: entry.minBid ?? keyMinBid,
Expand Down Expand Up @@ -544,7 +545,9 @@ export class ValidatorStore {
throw Error(`Invalid builder url: ${entry.url}`);
}
const authData =
entry.authData !== undefined ? toHex(fromHex(entry.authData)) : toHex(new TextEncoder().encode(entry.url));
entry.authData !== undefined
? toHex(fromHex(entry.authData))
: toHex(new TextEncoder().encode(new URL(entry.url).hostname));
const entryKey = `${entry.url}|${authData}`;
if (seenEntries.has(entryKey)) {
throw Error(`Duplicate builder entry url=${entry.url} authData=${authData}`);
Expand Down
15 changes: 13 additions & 2 deletions packages/validator/test/unit/validatorStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ describe("ValidatorStore", () => {
const entries = validatorStore.getResolvedBuilderEntries(pubkey);
expect(entries).toHaveLength(2);
// Omitted auth data derives from the entry url, omitted min bid takes the key default
expect(Buffer.from(entries[0].authData).toString("utf8")).toBe(builderUrl);
expect(Buffer.from(entries[0].authData).toString("utf8")).toBe("builder.example.com");
expect(entries[0].minBid).toBe(10n);
expect(entries[0].maxExecutionPayment).toBe(5n);
// Per-entry values win over the key defaults
Expand All @@ -279,6 +279,17 @@ describe("ValidatorStore", () => {
expect(validatorStore.getBuilderMinBid(pubkey)).toBe(0n);
});

it("Should derive an omitted auth data from the url hostname", () => {
const pubkey = toHexString(pubkeys[0]);
validatorStore.setBuilderConfig(pubkey, {
builders: [{url: "https://Builder.Example.com:443/bids/?x=1"}, {url: "https://builder.example.com"}],
});

const entries = validatorStore.getResolvedBuilderEntries(pubkey);
expect(Buffer.from(entries[0].authData).toString("utf8")).toBe("builder.example.com");
expect(entries[0].authData).toEqual(entries[1].authData);
});

it("Should resolve the validator client's default builder entries with key defaults applied", async () => {
const pubkey = toHexString(pubkeys[0]);
const builderUrl = "https://builder.example.com";
Expand All @@ -301,7 +312,7 @@ describe("ValidatorStore", () => {
// A key without its own builders follows the default entries, its key defaults still apply
const entries = store.getResolvedBuilderEntries(pubkey);
expect(entries).toHaveLength(2);
expect(Buffer.from(entries[0].authData).toString("utf8")).toBe(builderUrl);
expect(Buffer.from(entries[0].authData).toString("utf8")).toBe("builder.example.com");
expect(entries[0].minBid).toBe(30n);
expect(entries[0].builderBoostFactor).toBe(150n);
// Explicit auth data (e.g. from a --builder.urls fragment) is used as is
Expand Down
Loading