diff --git a/scripts/run_native_testnet.sh b/scripts/run_native_testnet.sh index 12aa06efadeb..2bd097b46ee9 100755 --- a/scripts/run_native_testnet.sh +++ b/scripts/run_native_testnet.sh @@ -35,7 +35,7 @@ METRICS=false DISABLE_BLOB_SINK=false LOG_LEVEL="info" ETHEREUM_HOSTS= -L1_CONSENSUS_HOST_URL= +L1_CONSENSUS_HOST_URLS= OTEL_COLLECTOR_ENDPOINT=${OTEL_COLLECTOR_ENDPOINT:-"http://localhost:4318"} @@ -55,7 +55,7 @@ function display_help { echo " -c Specify the otel collector endpoint (default: $OTEL_COLLECTOR_ENDPOINT)" echo " -b Disable the blob sink (default: false)" echo " -e Specify the ethereum host url (default: $ETHEREUM_HOSTS)" - echo " -cl Specify the l1 consensus host url (default: $L1_CONSENSUS_HOST_URL)" + echo " -cl Specify the l1 consensus host urls (default: $L1_CONSENSUS_HOST_URLS)" echo echo "Example:" echo " $0 -t ./test-4epochs.sh -val 5 -v" @@ -109,7 +109,7 @@ while [[ $# -gt 0 ]]; do shift 2 ;; -cl) - L1_CONSENSUS_HOST_URL="$2" + L1_CONSENSUS_HOST_URLS="$2" shift 2 ;; -b) @@ -138,8 +138,8 @@ fi if [ -n "$ETHEREUM_HOSTS" ]; then export ETHEREUM_HOSTS fi -if [ -n "$L1_CONSENSUS_HOST_URL" ]; then - export L1_CONSENSUS_HOST_URL +if [ -n "$L1_CONSENSUS_HOST_URLS" ]; then + export L1_CONSENSUS_HOST_URLS fi # If an ethereum url has been provided, do not run the ethereum.sh script diff --git a/spartan/aztec-network/files/config/setup-service-addresses.sh b/spartan/aztec-network/files/config/setup-service-addresses.sh index bfae91a1b452..43433e7c44e1 100644 --- a/spartan/aztec-network/files/config/setup-service-addresses.sh +++ b/spartan/aztec-network/files/config/setup-service-addresses.sh @@ -130,9 +130,9 @@ fi # Write addresses to file for sourcing echo "export ETHEREUM_HOSTS=${ETHEREUM_ADDR}" >>/shared/config/service-addresses -echo "export L1_CONSENSUS_HOST_URL=${ETHEREUM_CONSENSUS_ADDR}" >>/shared/config/service-addresses -echo "export L1_CONSENSUS_HOST_API_KEY=${EXTERNAL_ETHEREUM_CONSENSUS_HOST_API_KEY}" >>/shared/config/service-addresses -echo "export L1_CONSENSUS_HOST_API_KEY_HEADER=${EXTERNAL_ETHEREUM_CONSENSUS_HOST_API_KEY_HEADER}" >>/shared/config/service-addresses +echo "export L1_CONSENSUS_HOST_URLS=${ETHEREUM_CONSENSUS_ADDR}" >>/shared/config/service-addresses +echo "export L1_CONSENSUS_HOST_API_KEYS=${EXTERNAL_ETHEREUM_CONSENSUS_HOST_API_KEY}" >>/shared/config/service-addresses +echo "export L1_CONSENSUS_HOST_API_KEY_HEADERS=${EXTERNAL_ETHEREUM_CONSENSUS_HOST_API_KEY_HEADER}" >>/shared/config/service-addresses echo "export BOOT_NODE_HOST=${BOOT_NODE_ADDR}" >>/shared/config/service-addresses echo "export PROVER_NODE_HOST=${PROVER_NODE_ADDR}" >>/shared/config/service-addresses echo "export PROVER_BROKER_HOST=${PROVER_BROKER_ADDR}" >>/shared/config/service-addresses diff --git a/spartan/releases/testnet/aztec-sequencer.sh b/spartan/releases/testnet/aztec-sequencer.sh index dc817efd6c4e..71bfb932c7ce 100755 --- a/spartan/releases/testnet/aztec-sequencer.sh +++ b/spartan/releases/testnet/aztec-sequencer.sh @@ -19,6 +19,7 @@ DEFAULT_BIND_MOUNT_DIR="$HOME/aztec-data" # unset these to avoid conflicts with the host's environment ETHEREUM_HOSTS= +L1_CONSENSUS_HOST_URLS= IMAGE= BOOTNODE_URL= LOG_LEVEL=info @@ -42,8 +43,8 @@ parse_args() { ETHEREUM_HOSTS="$2" shift 2 ;; - -l | --l1-consensus-host-url) - L1_CONSENSUS_HOST_URL="$2" + -l | --l1-consensus-host-urls) + L1_CONSENSUS_HOST_URLS="$2" shift 2 ;; -p | --port) @@ -150,13 +151,13 @@ configure_environment() { done fi - if [ -n "$L1_CONSENSUS_HOST_URL" ]; then - L1_CONSENSUS_HOST_URL="$L1_CONSENSUS_HOST_URL" + if [ -n "$L1_CONSENSUS_HOST_URLS" ]; then + L1_CONSENSUS_HOST_URLS="$L1_CONSENSUS_HOST_URLS" else while true; do - read -p "L1 Consensus Host URL: " L1_CONSENSUS_HOST_URL - if [ -z "$L1_CONSENSUS_HOST_URL" ]; then - echo -e "${RED}Error: L1 Consensus Host URL is required${NC}" + read -p "L1 Consensus Host URLs: " L1_CONSENSUS_HOST_URLS + if [ -z "$L1_CONSENSUS_HOST_URLS" ]; then + echo -e "${RED}Error: L1 Consensus Host URLs are required${NC}" else break fi @@ -260,7 +261,7 @@ PXE_PROVER_ENABLED=true ETHEREUM_SLOT_DURATION=12 AZTEC_SLOT_DURATION=36 ETHEREUM_HOSTS=${ETHEREUM_HOSTS} -L1_CONSENSUS_HOST_URL=${L1_CONSENSUS_HOST_URL} +L1_CONSENSUS_HOST_URLS=${L1_CONSENSUS_HOST_URLS} AZTEC_EPOCH_DURATION=32 AZTEC_PROOF_SUBMISSION_WINDOW=64 BOOTSTRAP_NODES=${BOOTSTRAP_NODES} diff --git a/yarn-project/archiver/src/archiver/config.ts b/yarn-project/archiver/src/archiver/config.ts index 8fc408b04cf5..0ef05539312d 100644 --- a/yarn-project/archiver/src/archiver/config.ts +++ b/yarn-project/archiver/src/archiver/config.ts @@ -23,8 +23,8 @@ export type ArchiverConfig = { /** URL for an archiver service. If set, will return an archiver client as opposed to starting a new one. */ archiverUrl?: string; - /** URL for an L1 consensus client */ - l1ConsensusHostUrl?: string; + /** List of URLS for L1 consensus clients */ + l1ConsensusHostUrls?: string[]; /** The polling interval in ms for retrieving new L2 blocks and encrypted logs. */ archiverPollingIntervalMS?: number; @@ -55,10 +55,10 @@ export const archiverConfigMappings: ConfigMappingsType = { description: 'URL for an archiver service. If set, will return an archiver client as opposed to starting a new one.', }, - l1ConsensusHostUrl: { - env: 'L1_CONSENSUS_HOST_URL', - description: 'URL for an L1 consensus client.', - parseEnv: (val: string) => val, + l1ConsensusHostUrls: { + env: 'L1_CONSENSUS_HOST_URLS', + description: 'List of URLS for L1 consensus clients.', + parseEnv: (val: string) => val.split(',').map(url => url.trim().replace(/\/$/, '')), }, archiverPollingIntervalMS: { env: 'ARCHIVER_POLLING_INTERVAL_MS', diff --git a/yarn-project/aztec/src/cli/aztec_start_options.ts b/yarn-project/aztec/src/cli/aztec_start_options.ts index 9f5074394e7f..e7fa3d0f9e87 100644 --- a/yarn-project/aztec/src/cli/aztec_start_options.ts +++ b/yarn-project/aztec/src/cli/aztec_start_options.ts @@ -55,9 +55,9 @@ export const getOptions = (namespace: string, configMappings: Record', - description: 'URL of the Ethereum consensus node that services will connect to', - defaultValue: undefined, - envVar: 'L1_CONSENSUS_HOST_URL', + flag: '--l1-consensus-host-urls ', + description: 'List of URLs of the Ethereum consensus nodes that services will connect to (comma separated)', + defaultValue: [], + envVar: 'L1_CONSENSUS_HOST_URLS', + parseVal: (val: string) => val.split(',').map(url => url.trim().replace(/\/$/, '')), }, { - flag: '--l1-consensus-host-api-key ', - description: 'API key for the Ethereum consensus node', - defaultValue: undefined, - envVar: 'L1_CONSENSUS_HOST_API_KEY', + flag: '--l1-consensus-host-api-keys ', + description: 'List of API keys for the corresponding Ethereum consensus nodes', + defaultValue: [], + envVar: 'L1_CONSENSUS_HOST_API_KEYS', + parseVal: (val: string) => val.split(',').map(url => url.trim()), }, { - flag: '--l1-consensus-host-api-key-header ', + flag: '--l1-consensus-host-api-key-headers ', description: - 'API key header for the Ethereum consensus node. If not set, the api key will be appended to the URL as ?key=', - defaultValue: undefined, - envVar: 'L1_CONSENSUS_HOST_API_KEY_HEADER', + 'List of API key headers for the corresponding Ethereum consensus nodes. If not set, the api key for the corresponding node will be appended to the URL as ?key=', + defaultValue: [], + envVar: 'L1_CONSENSUS_HOST_API_KEY_HEADERS', + parseVal: (val: string) => val.split(',').map(url => url.trim()), }, ], STORAGE: [ diff --git a/yarn-project/blob-sink/README.md b/yarn-project/blob-sink/README.md index 3e6d4541e251..21489dbb084a 100644 --- a/yarn-project/blob-sink/README.md +++ b/yarn-project/blob-sink/README.md @@ -26,7 +26,7 @@ If no blob sink url or consensus host url is provided: A local version of the blob sink will be used. This stores blobs in a local file system. Blob sink url is provided: -If requesting from the blob sink, we send the blobkHash +If requesting from the blob sink, we send the blockHash Consensus host url is provided: If requesting from the beacon node, we send the slot number diff --git a/yarn-project/blob-sink/src/client/config.ts b/yarn-project/blob-sink/src/client/config.ts index ecf1c5bf9559..c060dafc26ca 100644 --- a/yarn-project/blob-sink/src/client/config.ts +++ b/yarn-project/blob-sink/src/client/config.ts @@ -17,19 +17,19 @@ export interface BlobSinkConfig extends BlobSinkArchiveApiConfig { l1RpcUrls?: string[]; /** - * The URL of the L1 consensus client + * List of URLs for L1 consensus clients */ - l1ConsensusHostUrl?: string; + l1ConsensusHostUrls?: string[]; /** - * The API key for the L1 consensus client. Added end of URL as "?key=" unless a header is defined + * List of API keys for the corresponding L1 consensus client URLs. Added at the end of the URL as "?key=" unless a header is defined */ - l1ConsensusHostApiKey?: string; + l1ConsensusHostApiKeys?: string[]; /** - * The header name for the L1 consensus client API key, if needed. Added as ": " + * List of header names for the corresponding L1 consensus client API keys, if needed. Added as ": " */ - l1ConsensusHostApiKeyHeader?: string; + l1ConsensusHostApiKeyHeaders?: string[]; } export const blobSinkConfigMapping: ConfigMappingsType = { @@ -42,19 +42,22 @@ export const blobSinkConfigMapping: ConfigMappingsType = { description: 'List of URLs for L1 RPC Execution clients', parseEnv: (val: string) => val.split(',').map(url => url.trim()), }, - l1ConsensusHostUrl: { - env: 'L1_CONSENSUS_HOST_URL', - description: 'The URL of the L1 consensus client', + l1ConsensusHostUrls: { + env: 'L1_CONSENSUS_HOST_URLS', + description: 'List of URLS for L1 consensus clients', + parseEnv: (val: string) => val.split(',').map(url => url.trim().replace(/\/$/, '')), }, - l1ConsensusHostApiKey: { - env: 'L1_CONSENSUS_HOST_API_KEY', + l1ConsensusHostApiKeys: { + env: 'L1_CONSENSUS_HOST_API_KEYS', description: - 'The API key for the L1 consensus client, if needed. Added end of URL as "?key=" unless a header is defined', + 'List of API keys for the corresponding L1 consensus clients, if needed. Added to the end of the corresponding URL as "?key=" unless a header is defined', + parseEnv: (val: string) => val.split(',').map(url => url.trim()), }, - l1ConsensusHostApiKeyHeader: { - env: 'L1_CONSENSUS_HOST_API_KEY_HEADER', + l1ConsensusHostApiKeyHeaders: { + env: 'L1_CONSENSUS_HOST_API_KEY_HEADERS', description: - 'The header name for the L1 consensus client API key, if needed. Added as ": "', + 'List of header names for the corresponding L1 consensus client API keys, if needed. Added to the corresponding request as ": "', + parseEnv: (val: string) => val.split(',').map(url => url.trim()), }, ...blobSinkArchiveApiConfigMappings, }; diff --git a/yarn-project/blob-sink/src/client/factory.ts b/yarn-project/blob-sink/src/client/factory.ts index 24d2e7b2a337..f03a0010e62b 100644 --- a/yarn-project/blob-sink/src/client/factory.ts +++ b/yarn-project/blob-sink/src/client/factory.ts @@ -5,7 +5,11 @@ import type { BlobSinkClientInterface } from './interface.js'; import { LocalBlobSinkClient } from './local.js'; export function createBlobSinkClient(config?: BlobSinkConfig): BlobSinkClientInterface { - if (!config?.blobSinkUrl && !config?.l1ConsensusHostUrl && !config?.archiveApiUrl) { + if ( + !config?.blobSinkUrl && + (!config?.l1ConsensusHostUrls || config?.l1ConsensusHostUrls?.length == 0) && + !config?.archiveApiUrl + ) { const blobStore = new MemoryBlobStore(); return new LocalBlobSinkClient(blobStore); } diff --git a/yarn-project/blob-sink/src/client/http.test.ts b/yarn-project/blob-sink/src/client/http.test.ts index 19f85e5ed668..2800311c3cb2 100644 --- a/yarn-project/blob-sink/src/client/http.test.ts +++ b/yarn-project/blob-sink/src/client/http.test.ts @@ -118,8 +118,27 @@ describe('HttpBlobSinkClient', () => { }); }; - const startConsensusHostServer = (): Promise => { + const startConsensusHostServer = (requireApiKey?: string, requireApiKeyHeader?: string): Promise => { consensusHostServer = http.createServer((req, res) => { + let isAuthorized = true; + if (requireApiKey) { + if (requireApiKeyHeader) { + const authHeader = req.headers[requireApiKeyHeader.toLowerCase()]; + isAuthorized = authHeader === requireApiKey; + } else { + const url = new URL(req.url || '', `http://${req.headers.host}`); + const apiKey = url.searchParams.get('key'); + isAuthorized = apiKey === requireApiKey; + } + } + + // If API key is required but not valid, reject the request + if (requireApiKey && !isAuthorized) { + res.writeHead(401, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Unauthorized: Invalid API key' })); + return; + } + if (req.url?.includes('/eth/v1/beacon/headers/')) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ data: { header: { message: { slot: MOCK_SLOT_NUMBER } } } })); @@ -189,11 +208,152 @@ describe('HttpBlobSinkClient', () => { const client = new HttpBlobSinkClient({ l1RpcUrls: [`http://localhost:${executionHostPort}`], - l1ConsensusHostUrl: `http://localhost:${consensusHostPort}`, + l1ConsensusHostUrls: [`http://localhost:${consensusHostPort}`], + }); + + const retrievedBlobs = await client.getBlobSidecar('0x1234', [testEncodedBlobHash]); + expect(retrievedBlobs).toEqual([testEncodedBlob]); + }); + + it('should handle when multiple consensus hosts are provided', async () => { + await startExecutionHostServer(); + await startConsensusHostServer(); + + const client = new HttpBlobSinkClient({ + l1RpcUrls: [`http://localhost:${executionHostPort}`], + l1ConsensusHostUrls: ['invalidURL', `http://localhost:${consensusHostPort}`, 'invalidURL'], + }); + + const retrievedBlobs = await client.getBlobSidecar('0x1234', [testEncodedBlobHash]); + expect(retrievedBlobs).toEqual([testEncodedBlob]); + }); + + it('should handle API keys without headers', async () => { + await startExecutionHostServer(); + await startConsensusHostServer('test-api-key'); + + const client = new HttpBlobSinkClient({ + l1RpcUrls: [`http://localhost:${executionHostPort}`], + l1ConsensusHostUrls: [`http://localhost:${consensusHostPort}`], + l1ConsensusHostApiKeys: ['test-api-key'], + }); + + const retrievedBlobs = await client.getBlobSidecar('0x1234', [testEncodedBlobHash]); + expect(retrievedBlobs).toEqual([testEncodedBlob]); + + const clientWithNoKey = new HttpBlobSinkClient({ + l1RpcUrls: [`http://localhost:${executionHostPort}`], + l1ConsensusHostUrls: [`http://localhost:${consensusHostPort}`], + l1ConsensusHostApiKeys: [], + }); + + const retrievedBlobsWithNoKey = await clientWithNoKey.getBlobSidecar('0x1234', [testEncodedBlobHash]); + expect(retrievedBlobsWithNoKey).toEqual([]); + + const clientWithInvalidKey = new HttpBlobSinkClient({ + l1RpcUrls: [`http://localhost:${executionHostPort}`], + l1ConsensusHostUrls: [`http://localhost:${consensusHostPort}`], + l1ConsensusHostApiKeys: ['invalid-key'], + }); + + const retrievedBlobsWithInvalidKey = await clientWithInvalidKey.getBlobSidecar('0x1234', [testEncodedBlobHash]); + expect(retrievedBlobsWithInvalidKey).toEqual([]); + }); + + it('should handle API keys in headers', async () => { + await startExecutionHostServer(); + await startConsensusHostServer('header-api-key', 'X-API-KEY'); + + const client = new HttpBlobSinkClient({ + l1RpcUrls: [`http://localhost:${executionHostPort}`], + l1ConsensusHostUrls: [`http://localhost:${consensusHostPort}`], + l1ConsensusHostApiKeys: ['header-api-key'], + l1ConsensusHostApiKeyHeaders: ['X-API-KEY'], }); const retrievedBlobs = await client.getBlobSidecar('0x1234', [testEncodedBlobHash]); expect(retrievedBlobs).toEqual([testEncodedBlob]); + + const clientWithWrongHeader = new HttpBlobSinkClient({ + l1RpcUrls: [`http://localhost:${executionHostPort}`], + l1ConsensusHostUrls: [`http://localhost:${consensusHostPort}`], + l1ConsensusHostApiKeys: ['header-api-key'], + l1ConsensusHostApiKeyHeaders: ['WRONG-HEADER'], + }); + + const retrievedBlobsWithWrongHeader = await clientWithWrongHeader.getBlobSidecar('0x1234', [testEncodedBlobHash]); + expect(retrievedBlobsWithWrongHeader).toEqual([]); + + const clientWithWrongKey = new HttpBlobSinkClient({ + l1RpcUrls: [`http://localhost:${executionHostPort}`], + l1ConsensusHostUrls: [`http://localhost:${consensusHostPort}`], + l1ConsensusHostApiKeys: ['invalid-key'], + l1ConsensusHostApiKeyHeaders: ['X-API-KEY'], + }); + + const retrievedBlobsWithWrongKey = await clientWithWrongKey.getBlobSidecar('0x1234', [testEncodedBlobHash]); + expect(retrievedBlobsWithWrongKey).toEqual([]); + }); + + it('should handle multiple consensus hosts with different API key methods', async () => { + await startExecutionHostServer(); + + // Create three separate servers for each API key scenario + await startConsensusHostServer(); + const consensusPort1 = consensusHostPort; + const consensusServer1 = consensusHostServer; + await startConsensusHostServer('test-api-key'); + const consensusPort2 = consensusHostPort; + const consensusServer2 = consensusHostServer; + await startConsensusHostServer('header-api-key', 'X-API-KEY'); + const consensusPort3 = consensusHostPort; + + // Verify that the first consensus host works + let client = new HttpBlobSinkClient({ + l1RpcUrls: [`http://localhost:${executionHostPort}`], + l1ConsensusHostUrls: [ + `http://localhost:${consensusPort1}`, + `http://localhost:${consensusPort2}`, + `http://localhost:${consensusPort3}`, + ], + l1ConsensusHostApiKeys: ['', 'test-api-key', 'header-api-key'], + l1ConsensusHostApiKeyHeaders: ['', '', 'X-API-KEY'], + }); + + let retrievedBlobs = await client.getBlobSidecar('0x1234', [testEncodedBlobHash]); + expect(retrievedBlobs).toEqual([testEncodedBlob]); + + // Verify that the second consensus host works when the first host fails + consensusServer1?.close(); + client = new HttpBlobSinkClient({ + l1RpcUrls: [`http://localhost:${executionHostPort}`], + l1ConsensusHostUrls: [ + `http://localhost:${consensusPort1}`, + `http://localhost:${consensusPort2}`, + `http://localhost:${consensusPort3}`, + ], + l1ConsensusHostApiKeys: ['', 'test-api-key', 'header-api-key'], + l1ConsensusHostApiKeyHeaders: ['', '', 'X-API-KEY'], + }); + + retrievedBlobs = await client.getBlobSidecar('0x1234', [testEncodedBlobHash]); + expect(retrievedBlobs).toEqual([testEncodedBlob]); + + // Verify that the third consensus host works when the first and second hosts fail + consensusServer2?.close(); + client = new HttpBlobSinkClient({ + l1RpcUrls: [`http://localhost:${executionHostPort}`], + l1ConsensusHostUrls: [ + `http://localhost:${consensusPort1}`, + `http://localhost:${consensusPort2}`, + `http://localhost:${consensusPort3}`, + ], + l1ConsensusHostApiKeys: ['', 'test-api-key', 'header-api-key'], + l1ConsensusHostApiKeyHeaders: ['', '', 'X-API-KEY'], + }); + + retrievedBlobs = await client.getBlobSidecar('0x1234', [testEncodedBlobHash]); + expect(retrievedBlobs).toEqual([testEncodedBlob]); }); it('even if we ask for non-encoded blobs, we should only get encoded blobs', async () => { @@ -202,7 +362,7 @@ describe('HttpBlobSinkClient', () => { const client = new HttpBlobSinkClient({ l1RpcUrls: [`http://localhost:${executionHostPort}`], - l1ConsensusHostUrl: `http://localhost:${consensusHostPort}`, + l1ConsensusHostUrls: [`http://localhost:${consensusHostPort}`], }); const retrievedBlobs = await client.getBlobSidecar('0x1234', [testEncodedBlobHash, testNonEncodedBlobHash]); @@ -216,7 +376,7 @@ describe('HttpBlobSinkClient', () => { const client = new HttpBlobSinkClient({ l1RpcUrls: [`http://localhost:${executionHostPort}`], - l1ConsensusHostUrl: `http://localhost:${consensusHostPort}`, + l1ConsensusHostUrls: [`http://localhost:${consensusHostPort}`], }); // Add spy on the fetch method diff --git a/yarn-project/blob-sink/src/client/http.ts b/yarn-project/blob-sink/src/client/http.ts index d9a548c73beb..fa1f1c728efe 100644 --- a/yarn-project/blob-sink/src/client/http.ts +++ b/yarn-project/blob-sink/src/client/http.ts @@ -78,7 +78,7 @@ export class HttpBlobSinkClient implements BlobSinkClientInterface { * If requesting from the beacon node, we send the slot number * * 1. First atttempts to get blobs from a configured blob sink - * 2. On failure, attempts to get blobs from a configured consensus host + * 2. On failure, attempts to get blobs from the list of configured consensus hosts * 3. On failure, attempts to get blobs from an archive client (eg blobscan) * 4. Else, fails * @@ -89,7 +89,7 @@ export class HttpBlobSinkClient implements BlobSinkClientInterface { public async getBlobSidecar(blockHash: `0x${string}`, blobHashes: Buffer[], indices?: number[]): Promise { let blobs: Blob[] = []; - const { blobSinkUrl, l1ConsensusHostUrl } = this.config; + const { blobSinkUrl, l1ConsensusHostUrls } = this.config; const ctx = { blockHash, blobHashes: blobHashes.map(bufferToHex), indices }; if (blobSinkUrl) { @@ -101,18 +101,30 @@ export class HttpBlobSinkClient implements BlobSinkClientInterface { } } - if (blobs.length == 0 && l1ConsensusHostUrl) { + if (blobs.length == 0 && l1ConsensusHostUrls && l1ConsensusHostUrls.length > 0) { // The beacon api can query by slot number, so we get that first - const consensusCtx = { l1ConsensusHostUrl, ...ctx }; + const consensusCtx = { l1ConsensusHostUrls, ...ctx }; this.log.trace(`Attempting to get slot number for block hash`, consensusCtx); const slotNumber = await this.getSlotNumber(blockHash); this.log.debug(`Got slot number ${slotNumber} from consensus host for querying blobs`, consensusCtx); + if (slotNumber) { - this.log.trace(`Attempting to get blobs from consensus host`, { slotNumber, ...consensusCtx }); - const blobs = await this.getBlobSidecarFrom(l1ConsensusHostUrl, slotNumber, blobHashes, indices); - this.log.debug(`Got ${blobs.length} blobs from consensus host`, { slotNumber, ...consensusCtx }); - if (blobs.length > 0) { - return blobs; + let l1ConsensusHostUrl: string; + for (let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++) { + l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex]; + this.log.trace(`Attempting to get blobs from consensus host`, { slotNumber, l1ConsensusHostUrl, ...ctx }); + const blobs = await this.getBlobSidecarFrom( + l1ConsensusHostUrl, + slotNumber, + blobHashes, + indices, + undefined, + l1ConsensusHostIndex, + ); + this.log.debug(`Got ${blobs.length} blobs from consensus host`, { slotNumber, l1ConsensusHostUrl, ...ctx }); + if (blobs.length > 0) { + return blobs; + } } } } @@ -143,6 +155,7 @@ export class HttpBlobSinkClient implements BlobSinkClientInterface { blobHashes: Buffer[], indices?: number[], maxRetries = 10, + l1ConsensusHostIndex?: number, ): Promise { try { let baseUrl = `${hostUrl}/eth/v1/beacon/blob_sidecars/${blockHashOrSlot}`; @@ -150,7 +163,7 @@ export class HttpBlobSinkClient implements BlobSinkClientInterface { baseUrl += `?indices=${indices.join(',')}`; } - const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config); + const { url, ...options } = getBeaconNodeFetchOptions(baseUrl, this.config, l1ConsensusHostIndex); this.log.debug(`Fetching blob sidecar for ${blockHashOrSlot}`, { url, ...options }); const res = await this.fetch(url, options); @@ -194,12 +207,13 @@ export class HttpBlobSinkClient implements BlobSinkClientInterface { * @returns The slot number */ private async getSlotNumber(blockHash: `0x${string}`): Promise { - if (!this.config.l1ConsensusHostUrl) { + const { l1ConsensusHostUrls, l1RpcUrls } = this.config; + if (!l1ConsensusHostUrls || l1ConsensusHostUrls.length === 0) { this.log.debug('No consensus host url configured'); return undefined; } - if (!this.config.l1RpcUrls) { + if (!l1RpcUrls || l1RpcUrls.length === 0) { this.log.debug('No execution host url configured'); return undefined; } @@ -207,7 +221,7 @@ export class HttpBlobSinkClient implements BlobSinkClientInterface { // Ping execution node to get the parentBeaconBlockRoot for this block let parentBeaconBlockRoot: string | undefined; const client = createPublicClient({ - transport: fallback(this.config.l1RpcUrls.map(url => http(url))), + transport: fallback(l1RpcUrls.map(url => http(url))), }); try { const res: RpcBlock = await client.request({ @@ -228,21 +242,26 @@ export class HttpBlobSinkClient implements BlobSinkClientInterface { } // Query beacon chain to get the slot number for that block root - try { - const { url, ...options } = getBeaconNodeFetchOptions( - `${this.config.l1ConsensusHostUrl}/eth/v1/beacon/headers/${parentBeaconBlockRoot}`, - this.config, - ); - const res = await this.fetch(url, options); - - if (res.ok) { - const body = await res.json(); - - // Add one to get the slot number of the original block hash - return Number(body.data.header.message.slot) + 1; + let l1ConsensusHostUrl: string; + for (let l1ConsensusHostIndex = 0; l1ConsensusHostIndex < l1ConsensusHostUrls.length; l1ConsensusHostIndex++) { + l1ConsensusHostUrl = l1ConsensusHostUrls[l1ConsensusHostIndex]; + try { + const { url, ...options } = getBeaconNodeFetchOptions( + `${l1ConsensusHostUrl}/eth/v1/beacon/headers/${parentBeaconBlockRoot}`, + this.config, + l1ConsensusHostIndex, + ); + const res = await this.fetch(url, options); + + if (res.ok) { + const body = await res.json(); + + // Add one to get the slot number of the original block hash + return Number(body.data.header.message.slot) + 1; + } + } catch (err) { + this.log.error(`Error getting slot number`, err); } - } catch (err) { - this.log.error(`Error getting slot number`, err); } return undefined; @@ -283,21 +302,26 @@ async function getRelevantBlobs(data: any, blobHashes: Buffer[], logger: Logger) return filteredBlobs; } -function getBeaconNodeFetchOptions(url: string, config: BlobSinkConfig) { +function getBeaconNodeFetchOptions(url: string, config: BlobSinkConfig, l1ConsensusHostIndex?: number) { + const { l1ConsensusHostApiKeys, l1ConsensusHostApiKeyHeaders } = config; + const l1ConsensusHostApiKey = + l1ConsensusHostIndex !== undefined && l1ConsensusHostApiKeys && l1ConsensusHostApiKeys[l1ConsensusHostIndex]; + const l1ConsensusHostApiKeyHeader = + l1ConsensusHostIndex !== undefined && + l1ConsensusHostApiKeyHeaders && + l1ConsensusHostApiKeyHeaders[l1ConsensusHostIndex]; + let formattedUrl = url; - if (config.l1ConsensusHostApiKey && !config.l1ConsensusHostApiKeyHeader) { - formattedUrl += `${formattedUrl.includes('?') ? '&' : '?'}key=${config.l1ConsensusHostApiKey}`; - } - // check if l1ConsensusHostUrl has a trailing '/' and remove it - if (config.l1ConsensusHostUrl && config.l1ConsensusHostUrl.endsWith('/')) { - config.l1ConsensusHostUrl = config.l1ConsensusHostUrl.slice(0, -1); + if (l1ConsensusHostApiKey && !l1ConsensusHostApiKeyHeader) { + formattedUrl += `${formattedUrl.includes('?') ? '&' : '?'}key=${l1ConsensusHostApiKey}`; } + return { url: formattedUrl, - ...(config.l1ConsensusHostApiKey && - config.l1ConsensusHostApiKeyHeader && { + ...(l1ConsensusHostApiKey && + l1ConsensusHostApiKeyHeader && { headers: { - [config.l1ConsensusHostApiKeyHeader]: config.l1ConsensusHostApiKey, + [l1ConsensusHostApiKeyHeader]: l1ConsensusHostApiKey, }, }), }; diff --git a/yarn-project/end-to-end/scripts/native-network/boot-node.sh b/yarn-project/end-to-end/scripts/native-network/boot-node.sh index b2b397335ba3..b791d40f6854 100755 --- a/yarn-project/end-to-end/scripts/native-network/boot-node.sh +++ b/yarn-project/end-to-end/scripts/native-network/boot-node.sh @@ -16,7 +16,7 @@ export PORT=${PORT:-"8080"} export DEBUG=${DEBUG:-""} export LOG_LEVEL=${LOG_LEVEL:-"verbose"} export ETHEREUM_HOSTS=${ETHEREUM_HOSTS:-"http://127.0.0.1:8545"} -export L1_CONSENSUS_HOST_URL=${L1_CONSENSUS_HOST_URL:-} +export L1_CONSENSUS_HOST_URLS=${L1_CONSENSUS_HOST_URLS:-} export P2P_ENABLED="true" export VALIDATOR_DISABLED="true" export BLOB_SINK_URL="http://127.0.0.1:${BLOB_SINK_PORT:-5053}" diff --git a/yarn-project/end-to-end/scripts/native-network/prover-node.sh b/yarn-project/end-to-end/scripts/native-network/prover-node.sh index 8f3af0a97631..33d899dbdfc4 100755 --- a/yarn-project/end-to-end/scripts/native-network/prover-node.sh +++ b/yarn-project/end-to-end/scripts/native-network/prover-node.sh @@ -39,7 +39,7 @@ export BOOTSTRAP_NODES=$(echo "$output" | grep -oP 'Node ENR: \K.*') # Set environment variables export LOG_LEVEL=${LOG_LEVEL:-"verbose"} export DEBUG=${DEBUG:-""} -export L1_CONSENSUS_HOST_URL=${L1_CONSENSUS_HOST_URL:-} +export L1_CONSENSUS_HOST_URLS=${L1_CONSENSUS_HOST_URLS:-} export PROVER_AGENT_COUNT="1" export PROVER_AGENT_ENABLED="true" export PROVER_PUBLISHER_PRIVATE_KEY=${PROVER_PUBLISHER_PRIVATE_KEY:-"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"} diff --git a/yarn-project/end-to-end/scripts/native-network/pxe.sh b/yarn-project/end-to-end/scripts/native-network/pxe.sh index 7cd4faab94c4..581d0f2f705f 100755 --- a/yarn-project/end-to-end/scripts/native-network/pxe.sh +++ b/yarn-project/end-to-end/scripts/native-network/pxe.sh @@ -11,7 +11,7 @@ exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname # Starts the PXE (Private eXecution Environment) service # Set environment variables export ETHEREUM_HOSTS=${ETHEREUM_HOSTS:-"http://127.0.0.1:8545"} -export L1_CONSENSUS_HOST_URL=${L1_CONSENSUS_HOST_URL:-} +export L1_CONSENSUS_HOST_URLS=${L1_CONSENSUS_HOST_URLS:-} export AZTEC_NODE_URL=${AZTEC_NODE_URL:-"http://127.0.0.1:8080"} export VALIDATOR_NODE_URL=${VALIDATOR_NODE_URL:-"http://127.0.0.1:8081"} export LOG_LEVEL=${LOG_LEVEL:-"verbose"} diff --git a/yarn-project/end-to-end/scripts/native-network/validator.sh b/yarn-project/end-to-end/scripts/native-network/validator.sh index d421cc961ea2..4ef038b51312 100755 --- a/yarn-project/end-to-end/scripts/native-network/validator.sh +++ b/yarn-project/end-to-end/scripts/native-network/validator.sh @@ -59,7 +59,7 @@ unset VALIDATOR_PRIVATE_KEY export DEBUG=${DEBUG:-""} export LOG_LEVEL=${LOG_LEVEL:-"verbose"} -export L1_CONSENSUS_HOST_URL=${L1_CONSENSUS_HOST_URL:-} +export L1_CONSENSUS_HOST_URLS=${L1_CONSENSUS_HOST_URLS:-} export P2P_ENABLED="true" export VALIDATOR_DISABLED="false" export SEQ_MIN_TX_PER_BLOCK="1" diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 6040e74639e9..760b0a13f035 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -66,9 +66,9 @@ export type EnvVar = | 'GOVERNANCE_PROPOSER_PAYLOAD_ADDRESS' | 'INBOX_CONTRACT_ADDRESS' | 'L1_CHAIN_ID' - | 'L1_CONSENSUS_HOST_URL' - | 'L1_CONSENSUS_HOST_API_KEY' - | 'L1_CONSENSUS_HOST_API_KEY_HEADER' + | 'L1_CONSENSUS_HOST_URLS' + | 'L1_CONSENSUS_HOST_API_KEYS' + | 'L1_CONSENSUS_HOST_API_KEY_HEADERS' | 'L1_PRIVATE_KEY' | 'LOG_JSON' | 'LOG_MULTILINE' diff --git a/yarn-project/foundation/src/log/pino-logger.ts b/yarn-project/foundation/src/log/pino-logger.ts index eba6241d43d4..65d38d546f92 100644 --- a/yarn-project/foundation/src/log/pino-logger.ts +++ b/yarn-project/foundation/src/log/pino-logger.ts @@ -109,7 +109,7 @@ const redactedPaths = [ 'l1PrivateKey', 'senderPrivateKey', // blob sink - 'l1ConsensusHostApiKey', + 'l1ConsensusHostApiKeys', // sensitive options used in the CLI 'privateKey', 'mnemonic',