Skip to content

Commit 426a675

Browse files
authored
feat: merge-train/spartan-v5 (#23885)
BEGIN_COMMIT_OVERRIDE feat!: proofless tx lookup in getTxByHash and getTxsByHash (#23827) END_COMMIT_OVERRIDE
2 parents e3f1921 + 68071ad commit 426a675

22 files changed

Lines changed: 316 additions & 87 deletions

File tree

docs/docs-developers/docs/resources/migration_notes.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,22 @@ The empire slashing model has been removed. Only the tally-based slashing model
959959

960960
## Unreleased (v5)
961961

962+
### [Aztec Node] `getTxByHash`, `getTxsByHash` and `getPendingTxs` no longer return tx proofs by default
963+
964+
`AztecNode.getTxByHash`, `AztecNode.getTxsByHash` and `AztecNode.getPendingTxs` (also exposed on the P2P API) now take an optional `GetTxByHashOptions` argument with an `includeProof` flag. The proof is stripped from returned txs unless `includeProof: true` is passed, cutting roughly 35-52KB per tx over the wire.
965+
966+
**Migration:**
967+
968+
```diff
969+
- const tx = await node.getTxByHash(txHash);
970+
+ const tx = await node.getTxByHash(txHash, { includeProof: true });
971+
972+
- const txs = await node.getPendingTxs(limit, after);
973+
+ const txs = await node.getPendingTxs(limit, after, { includeProof: true });
974+
```
975+
976+
**Impact**: Callers that read the proof off returned txs (eg to re-broadcast or validate them) must now pass `{ includeProof: true }` explicitly; by default the returned txs carry an empty proof.
977+
962978
### [aztec.js] `DeployMethod.send()` always returns `{ contract, receipt, instance }`
963979

964980
The `returnReceipt` option in deploy wait options has been removed. `DeployMethod.send()` now always returns an object with `contract`, `receipt`, and `instance` at the top level, provided the user waits for the transaction to be included.

yarn-project/aztec-node/src/aztec-node/server.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1639,7 +1639,9 @@ describe('aztec node', () => {
16391639
p2p.getTxStatus.mockResolvedValue('pending');
16401640
l2BlockSource.getTxEffect.mockResolvedValue(undefined);
16411641
const pendingTx = await mockTx();
1642-
p2p.getTxByHashFromPool.mockResolvedValue(pendingTx);
1642+
p2p.getTxByHashFromPool.mockImplementation((_h, opts) =>
1643+
Promise.resolve(opts?.includeProof === false ? pendingTx.withoutProof() : pendingTx),
1644+
);
16431645

16441646
const receipt = await node.getTxReceipt(txHash, { includePendingTx: true });
16451647
expect(receipt).toBeInstanceOf(PendingTxReceipt);
@@ -1650,6 +1652,24 @@ describe('aztec node', () => {
16501652
expect(receipt.tx!.chonkProof).toEqual(pendingTx.withoutProof().chonkProof);
16511653
});
16521654

1655+
it('attaches the pending tx with its proof when includePendingTx and includeProof are set', async () => {
1656+
const txHash = TxHash.random();
1657+
p2p.getTxStatus.mockResolvedValue('pending');
1658+
l2BlockSource.getTxEffect.mockResolvedValue(undefined);
1659+
const pendingTx = await mockTx();
1660+
p2p.getTxByHashFromPool.mockImplementation((_h, opts) =>
1661+
Promise.resolve(opts?.includeProof === false ? pendingTx.withoutProof() : pendingTx),
1662+
);
1663+
1664+
const receipt = await node.getTxReceipt(txHash, { includePendingTx: true, includeProof: true });
1665+
expect(receipt).toBeInstanceOf(PendingTxReceipt);
1666+
if (!receipt.isPending()) {
1667+
throw new Error('expected a pending receipt');
1668+
}
1669+
expect(receipt.tx).toBeDefined();
1670+
expect(receipt.tx!.chonkProof).toEqual(pendingTx.chonkProof);
1671+
});
1672+
16531673
it('returns a dropped receipt when the tx is unknown to the pool and not mined', async () => {
16541674
const txHash = TxHash.random();
16551675
p2p.getTxStatus.mockResolvedValue(undefined);

yarn-project/aztec-node/src/aztec-node/server.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ import type {
9898
CheckpointIncludeOptions,
9999
CheckpointParameter,
100100
CheckpointResponse,
101+
GetTxByHashOptions,
101102
} from '@aztec/stdlib/interfaces/client';
102103
import { AztecNodeAdminConfigSchema } from '@aztec/stdlib/interfaces/client';
103104
import {
@@ -1114,7 +1115,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
11141115
}
11151116

11161117
public async getMaxPriorityFees(): Promise<GasFees> {
1117-
for await (const tx of this.p2pClient.iteratePendingTxs()) {
1118+
for await (const tx of this.p2pClient.iteratePendingTxs({ includeProof: false })) {
11181119
return tx.getGasSettings().maxPriorityFeesPerGas;
11191120
}
11201121

@@ -1216,8 +1217,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
12161217
if (options?.includePendingTx) {
12171218
// The tx may have left the pool since we checked its status (mined or dropped); in that case we
12181219
// leave `tx` unset and still return a pending receipt.
1219-
const pendingTx = await this.p2pClient.getTxByHashFromPool(txHash);
1220-
tx = pendingTx && !options.includeProof ? pendingTx.withoutProof() : pendingTx;
1220+
tx = await this.p2pClient.getTxByHashFromPool(txHash, { includeProof: !!options.includeProof });
12211221
}
12221222
receipt = new PendingTxReceipt(txHash, tx);
12231223
} else {
@@ -1305,30 +1305,35 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
13051305
* @param after - The last known pending tx. Used for pagination
13061306
* @returns - The pending txs.
13071307
*/
1308-
public getPendingTxs(limit?: number, after?: TxHash): Promise<Tx[]> {
1309-
return this.p2pClient!.getPendingTxs(limit, after);
1308+
public getPendingTxs(limit?: number, after?: TxHash, options?: GetTxByHashOptions): Promise<Tx[]> {
1309+
return this.p2pClient!.getPendingTxs(limit, after, options);
13101310
}
13111311

13121312
public getPendingTxCount(): Promise<number> {
13131313
return this.p2pClient!.getPendingTxCount();
13141314
}
13151315

13161316
/**
1317-
* Method to retrieve a single tx from the mempool or unfinalized chain.
1317+
* Method to retrieve a single tx from the mempool or unfinalized chain. The tx's proof is only loaded and returned
1318+
* when `includeProof` is set.
13181319
* @param txHash - The transaction hash to return.
1320+
* @param options - Options for the returned tx (eg whether to include its proof).
13191321
* @returns - The tx if it exists.
13201322
*/
1321-
public getTxByHash(txHash: TxHash): Promise<Tx | undefined> {
1322-
return Promise.resolve(this.p2pClient!.getTxByHashFromPool(txHash));
1323+
public getTxByHash(txHash: TxHash, options?: GetTxByHashOptions): Promise<Tx | undefined> {
1324+
return this.p2pClient!.getTxByHashFromPool(txHash, { includeProof: !!options?.includeProof });
13231325
}
13241326

13251327
/**
1326-
* Method to retrieve txs from the mempool or unfinalized chain.
1328+
* Method to retrieve txs from the mempool or unfinalized chain. The txs' proofs are only loaded and returned when
1329+
* `includeProof` is set.
13271330
* @param txHash - The transaction hash to return.
1331+
* @param options - Options for the returned txs (eg whether to include their proofs).
13281332
* @returns - The txs if it exists.
13291333
*/
1330-
public async getTxsByHash(txHashes: TxHash[]): Promise<Tx[]> {
1331-
return compactArray(await Promise.all(txHashes.map(txHash => this.getTxByHash(txHash))));
1334+
public async getTxsByHash(txHashes: TxHash[], options?: GetTxByHashOptions): Promise<Tx[]> {
1335+
const txs = await this.p2pClient!.getTxsByHashFromPool(txHashes, { includeProof: !!options?.includeProof });
1336+
return compactArray(txs);
13321337
}
13331338

13341339
public async findLeavesIndexes(

yarn-project/p2p/src/client/factory.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ export async function createP2PClient(
7575
}
7676

7777
const bindings = logger.getBindings();
78-
const store = deps.store ?? (await createStore(P2P_STORE_NAME, 2, config, bindings));
78+
// Schema version 3: tx proofs are stored in a separate map from the tx data (see TxPoolV2Impl).
79+
const store = deps.store ?? (await createStore(P2P_STORE_NAME, 3, config, bindings));
7980
const archive = await createStore(P2P_ARCHIVE_STORE_NAME, 1, config, bindings);
8081
const peerStore = await createStore(P2P_PEER_STORE_NAME, 1, config, bindings);
8182
const attestationStore = await createStore(P2P_ATTESTATION_STORE_NAME, 2, config, bindings);

yarn-project/p2p/src/client/interface.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,18 @@ export type P2P = P2PClient & {
124124
/**
125125
* Returns a transaction in the transaction pool by its hash.
126126
* @param txHash - Hash of tx to return.
127+
* @param opts - Set `includeProof: false` to skip loading the tx proof from the DB.
127128
* @returns A single tx or undefined.
128129
*/
129-
getTxByHashFromPool(txHash: TxHash): Promise<Tx | undefined>;
130+
getTxByHashFromPool(txHash: TxHash, opts?: { includeProof?: boolean }): Promise<Tx | undefined>;
130131

131132
/**
132133
* Returns transactions in the transaction pool by hash.
133134
* @param txHashes - Hashes of txs to return.
135+
* @param opts - Set `includeProof: false` to skip loading tx proofs from the DB.
134136
* @returns An array of txs or undefined.
135137
*/
136-
getTxsByHashFromPool(txHashes: TxHash[]): Promise<(Tx | undefined)[]>;
138+
getTxsByHashFromPool(txHashes: TxHash[], opts?: { includeProof?: boolean }): Promise<(Tx | undefined)[]>;
137139

138140
/**
139141
* Checks if transactions exist in the pool
@@ -156,11 +158,17 @@ export type P2P = P2PClient & {
156158
*/
157159
getTxStatus(txHash: TxHash): Promise<'pending' | 'mined' | 'deleted' | undefined>;
158160

159-
/** Returns an iterator over pending txs on the mempool. */
160-
iteratePendingTxs(): AsyncIterableIterator<Tx>;
161+
/**
162+
* Returns an iterator over pending txs on the mempool.
163+
* Set `includeProof: false` to skip loading tx proofs from the DB.
164+
*/
165+
iteratePendingTxs(opts?: { includeProof?: boolean }): AsyncIterableIterator<Tx>;
161166

162-
/** Returns an iterator over pending txs that have been in the pool long enough to be eligible for block building. */
163-
iterateEligiblePendingTxs(): AsyncIterableIterator<Tx>;
167+
/**
168+
* Returns an iterator over pending txs that have been in the pool long enough to be eligible for block building.
169+
* Set `includeProof: false` to skip loading tx proofs from the DB.
170+
*/
171+
iterateEligiblePendingTxs(opts?: { includeProof?: boolean }): AsyncIterableIterator<Tx>;
164172

165173
/** Returns the number of pending txs in the mempool. */
166174
getPendingTxCount(): Promise<number>;

yarn-project/p2p/src/client/p2p_client.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
} from '@aztec/stdlib/block';
2626
import type { ContractDataSource } from '@aztec/stdlib/contract';
2727
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
28-
import { type PeerInfo, tryStop } from '@aztec/stdlib/interfaces/server';
28+
import { type GetTxByHashOptions, type PeerInfo, tryStop } from '@aztec/stdlib/interfaces/server';
2929
import { type BlockProposal, CheckpointAttestation, type CheckpointProposal, type TopicType } from '@aztec/stdlib/p2p';
3030
import type { BlockHeader, Tx, TxHash } from '@aztec/stdlib/tx';
3131
import { Attributes, type TelemetryClient, WithTracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
@@ -432,7 +432,7 @@ export class P2PClient extends WithTracer implements P2P {
432432
this.p2pService.registerCheckpointAttestationCallback(callback);
433433
}
434434

435-
public async getPendingTxs(limit?: number, after?: TxHash): Promise<Tx[]> {
435+
public async getPendingTxs(limit?: number, after?: TxHash, options?: GetTxByHashOptions): Promise<Tx[]> {
436436
if (limit !== undefined && limit <= 0) {
437437
throw new TypeError('limit must be greater than 0');
438438
}
@@ -451,26 +451,28 @@ export class P2PClient extends WithTracer implements P2P {
451451
const endIndex = limit !== undefined ? startIndex + limit : undefined;
452452
txHashes = txHashes.slice(startIndex, endIndex);
453453

454-
const maybeTxs = await Promise.all(txHashes.map(txHash => this.txPool.getTxByHash(txHash)));
454+
// This is a public-facing API (exposed via both the node and the p2p RPC), so proofs are opt-in.
455+
const includeProof = !!options?.includeProof;
456+
const maybeTxs = await Promise.all(txHashes.map(txHash => this.txPool.getTxByHash(txHash, { includeProof })));
455457
return maybeTxs.filter((tx): tx is Tx => !!tx);
456458
}
457459

458460
public getPendingTxCount(): Promise<number> {
459461
return this.txPool.getPendingTxCount();
460462
}
461463

462-
public async *iteratePendingTxs(): AsyncIterableIterator<Tx> {
464+
public async *iteratePendingTxs(opts?: { includeProof?: boolean }): AsyncIterableIterator<Tx> {
463465
for (const txHash of await this.txPool.getPendingTxHashes()) {
464-
const tx = await this.txPool.getTxByHash(txHash);
466+
const tx = await this.txPool.getTxByHash(txHash, opts);
465467
if (tx) {
466468
yield tx;
467469
}
468470
}
469471
}
470472

471-
public async *iterateEligiblePendingTxs(): AsyncIterableIterator<Tx> {
473+
public async *iterateEligiblePendingTxs(opts?: { includeProof?: boolean }): AsyncIterableIterator<Tx> {
472474
for (const txHash of await this.txPool.getEligiblePendingTxHashes()) {
473-
const tx = await this.txPool.getTxByHash(txHash);
475+
const tx = await this.txPool.getTxByHash(txHash, opts);
474476
if (tx) {
475477
yield tx;
476478
}
@@ -480,19 +482,21 @@ export class P2PClient extends WithTracer implements P2P {
480482
/**
481483
* Returns a transaction in the transaction pool by its hash.
482484
* @param txHash - Hash of the transaction to look for in the pool.
485+
* @param opts - Set `includeProof: false` to skip loading the tx proof from the DB.
483486
* @returns A single tx or undefined.
484487
*/
485-
getTxByHashFromPool(txHash: TxHash): Promise<Tx | undefined> {
486-
return this.txPool.getTxByHash(txHash);
488+
getTxByHashFromPool(txHash: TxHash, opts?: { includeProof?: boolean }): Promise<Tx | undefined> {
489+
return this.txPool.getTxByHash(txHash, opts);
487490
}
488491

489492
/**
490493
* Returns transactions in the transaction pool by hash.
491494
* @param txHashes - Hashes of the transactions to look for.
495+
* @param opts - Set `includeProof: false` to skip loading tx proofs from the DB.
492496
* @returns The txs found, in the same order as the requested hashes. If a tx is not found, it will be undefined.
493497
*/
494-
getTxsByHashFromPool(txHashes: TxHash[]): Promise<(Tx | undefined)[]> {
495-
return this.txPool.getTxsByHash(txHashes);
498+
getTxsByHashFromPool(txHashes: TxHash[], opts?: { includeProof?: boolean }): Promise<(Tx | undefined)[]> {
499+
return this.txPool.getTxsByHash(txHashes, opts);
496500
}
497501

498502
hasTxsInPool(txHashes: TxHash[]): Promise<boolean[]> {

yarn-project/p2p/src/mem_pools/tx_pool_v2/deleted_pool.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('DeletedPool', () => {
1313
beforeEach(async () => {
1414
store = await openTmpStore('deleted-pool-test');
1515
txsDB = store.openMap('txs');
16-
pool = new DeletedPool(store, txsDB, createLogger('test'));
16+
pool = new DeletedPool(store, txHash => txsDB.delete(txHash), createLogger('test'));
1717
await pool.hydrateFromDatabase();
1818
});
1919

@@ -111,7 +111,7 @@ describe('DeletedPool', () => {
111111
// Clear tx1 (re-mined higher), keep tx2
112112
await pool.clearIfMinedHigher('tx1', BlockNumber(12));
113113

114-
const pool2 = new DeletedPool(store, txsDB, createLogger('test2'));
114+
const pool2 = new DeletedPool(store, txHash => txsDB.delete(txHash), createLogger('test2'));
115115
await pool2.hydrateFromDatabase();
116116

117117
expect(pool2.isFromPrunedBlock('tx1')).toBe(false);
@@ -306,7 +306,7 @@ describe('DeletedPool', () => {
306306
]);
307307

308308
// Create a new pool instance with the same store
309-
const pool2 = new DeletedPool(store, txsDB, createLogger('test2'));
309+
const pool2 = new DeletedPool(store, txHash => txsDB.delete(txHash), createLogger('test2'));
310310
await pool2.hydrateFromDatabase();
311311

312312
expect(pool2.isFromPrunedBlock('tx1')).toBe(true);
@@ -328,7 +328,7 @@ describe('DeletedPool', () => {
328328
await pool.finalizeBlock(BlockNumber(5));
329329

330330
// Create a new pool instance with the same store
331-
const pool2 = new DeletedPool(store, txsDB, createLogger('test2'));
331+
const pool2 = new DeletedPool(store, txHash => txsDB.delete(txHash), createLogger('test2'));
332332
await pool2.hydrateFromDatabase();
333333

334334
expect(pool2.isFromPrunedBlock('tx1')).toBe(false);
@@ -351,7 +351,7 @@ describe('DeletedPool', () => {
351351
expect(pool.isSoftDeleted('tx2')).toBe(false);
352352

353353
// Create a new pool instance with the same store (simulates restart)
354-
const pool2 = new DeletedPool(store, txsDB, createLogger('test2'));
354+
const pool2 = new DeletedPool(store, txHash => txsDB.delete(txHash), createLogger('test2'));
355355
await pool2.hydrateFromDatabase();
356356

357357
// Soft-deleted state should be preserved
@@ -487,7 +487,7 @@ describe('DeletedPool', () => {
487487
expect(await txsDB.getAsync('tx2')).toBeDefined();
488488

489489
// Simulate restart
490-
const pool2 = new DeletedPool(store, txsDB, createLogger('test2'));
490+
const pool2 = new DeletedPool(store, txHash => txsDB.delete(txHash), createLogger('test2'));
491491
await pool2.hydrateFromDatabase();
492492

493493
// Both should be hard-deleted after hydration
@@ -507,7 +507,7 @@ describe('DeletedPool', () => {
507507
await pool.deleteTx('tx2');
508508

509509
// Simulate restart
510-
const pool2 = new DeletedPool(store, txsDB, createLogger('test2'));
510+
const pool2 = new DeletedPool(store, txHash => txsDB.delete(txHash), createLogger('test2'));
511511
await pool2.hydrateFromDatabase();
512512

513513
// tx1 (prune-soft-deleted) should still be in DB

yarn-project/p2p/src/mem_pools/tx_pool_v2/deleted_pool.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ export class DeletedPool {
5252
/** Persisted map: txHash -> DeletedTxState (serialized) - for prune-based soft deletions */
5353
#deletedTxsDB: AztecAsyncMap<string, Buffer>;
5454

55-
/** Reference to the main txs database for hard deletion */
56-
#txsDB: AztecAsyncMap<string, Buffer>;
55+
/** Hard-deletes a tx's stored data (tx blob and proof) from the pool's stores. */
56+
#deleteTxData: (txHash: string) => Promise<void>;
5757

5858
/** In-memory state for transactions from pruned blocks */
5959
#state: Map<string, DeletedTxState> = new Map();
@@ -69,10 +69,10 @@ export class DeletedPool {
6969

7070
#log: Logger;
7171

72-
constructor(store: AztecAsyncKVStore, txsDB: AztecAsyncMap<string, Buffer>, log: Logger) {
72+
constructor(store: AztecAsyncKVStore, deleteTxData: (txHash: string) => Promise<void>, log: Logger) {
7373
this.#deletedTxsDB = store.openMap('deleted_txs');
7474
this.#slotDeletedDB = store.openSet('slot_deleted_txs');
75-
this.#txsDB = txsDB;
75+
this.#deleteTxData = deleteTxData;
7676
this.#log = log;
7777
}
7878

@@ -100,7 +100,7 @@ export class DeletedPool {
100100
// Slot-deleted txs are stale after restart - hard-delete them all
101101
let slotDeletedCount = 0;
102102
for await (const txHash of this.#slotDeletedDB.entriesAsync()) {
103-
await this.#txsDB.delete(txHash);
103+
await this.#deleteTxData(txHash);
104104
await this.#slotDeletedDB.delete(txHash);
105105
slotDeletedCount++;
106106
}
@@ -234,7 +234,7 @@ export class DeletedPool {
234234
for (const txHash of toHardDelete) {
235235
this.#state.delete(txHash);
236236
await this.#deletedTxsDB.delete(txHash);
237-
await this.#txsDB.delete(txHash);
237+
await this.#deleteTxData(txHash);
238238
}
239239

240240
this.#log.debug(`Finalized ${toHardDelete.length} txs from pruned blocks at block ${finalizedBlockNumber}`, {
@@ -266,7 +266,7 @@ export class DeletedPool {
266266
for (const txHash of toHardDelete) {
267267
this.#slotDeletedTxs.delete(txHash);
268268
await this.#slotDeletedDB.delete(txHash);
269-
await this.#txsDB.delete(txHash);
269+
await this.#deleteTxData(txHash);
270270
}
271271

272272
this.#log.debug(

yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,11 @@ export interface TxPoolV2 extends TypedEventEmitter<TxPoolV2Events> {
192192
*/
193193
handleFinalizedBlock(block: BlockHeader): Promise<void>;
194194

195-
/** Gets a transaction by its hash */
196-
getTxByHash(txHash: TxHash): Promise<Tx | undefined>;
195+
/** Gets a transaction by its hash. Set `includeProof: false` to skip loading the proof from the DB. */
196+
getTxByHash(txHash: TxHash, opts?: { includeProof?: boolean }): Promise<Tx | undefined>;
197197

198-
/** Gets multiple transactions by their hashes */
199-
getTxsByHash(txHashes: TxHash[]): Promise<(Tx | undefined)[]>;
198+
/** Gets multiple transactions by their hashes. Set `includeProof: false` to skip loading proofs from the DB. */
199+
getTxsByHash(txHashes: TxHash[], opts?: { includeProof?: boolean }): Promise<(Tx | undefined)[]>;
200200

201201
/** Checks if transactions exist in the pool */
202202
hasTxs(txHashes: TxHash[]): Promise<boolean[]>;

0 commit comments

Comments
 (0)