Skip to content

Commit

Permalink
feat: Remote quote provider (#8946)
Browse files Browse the repository at this point in the history
Adds an http quote provider that sends a fetch request to the target URL
to get a quote, instead of using a hardcoded value from env vars.

Builds on #8864
  • Loading branch information
spalladino authored Oct 3, 2024
1 parent 0be9f25 commit 1c3cb63
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 9 deletions.
1 change: 1 addition & 0 deletions yarn-project/foundation/src/config/env_var.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/prover-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@
"engines": {
"node": ">=18"
}
}
}
6 changes: 6 additions & 0 deletions yarn-project/prover-node/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type ProverNodeConfig = ArchiverConfig &
export type QuoteProviderConfig = {
quoteProviderBasisPointFee: number;
quoteProviderBondAmount: bigint;
quoteProviderUrl?: string;
};

const specificProverNodeConfigMappings: ConfigMappingsType<
Expand Down Expand Up @@ -64,6 +65,11 @@ const quoteProviderConfigMappings: ConfigMappingsType<QuoteProviderConfig> = {
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<ProverNodeConfig> = {
Expand Down
5 changes: 4 additions & 1 deletion yarn-project/prover-node/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/prover-node/src/prover-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion yarn-project/prover-node/src/prover-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler {
async handleEpochCompleted(epochNumber: bigint): Promise<void> {
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;
Expand Down
75 changes: 75 additions & 0 deletions yarn-project/prover-node/src/quote-provider/http.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
49 changes: 47 additions & 2 deletions yarn-project/prover-node/src/quote-provider/http.ts
Original file line number Diff line number Diff line change
@@ -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<QuoteProviderResult | undefined> {
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;
};
4 changes: 2 additions & 2 deletions yarn-project/prover-node/src/quote-provider/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { type EpochProofQuotePayload, type L2Block } from '@aztec/circuit-types';

type QuoteProviderResult = Pick<EpochProofQuotePayload, 'basisPointFee' | 'bondAmount'> &
export type QuoteProviderResult = Pick<EpochProofQuotePayload, 'basisPointFee' | 'bondAmount'> &
Partial<Pick<EpochProofQuotePayload, 'validUntilSlot'>>;

export interface QuoteProvider {
getQuote(epoch: L2Block[]): Promise<QuoteProviderResult | undefined>;
getQuote(epochNumber: number, epoch: L2Block[]): Promise<QuoteProviderResult | undefined>;
}
5 changes: 4 additions & 1 deletion yarn-project/prover-node/src/quote-provider/simple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Pick<EpochProofQuotePayload, 'basisPointFee' | 'bondAmount'>> {
getQuote(
_epochNumber: number,
_epoch: L2Block[],
): Promise<Pick<EpochProofQuotePayload, 'basisPointFee' | 'bondAmount'>> {
const { basisPointFee, bondAmount } = this;
return Promise.resolve({ basisPointFee, bondAmount });
}
Expand Down

0 comments on commit 1c3cb63

Please sign in to comment.