From da16cc64f3d5c293f6b0735d00e4eebebc0fbaaa Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Wed, 16 Sep 2026 21:29:05 +0100 Subject: [PATCH 1/3] fix: derive default builder auth data from the url hostname --- .../run/validator-management/vc-configuration.md | 2 +- packages/api/src/keymanager/routes.ts | 2 +- packages/cli/src/cmds/validator/options.ts | 2 +- packages/cli/src/util/proposerConfig.ts | 7 ++++--- .../test/unit/validator/parseBuilderUrls.test.ts | 2 +- packages/validator/src/services/validatorStore.ts | 9 ++++++--- .../validator/test/unit/validatorStore.test.ts | 15 +++++++++++++-- 7 files changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/pages/run/validator-management/vc-configuration.md b/docs/pages/run/validator-management/vc-configuration.md index e187d76c526e..c7cf597c804d 100644 --- a/docs/pages/run/validator-management/vc-configuration.md +++ b/docs/pages/run/validator-management/vc-configuration.md @@ -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. diff --git a/packages/api/src/keymanager/routes.ts b/packages/api/src/keymanager/routes.ts index e7427931f96b..ff4148fa7af8 100644 --- a/packages/api/src/keymanager/routes.ts +++ b/packages/api/src/keymanager/routes.ts @@ -107,7 +107,7 @@ export type BuilderBoostFactorData = ValueOf; 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[]; diff --git a/packages/cli/src/cmds/validator/options.ts b/packages/cli/src/cmds/validator/options.ts index 406dd4bed879..1780560c5066 100644 --- a/packages/cli/src/cmds/validator/options.ts +++ b/packages/cli/src/cmds/validator/options.ts @@ -290,7 +290,7 @@ export const validatorOptions: CliCommandOptions = { "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(",")), diff --git a/packages/cli/src/util/proposerConfig.ts b/packages/cli/src/util/proposerConfig.ts index 89d98d49d0cf..c38d9f8de1dd 100644 --- a/packages/cli/src/util/proposerConfig.ts +++ b/packages/cli/src/util/proposerConfig.ts @@ -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}`); @@ -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; @@ -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}`); } diff --git a/packages/cli/test/unit/validator/parseBuilderUrls.test.ts b/packages/cli/test/unit/validator/parseBuilderUrls.test.ts index b795536ea348..ac7305160f52 100644 --- a/packages/cli/test/unit/validator/parseBuilderUrls.test.ts +++ b/packages/cli/test/unit/validator/parseBuilderUrls.test.ts @@ -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/); }); diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index df6489a5a6a2..acc571c1eab4 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -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); @@ -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, @@ -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}`); diff --git a/packages/validator/test/unit/validatorStore.test.ts b/packages/validator/test/unit/validatorStore.test.ts index dc646c465294..1ff78b4469a6 100644 --- a/packages/validator/test/unit/validatorStore.test.ts +++ b/packages/validator/test/unit/validatorStore.test.ts @@ -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 @@ -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"; @@ -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 From 85628e6401fe7794c4544d26063d1cfa650dcf48 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Wed, 16 Sep 2026 21:50:09 +0100 Subject: [PATCH 2/3] add hostname to docs wordlist --- .wordlist.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.wordlist.txt b/.wordlist.txt index ebfefebe87b4..cb40db0504a9 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -54,6 +54,7 @@ Gossipsub Grafana Grandine Gwei +hostname HTTPS HackMD Hashicorp From 5ef8f4630836488c77987f106ba2d40e60a1c600 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Wed, 16 Sep 2026 21:52:47 +0100 Subject: [PATCH 3/3] sort docs wordlist --- .wordlist.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.wordlist.txt b/.wordlist.txt index cb40db0504a9..c91130b1b95e 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -54,7 +54,6 @@ Gossipsub Grafana Grandine Gwei -hostname HTTPS HackMD Hashicorp @@ -183,6 +182,7 @@ heapdump heaptrack holesky hoodi +hostname interop js keymanager