Skip to content

Commit 1c3cb63

Browse files
authored
feat: Remote quote provider (#8946)
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
1 parent 0be9f25 commit 1c3cb63

File tree

10 files changed

+142
-9
lines changed

10 files changed

+142
-9
lines changed

yarn-project/foundation/src/config/env_var.ts

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export type EnvVar =
104104
| 'PXE_PROVER_ENABLED'
105105
| 'QUOTE_PROVIDER_BASIS_POINT_FEE'
106106
| 'QUOTE_PROVIDER_BOND_AMOUNT'
107+
| 'QUOTE_PROVIDER_URL'
107108
| 'REGISTRY_CONTRACT_ADDRESS'
108109
| 'ROLLUP_CONTRACT_ADDRESS'
109110
| 'SEQ_ALLOWED_SETUP_FN'

yarn-project/prover-node/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,4 @@
8686
"engines": {
8787
"node": ">=18"
8888
}
89-
}
89+
}

yarn-project/prover-node/src/config.ts

+6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export type ProverNodeConfig = ArchiverConfig &
3636
export type QuoteProviderConfig = {
3737
quoteProviderBasisPointFee: number;
3838
quoteProviderBondAmount: bigint;
39+
quoteProviderUrl?: string;
3940
};
4041

4142
const specificProverNodeConfigMappings: ConfigMappingsType<
@@ -64,6 +65,11 @@ const quoteProviderConfigMappings: ConfigMappingsType<QuoteProviderConfig> = {
6465
description: 'The bond amount to charge for providing quotes',
6566
...bigintConfigHelper(1000n),
6667
},
68+
quoteProviderUrl: {
69+
env: 'QUOTE_PROVIDER_URL',
70+
description:
71+
'The URL of the remote quote provider. Overrides QUOTE_PROVIDER_BASIS_POINT_FEE and QUOTE_PROVIDER_BOND_AMOUNT.',
72+
},
6773
};
6874

6975
export const proverNodeConfigMappings: ConfigMappingsType<ProverNodeConfig> = {

yarn-project/prover-node/src/factory.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ClaimsMonitor } from './monitors/claims-monitor.js';
1818
import { EpochMonitor } from './monitors/epoch-monitor.js';
1919
import { createProverCoordination } from './prover-coordination/factory.js';
2020
import { ProverNode } from './prover-node.js';
21+
import { HttpQuoteProvider } from './quote-provider/http.js';
2122
import { SimpleQuoteProvider } from './quote-provider/simple.js';
2223
import { QuoteSigner } from './quote-signer.js';
2324

@@ -78,7 +79,9 @@ export async function createProverNode(
7879
}
7980

8081
function createQuoteProvider(config: QuoteProviderConfig) {
81-
return new SimpleQuoteProvider(config.quoteProviderBasisPointFee, config.quoteProviderBondAmount);
82+
return config.quoteProviderUrl
83+
? new HttpQuoteProvider(config.quoteProviderUrl)
84+
: new SimpleQuoteProvider(config.quoteProviderBasisPointFee, config.quoteProviderBondAmount);
8285
}
8386

8487
function createQuoteSigner(config: ProverNodeConfig) {

yarn-project/prover-node/src/prover-node.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ describe('prover-node', () => {
150150
it('sends a quote on a finished epoch', async () => {
151151
await proverNode.handleEpochCompleted(10n);
152152

153-
expect(quoteProvider.getQuote).toHaveBeenCalledWith(blocks);
153+
expect(quoteProvider.getQuote).toHaveBeenCalledWith(10, blocks);
154154
expect(quoteSigner.sign).toHaveBeenCalledWith(expect.objectContaining(partialQuote));
155155
expect(coordination.addEpochProofQuote).toHaveBeenCalledTimes(1);
156156

yarn-project/prover-node/src/prover-node.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler {
116116
async handleEpochCompleted(epochNumber: bigint): Promise<void> {
117117
try {
118118
const blocks = await this.l2BlockSource.getBlocksForEpoch(epochNumber);
119-
const partialQuote = await this.quoteProvider.getQuote(blocks);
119+
const partialQuote = await this.quoteProvider.getQuote(Number(epochNumber), blocks);
120120
if (!partialQuote) {
121121
this.log.verbose(`No quote produced for epoch ${epochNumber}`);
122122
return;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { L2Block } from '@aztec/circuit-types';
2+
import { times } from '@aztec/foundation/collection';
3+
import { promiseWithResolvers } from '@aztec/foundation/promise';
4+
5+
import { type Server, createServer } from 'http';
6+
import { type AddressInfo } from 'net';
7+
8+
import { HttpQuoteProvider } from './http.js';
9+
10+
describe('HttpQuoteProvider', () => {
11+
let server: Server;
12+
let port: number;
13+
14+
let status: number = 200;
15+
let response: any = {};
16+
let request: any = {};
17+
18+
let provider: HttpQuoteProvider;
19+
let blocks: L2Block[];
20+
21+
beforeAll(async () => {
22+
server = createServer({ keepAliveTimeout: 60000 }, (req, res) => {
23+
const chunks: Buffer[] = [];
24+
req
25+
.on('data', (chunk: Buffer) => {
26+
chunks.push(chunk);
27+
})
28+
.on('end', () => {
29+
request = JSON.parse(Buffer.concat(chunks).toString());
30+
});
31+
32+
res.writeHead(status, { 'Content-Type': 'application/json' });
33+
res.end(JSON.stringify(response));
34+
});
35+
36+
const { promise, resolve } = promiseWithResolvers();
37+
server.listen(0, '127.0.0.1', () => resolve(null));
38+
await promise;
39+
port = (server.address() as AddressInfo).port;
40+
});
41+
42+
beforeEach(() => {
43+
provider = new HttpQuoteProvider(`http://127.0.0.1:${port}`);
44+
blocks = times(3, i => L2Block.random(i + 1, 4));
45+
response = { basisPointFee: 100, bondAmount: '100000000000000000000', validUntilSlot: '100' };
46+
});
47+
48+
afterAll(() => {
49+
server?.close();
50+
});
51+
52+
it('requests a quote sending epoch data', async () => {
53+
const quote = await provider.getQuote(1, blocks);
54+
55+
expect(request).toEqual(
56+
expect.objectContaining({ epochNumber: 1, fromBlock: 1, toBlock: 3, txCount: 12, totalFees: expect.any(String) }),
57+
);
58+
59+
expect(quote).toEqual({
60+
basisPointFee: response.basisPointFee,
61+
bondAmount: BigInt(response.bondAmount),
62+
validUntilSlot: BigInt(response.validUntilSlot),
63+
});
64+
});
65+
66+
it('throws an error if the response is missing required fields', async () => {
67+
response = { basisPointFee: 100 };
68+
await expect(provider.getQuote(1, blocks)).rejects.toThrow(/Missing required fields/i);
69+
});
70+
71+
it('throws an error if the response is not ok', async () => {
72+
status = 400;
73+
await expect(provider.getQuote(1, blocks)).rejects.toThrow(/Failed to fetch quote/i);
74+
});
75+
});
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,47 @@
1-
// TODO: Implement me! This should send a request to a configurable URL to get the quote.
2-
export class HttpQuoteProvider {}
1+
import { type L2Block } from '@aztec/circuit-types';
2+
3+
import { type QuoteProvider, type QuoteProviderResult } from './index.js';
4+
import { getTotalFees, getTxCount } from './utils.js';
5+
6+
export class HttpQuoteProvider implements QuoteProvider {
7+
constructor(private readonly url: string) {}
8+
9+
public async getQuote(epochNumber: number, epoch: L2Block[]): Promise<QuoteProviderResult | undefined> {
10+
const payload: HttpQuoteRequestPayload = {
11+
epochNumber,
12+
fromBlock: epoch[0].number,
13+
toBlock: epoch.at(-1)!.number,
14+
totalFees: getTotalFees(epoch).toString(),
15+
txCount: getTxCount(epoch),
16+
};
17+
18+
const response = await fetch(this.url, {
19+
method: 'POST',
20+
body: JSON.stringify(payload),
21+
headers: { 'content-type': 'application/json' },
22+
});
23+
24+
if (!response.ok) {
25+
throw new Error(`Failed to fetch quote: ${response.statusText}`);
26+
}
27+
28+
const data = await response.json();
29+
if (!data.basisPointFee || !data.bondAmount) {
30+
throw new Error(`Missing required fields in response: ${JSON.stringify(data)}`);
31+
}
32+
33+
const basisPointFee = Number(data.basisPointFee);
34+
const bondAmount = BigInt(data.bondAmount);
35+
const validUntilSlot = data.validUntilSlot ? BigInt(data.validUntilSlot) : undefined;
36+
37+
return { basisPointFee, bondAmount, validUntilSlot };
38+
}
39+
}
40+
41+
export type HttpQuoteRequestPayload = {
42+
epochNumber: number;
43+
fromBlock: number;
44+
toBlock: number;
45+
totalFees: string;
46+
txCount: number;
47+
};
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { type EpochProofQuotePayload, type L2Block } from '@aztec/circuit-types';
22

3-
type QuoteProviderResult = Pick<EpochProofQuotePayload, 'basisPointFee' | 'bondAmount'> &
3+
export type QuoteProviderResult = Pick<EpochProofQuotePayload, 'basisPointFee' | 'bondAmount'> &
44
Partial<Pick<EpochProofQuotePayload, 'validUntilSlot'>>;
55

66
export interface QuoteProvider {
7-
getQuote(epoch: L2Block[]): Promise<QuoteProviderResult | undefined>;
7+
getQuote(epochNumber: number, epoch: L2Block[]): Promise<QuoteProviderResult | undefined>;
88
}

yarn-project/prover-node/src/quote-provider/simple.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { type QuoteProvider } from './index.js';
55
export class SimpleQuoteProvider implements QuoteProvider {
66
constructor(public readonly basisPointFee: number, public readonly bondAmount: bigint) {}
77

8-
getQuote(_epoch: L2Block[]): Promise<Pick<EpochProofQuotePayload, 'basisPointFee' | 'bondAmount'>> {
8+
getQuote(
9+
_epochNumber: number,
10+
_epoch: L2Block[],
11+
): Promise<Pick<EpochProofQuotePayload, 'basisPointFee' | 'bondAmount'>> {
912
const { basisPointFee, bondAmount } = this;
1013
return Promise.resolve({ basisPointFee, bondAmount });
1114
}

0 commit comments

Comments
 (0)