diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 146a4724b00..d20de524be5 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -104,6 +104,7 @@ export type EnvVar = | 'PXE_PROVER_ENABLED' | 'QUOTE_PROVIDER_BASIS_POINT_FEE' | 'QUOTE_PROVIDER_BOND_AMOUNT' + | 'QUOTE_PROVIDER_URL' | 'REGISTRY_CONTRACT_ADDRESS' | 'ROLLUP_CONTRACT_ADDRESS' | 'SEQ_ALLOWED_SETUP_FN' diff --git a/yarn-project/prover-node/package.json b/yarn-project/prover-node/package.json index 21c4d635e3e..5444e608585 100644 --- a/yarn-project/prover-node/package.json +++ b/yarn-project/prover-node/package.json @@ -86,4 +86,4 @@ "engines": { "node": ">=18" } -} +} \ No newline at end of file diff --git a/yarn-project/prover-node/src/config.ts b/yarn-project/prover-node/src/config.ts index bfca132153d..2a4d5a99ec5 100644 --- a/yarn-project/prover-node/src/config.ts +++ b/yarn-project/prover-node/src/config.ts @@ -36,6 +36,7 @@ export type ProverNodeConfig = ArchiverConfig & export type QuoteProviderConfig = { quoteProviderBasisPointFee: number; quoteProviderBondAmount: bigint; + quoteProviderUrl?: string; }; const specificProverNodeConfigMappings: ConfigMappingsType< @@ -64,6 +65,11 @@ const quoteProviderConfigMappings: ConfigMappingsType = { description: 'The bond amount to charge for providing quotes', ...bigintConfigHelper(1000n), }, + quoteProviderUrl: { + env: 'QUOTE_PROVIDER_URL', + description: + 'The URL of the remote quote provider. Overrides QUOTE_PROVIDER_BASIS_POINT_FEE and QUOTE_PROVIDER_BOND_AMOUNT.', + }, }; export const proverNodeConfigMappings: ConfigMappingsType = { diff --git a/yarn-project/prover-node/src/factory.ts b/yarn-project/prover-node/src/factory.ts index 97364a15f1c..52b4431a958 100644 --- a/yarn-project/prover-node/src/factory.ts +++ b/yarn-project/prover-node/src/factory.ts @@ -18,6 +18,7 @@ import { ClaimsMonitor } from './monitors/claims-monitor.js'; import { EpochMonitor } from './monitors/epoch-monitor.js'; import { createProverCoordination } from './prover-coordination/factory.js'; import { ProverNode } from './prover-node.js'; +import { HttpQuoteProvider } from './quote-provider/http.js'; import { SimpleQuoteProvider } from './quote-provider/simple.js'; import { QuoteSigner } from './quote-signer.js'; @@ -78,7 +79,9 @@ export async function createProverNode( } function createQuoteProvider(config: QuoteProviderConfig) { - return new SimpleQuoteProvider(config.quoteProviderBasisPointFee, config.quoteProviderBondAmount); + return config.quoteProviderUrl + ? new HttpQuoteProvider(config.quoteProviderUrl) + : new SimpleQuoteProvider(config.quoteProviderBasisPointFee, config.quoteProviderBondAmount); } function createQuoteSigner(config: ProverNodeConfig) { diff --git a/yarn-project/prover-node/src/prover-node.test.ts b/yarn-project/prover-node/src/prover-node.test.ts index 13d115d72cc..5e764ed73b4 100644 --- a/yarn-project/prover-node/src/prover-node.test.ts +++ b/yarn-project/prover-node/src/prover-node.test.ts @@ -150,7 +150,7 @@ describe('prover-node', () => { it('sends a quote on a finished epoch', async () => { await proverNode.handleEpochCompleted(10n); - expect(quoteProvider.getQuote).toHaveBeenCalledWith(blocks); + expect(quoteProvider.getQuote).toHaveBeenCalledWith(10, blocks); expect(quoteSigner.sign).toHaveBeenCalledWith(expect.objectContaining(partialQuote)); expect(coordination.addEpochProofQuote).toHaveBeenCalledTimes(1); diff --git a/yarn-project/prover-node/src/prover-node.ts b/yarn-project/prover-node/src/prover-node.ts index 31fb8a3829b..19a67341c11 100644 --- a/yarn-project/prover-node/src/prover-node.ts +++ b/yarn-project/prover-node/src/prover-node.ts @@ -116,7 +116,7 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler { async handleEpochCompleted(epochNumber: bigint): Promise { try { const blocks = await this.l2BlockSource.getBlocksForEpoch(epochNumber); - const partialQuote = await this.quoteProvider.getQuote(blocks); + const partialQuote = await this.quoteProvider.getQuote(Number(epochNumber), blocks); if (!partialQuote) { this.log.verbose(`No quote produced for epoch ${epochNumber}`); return; diff --git a/yarn-project/prover-node/src/quote-provider/http.test.ts b/yarn-project/prover-node/src/quote-provider/http.test.ts new file mode 100644 index 00000000000..2f498f5312e --- /dev/null +++ b/yarn-project/prover-node/src/quote-provider/http.test.ts @@ -0,0 +1,75 @@ +import { L2Block } from '@aztec/circuit-types'; +import { times } from '@aztec/foundation/collection'; +import { promiseWithResolvers } from '@aztec/foundation/promise'; + +import { type Server, createServer } from 'http'; +import { type AddressInfo } from 'net'; + +import { HttpQuoteProvider } from './http.js'; + +describe('HttpQuoteProvider', () => { + let server: Server; + let port: number; + + let status: number = 200; + let response: any = {}; + let request: any = {}; + + let provider: HttpQuoteProvider; + let blocks: L2Block[]; + + beforeAll(async () => { + server = createServer({ keepAliveTimeout: 60000 }, (req, res) => { + const chunks: Buffer[] = []; + req + .on('data', (chunk: Buffer) => { + chunks.push(chunk); + }) + .on('end', () => { + request = JSON.parse(Buffer.concat(chunks).toString()); + }); + + res.writeHead(status, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(response)); + }); + + const { promise, resolve } = promiseWithResolvers(); + server.listen(0, '127.0.0.1', () => resolve(null)); + await promise; + port = (server.address() as AddressInfo).port; + }); + + beforeEach(() => { + provider = new HttpQuoteProvider(`http://127.0.0.1:${port}`); + blocks = times(3, i => L2Block.random(i + 1, 4)); + response = { basisPointFee: 100, bondAmount: '100000000000000000000', validUntilSlot: '100' }; + }); + + afterAll(() => { + server?.close(); + }); + + it('requests a quote sending epoch data', async () => { + const quote = await provider.getQuote(1, blocks); + + expect(request).toEqual( + expect.objectContaining({ epochNumber: 1, fromBlock: 1, toBlock: 3, txCount: 12, totalFees: expect.any(String) }), + ); + + expect(quote).toEqual({ + basisPointFee: response.basisPointFee, + bondAmount: BigInt(response.bondAmount), + validUntilSlot: BigInt(response.validUntilSlot), + }); + }); + + it('throws an error if the response is missing required fields', async () => { + response = { basisPointFee: 100 }; + await expect(provider.getQuote(1, blocks)).rejects.toThrow(/Missing required fields/i); + }); + + it('throws an error if the response is not ok', async () => { + status = 400; + await expect(provider.getQuote(1, blocks)).rejects.toThrow(/Failed to fetch quote/i); + }); +}); diff --git a/yarn-project/prover-node/src/quote-provider/http.ts b/yarn-project/prover-node/src/quote-provider/http.ts index b5ada13cbb9..210f397d374 100644 --- a/yarn-project/prover-node/src/quote-provider/http.ts +++ b/yarn-project/prover-node/src/quote-provider/http.ts @@ -1,2 +1,47 @@ -// TODO: Implement me! This should send a request to a configurable URL to get the quote. -export class HttpQuoteProvider {} +import { type L2Block } from '@aztec/circuit-types'; + +import { type QuoteProvider, type QuoteProviderResult } from './index.js'; +import { getTotalFees, getTxCount } from './utils.js'; + +export class HttpQuoteProvider implements QuoteProvider { + constructor(private readonly url: string) {} + + public async getQuote(epochNumber: number, epoch: L2Block[]): Promise { + const payload: HttpQuoteRequestPayload = { + epochNumber, + fromBlock: epoch[0].number, + toBlock: epoch.at(-1)!.number, + totalFees: getTotalFees(epoch).toString(), + txCount: getTxCount(epoch), + }; + + const response = await fetch(this.url, { + method: 'POST', + body: JSON.stringify(payload), + headers: { 'content-type': 'application/json' }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch quote: ${response.statusText}`); + } + + const data = await response.json(); + if (!data.basisPointFee || !data.bondAmount) { + throw new Error(`Missing required fields in response: ${JSON.stringify(data)}`); + } + + const basisPointFee = Number(data.basisPointFee); + const bondAmount = BigInt(data.bondAmount); + const validUntilSlot = data.validUntilSlot ? BigInt(data.validUntilSlot) : undefined; + + return { basisPointFee, bondAmount, validUntilSlot }; + } +} + +export type HttpQuoteRequestPayload = { + epochNumber: number; + fromBlock: number; + toBlock: number; + totalFees: string; + txCount: number; +}; diff --git a/yarn-project/prover-node/src/quote-provider/index.ts b/yarn-project/prover-node/src/quote-provider/index.ts index 770833cb280..ec768900ebd 100644 --- a/yarn-project/prover-node/src/quote-provider/index.ts +++ b/yarn-project/prover-node/src/quote-provider/index.ts @@ -1,8 +1,8 @@ import { type EpochProofQuotePayload, type L2Block } from '@aztec/circuit-types'; -type QuoteProviderResult = Pick & +export type QuoteProviderResult = Pick & Partial>; export interface QuoteProvider { - getQuote(epoch: L2Block[]): Promise; + getQuote(epochNumber: number, epoch: L2Block[]): Promise; } diff --git a/yarn-project/prover-node/src/quote-provider/simple.ts b/yarn-project/prover-node/src/quote-provider/simple.ts index c2eff3fc5c0..bcd4cbe05c1 100644 --- a/yarn-project/prover-node/src/quote-provider/simple.ts +++ b/yarn-project/prover-node/src/quote-provider/simple.ts @@ -5,7 +5,10 @@ import { type QuoteProvider } from './index.js'; export class SimpleQuoteProvider implements QuoteProvider { constructor(public readonly basisPointFee: number, public readonly bondAmount: bigint) {} - getQuote(_epoch: L2Block[]): Promise> { + getQuote( + _epochNumber: number, + _epoch: L2Block[], + ): Promise> { const { basisPointFee, bondAmount } = this; return Promise.resolve({ basisPointFee, bondAmount }); }