Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,22 @@ The empire slashing model has been removed. Only the tally-based slashing model

## Unreleased (v5)

### [Aztec Node] `getTxByHash`, `getTxsByHash` and `getPendingTxs` no longer return tx proofs by default

`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.

**Migration:**

```diff
- const tx = await node.getTxByHash(txHash);
+ const tx = await node.getTxByHash(txHash, { includeProof: true });

- const txs = await node.getPendingTxs(limit, after);
+ const txs = await node.getPendingTxs(limit, after, { includeProof: true });
```

**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.

### [aztec.js] `DeployMethod.send()` always returns `{ contract, receipt, instance }`

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.
Expand Down
22 changes: 21 additions & 1 deletion yarn-project/aztec-node/src/aztec-node/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1639,7 +1639,9 @@ describe('aztec node', () => {
p2p.getTxStatus.mockResolvedValue('pending');
l2BlockSource.getTxEffect.mockResolvedValue(undefined);
const pendingTx = await mockTx();
p2p.getTxByHashFromPool.mockResolvedValue(pendingTx);
p2p.getTxByHashFromPool.mockImplementation((_h, opts) =>
Promise.resolve(opts?.includeProof === false ? pendingTx.withoutProof() : pendingTx),
);

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

it('attaches the pending tx with its proof when includePendingTx and includeProof are set', async () => {
const txHash = TxHash.random();
p2p.getTxStatus.mockResolvedValue('pending');
l2BlockSource.getTxEffect.mockResolvedValue(undefined);
const pendingTx = await mockTx();
p2p.getTxByHashFromPool.mockImplementation((_h, opts) =>
Promise.resolve(opts?.includeProof === false ? pendingTx.withoutProof() : pendingTx),
);

const receipt = await node.getTxReceipt(txHash, { includePendingTx: true, includeProof: true });
expect(receipt).toBeInstanceOf(PendingTxReceipt);
if (!receipt.isPending()) {
throw new Error('expected a pending receipt');
}
expect(receipt.tx).toBeDefined();
expect(receipt.tx!.chonkProof).toEqual(pendingTx.chonkProof);
});

it('returns a dropped receipt when the tx is unknown to the pool and not mined', async () => {
const txHash = TxHash.random();
p2p.getTxStatus.mockResolvedValue(undefined);
Expand Down
27 changes: 16 additions & 11 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import type {
CheckpointIncludeOptions,
CheckpointParameter,
CheckpointResponse,
GetTxByHashOptions,
} from '@aztec/stdlib/interfaces/client';
import { AztecNodeAdminConfigSchema } from '@aztec/stdlib/interfaces/client';
import {
Expand Down Expand Up @@ -1114,7 +1115,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
}

public async getMaxPriorityFees(): Promise<GasFees> {
for await (const tx of this.p2pClient.iteratePendingTxs()) {
for await (const tx of this.p2pClient.iteratePendingTxs({ includeProof: false })) {
return tx.getGasSettings().maxPriorityFeesPerGas;
}

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

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

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

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

public async findLeavesIndexes(
Expand Down
3 changes: 2 additions & 1 deletion yarn-project/p2p/src/client/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ export async function createP2PClient(
}

const bindings = logger.getBindings();
const store = deps.store ?? (await createStore(P2P_STORE_NAME, 2, config, bindings));
// Schema version 3: tx proofs are stored in a separate map from the tx data (see TxPoolV2Impl).
const store = deps.store ?? (await createStore(P2P_STORE_NAME, 3, config, bindings));
const archive = await createStore(P2P_ARCHIVE_STORE_NAME, 1, config, bindings);
const peerStore = await createStore(P2P_PEER_STORE_NAME, 1, config, bindings);
const attestationStore = await createStore(P2P_ATTESTATION_STORE_NAME, 2, config, bindings);
Expand Down
20 changes: 14 additions & 6 deletions yarn-project/p2p/src/client/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,18 @@ export type P2P = P2PClient & {
/**
* Returns a transaction in the transaction pool by its hash.
* @param txHash - Hash of tx to return.
* @param opts - Set `includeProof: false` to skip loading the tx proof from the DB.
* @returns A single tx or undefined.
*/
getTxByHashFromPool(txHash: TxHash): Promise<Tx | undefined>;
getTxByHashFromPool(txHash: TxHash, opts?: { includeProof?: boolean }): Promise<Tx | undefined>;

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

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

/** Returns an iterator over pending txs on the mempool. */
iteratePendingTxs(): AsyncIterableIterator<Tx>;
/**
* Returns an iterator over pending txs on the mempool.
* Set `includeProof: false` to skip loading tx proofs from the DB.
*/
iteratePendingTxs(opts?: { includeProof?: boolean }): AsyncIterableIterator<Tx>;

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

/** Returns the number of pending txs in the mempool. */
getPendingTxCount(): Promise<number>;
Expand Down
26 changes: 15 additions & 11 deletions yarn-project/p2p/src/client/p2p_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from '@aztec/stdlib/block';
import type { ContractDataSource } from '@aztec/stdlib/contract';
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
import { type PeerInfo, tryStop } from '@aztec/stdlib/interfaces/server';
import { type GetTxByHashOptions, type PeerInfo, tryStop } from '@aztec/stdlib/interfaces/server';
import { type BlockProposal, CheckpointAttestation, type CheckpointProposal, type TopicType } from '@aztec/stdlib/p2p';
import type { BlockHeader, Tx, TxHash } from '@aztec/stdlib/tx';
import { Attributes, type TelemetryClient, WithTracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
Expand Down Expand Up @@ -432,7 +432,7 @@ export class P2PClient extends WithTracer implements P2P {
this.p2pService.registerCheckpointAttestationCallback(callback);
}

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

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

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

public async *iteratePendingTxs(): AsyncIterableIterator<Tx> {
public async *iteratePendingTxs(opts?: { includeProof?: boolean }): AsyncIterableIterator<Tx> {
for (const txHash of await this.txPool.getPendingTxHashes()) {
const tx = await this.txPool.getTxByHash(txHash);
const tx = await this.txPool.getTxByHash(txHash, opts);
if (tx) {
yield tx;
}
}
}

public async *iterateEligiblePendingTxs(): AsyncIterableIterator<Tx> {
public async *iterateEligiblePendingTxs(opts?: { includeProof?: boolean }): AsyncIterableIterator<Tx> {
for (const txHash of await this.txPool.getEligiblePendingTxHashes()) {
const tx = await this.txPool.getTxByHash(txHash);
const tx = await this.txPool.getTxByHash(txHash, opts);
if (tx) {
yield tx;
}
Expand All @@ -480,19 +482,21 @@ export class P2PClient extends WithTracer implements P2P {
/**
* Returns a transaction in the transaction pool by its hash.
* @param txHash - Hash of the transaction to look for in the pool.
* @param opts - Set `includeProof: false` to skip loading the tx proof from the DB.
* @returns A single tx or undefined.
*/
getTxByHashFromPool(txHash: TxHash): Promise<Tx | undefined> {
return this.txPool.getTxByHash(txHash);
getTxByHashFromPool(txHash: TxHash, opts?: { includeProof?: boolean }): Promise<Tx | undefined> {
return this.txPool.getTxByHash(txHash, opts);
}

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

hasTxsInPool(txHashes: TxHash[]): Promise<boolean[]> {
Expand Down
14 changes: 7 additions & 7 deletions yarn-project/p2p/src/mem_pools/tx_pool_v2/deleted_pool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('DeletedPool', () => {
beforeEach(async () => {
store = await openTmpStore('deleted-pool-test');
txsDB = store.openMap('txs');
pool = new DeletedPool(store, txsDB, createLogger('test'));
pool = new DeletedPool(store, txHash => txsDB.delete(txHash), createLogger('test'));
await pool.hydrateFromDatabase();
});

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

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

expect(pool2.isFromPrunedBlock('tx1')).toBe(false);
Expand Down Expand Up @@ -306,7 +306,7 @@ describe('DeletedPool', () => {
]);

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

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

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

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

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

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

// Simulate restart
const pool2 = new DeletedPool(store, txsDB, createLogger('test2'));
const pool2 = new DeletedPool(store, txHash => txsDB.delete(txHash), createLogger('test2'));
await pool2.hydrateFromDatabase();

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

// Simulate restart
const pool2 = new DeletedPool(store, txsDB, createLogger('test2'));
const pool2 = new DeletedPool(store, txHash => txsDB.delete(txHash), createLogger('test2'));
await pool2.hydrateFromDatabase();

// tx1 (prune-soft-deleted) should still be in DB
Expand Down
14 changes: 7 additions & 7 deletions yarn-project/p2p/src/mem_pools/tx_pool_v2/deleted_pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ export class DeletedPool {
/** Persisted map: txHash -> DeletedTxState (serialized) - for prune-based soft deletions */
#deletedTxsDB: AztecAsyncMap<string, Buffer>;

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

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

#log: Logger;

constructor(store: AztecAsyncKVStore, txsDB: AztecAsyncMap<string, Buffer>, log: Logger) {
constructor(store: AztecAsyncKVStore, deleteTxData: (txHash: string) => Promise<void>, log: Logger) {
this.#deletedTxsDB = store.openMap('deleted_txs');
this.#slotDeletedDB = store.openSet('slot_deleted_txs');
this.#txsDB = txsDB;
this.#deleteTxData = deleteTxData;
this.#log = log;
}

Expand Down Expand Up @@ -100,7 +100,7 @@ export class DeletedPool {
// Slot-deleted txs are stale after restart - hard-delete them all
let slotDeletedCount = 0;
for await (const txHash of this.#slotDeletedDB.entriesAsync()) {
await this.#txsDB.delete(txHash);
await this.#deleteTxData(txHash);
await this.#slotDeletedDB.delete(txHash);
slotDeletedCount++;
}
Expand Down Expand Up @@ -234,7 +234,7 @@ export class DeletedPool {
for (const txHash of toHardDelete) {
this.#state.delete(txHash);
await this.#deletedTxsDB.delete(txHash);
await this.#txsDB.delete(txHash);
await this.#deleteTxData(txHash);
}

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

this.#log.debug(
Expand Down
8 changes: 4 additions & 4 deletions yarn-project/p2p/src/mem_pools/tx_pool_v2/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,11 @@ export interface TxPoolV2 extends TypedEventEmitter<TxPoolV2Events> {
*/
handleFinalizedBlock(block: BlockHeader): Promise<void>;

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

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

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