Skip to content

Commit b0e3c52

Browse files
nchamoaztec-bot
authored andcommitted
refactor(pxe): deduplicate tx hash lookups in MessageContextService (#23075)
1 parent cb6863c commit b0e3c52

2 files changed

Lines changed: 48 additions & 25 deletions

File tree

yarn-project/pxe/src/messages/message_context_service.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Fr } from '@aztec/foundation/curves/bn254';
33
import { BlockHash } from '@aztec/stdlib/block';
44
import type { AztecNode } from '@aztec/stdlib/interfaces/server';
55
import { MessageContext } from '@aztec/stdlib/logs';
6-
import { TxHash } from '@aztec/stdlib/tx';
6+
import { TxEffect, TxHash } from '@aztec/stdlib/tx';
77

88
import { mock } from 'jest-mock-extended';
99

@@ -123,4 +123,26 @@ describe('MessageContextService', () => {
123123
// Zero hash should not trigger getTxEffect
124124
expect(aztecNode.getTxEffect).toHaveBeenCalledTimes(3);
125125
});
126+
127+
it('deduplicates repeated tx hashes so each is fetched only once', async () => {
128+
const txEffect = TxEffect.from({
129+
...(await TxEffect.random({ numNoteHashes: 1, numNullifiers: 1 })),
130+
});
131+
132+
aztecNode.getTxEffect.mockResolvedValue({
133+
l2BlockNumber: BlockNumber(anchorBlockNumber - 1),
134+
l2BlockHash: BlockHash.random(),
135+
txIndexInBlock: 0,
136+
data: txEffect,
137+
});
138+
139+
const results = await service.getMessageContextsByTxHash(
140+
[txEffect.txHash.hash, txEffect.txHash.hash, txEffect.txHash.hash],
141+
anchorBlockNumber,
142+
);
143+
144+
const expected = new MessageContext(txEffect.txHash, txEffect.noteHashes, txEffect.nullifiers[0]);
145+
expect(results).toEqual([expected, expected, expected]);
146+
expect(aztecNode.getTxEffect).toHaveBeenCalledTimes(1);
147+
});
126148
});
Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { uniqueBy } from '@aztec/foundation/collection';
12
import { Fr } from '@aztec/foundation/curves/bn254';
23
import type { AztecNode } from '@aztec/stdlib/interfaces/server';
34
import { MessageContext } from '@aztec/stdlib/logs';
4-
import { TxHash } from '@aztec/stdlib/tx';
5+
import { type IndexedTxEffect, TxHash } from '@aztec/stdlib/tx';
56

67
/** Resolves transaction hashes into the context needed to process messages. */
78
export class MessageContextService {
@@ -14,31 +15,31 @@ export class MessageContextService {
1415
* process messages that originated from that transaction. Returns `null` for tx hashes that are zero, not yet
1516
* available, or in blocks beyond the anchor block.
1617
*/
17-
getMessageContextsByTxHash(txHashes: Fr[], anchorBlockNumber: number): Promise<(MessageContext | null)[]> {
18-
// TODO: optimize, we might be hitting the node to get the same txHash repeatedly
19-
return Promise.all(
20-
txHashes.map(async txHashField => {
21-
// A zero tx hash indicates a tx-less offchain message (e.g. one not tied to any onchain transaction).
22-
// These messages don't have a transaction context to resolve, so we return null.
23-
if (txHashField.isZero()) {
24-
return null;
25-
}
18+
async getMessageContextsByTxHash(txHashes: Fr[], anchorBlockNumber: number): Promise<(MessageContext | null)[]> {
19+
const nonZeroTxHashes = txHashes.filter(h => !h.isZero()).map(h => TxHash.fromField(h));
20+
const uniqueTxHashes = uniqueBy(nonZeroTxHashes, h => h.toString());
21+
const fetched = await Promise.all(uniqueTxHashes.map(h => this.aztecNode.getTxEffect(h)));
22+
const txEffects = new Map(
23+
uniqueTxHashes
24+
.map((h, i): [string, IndexedTxEffect | undefined] => [h.toString(), fetched[i]])
25+
.filter((entry): entry is [string, IndexedTxEffect] => entry[1] !== undefined),
26+
);
2627

27-
const txHash = TxHash.fromField(txHashField);
28-
const txEffect = await this.aztecNode.getTxEffect(txHash);
29-
if (!txEffect || txEffect.l2BlockNumber > anchorBlockNumber) {
30-
return null;
31-
}
28+
return txHashes.map(txHashField => {
29+
const txHash = TxHash.fromField(txHashField);
30+
const txEffect = txEffects.get(txHash.toString());
31+
if (!txEffect || txEffect.l2BlockNumber > anchorBlockNumber) {
32+
return null;
33+
}
3234

33-
// Every tx has at least one nullifier (the first nullifier derived from the tx hash). Hitting this condition
34-
// would mean a buggy node, but since we need to access data.nullifiers[0], the defensive check does no harm.
35-
const data = txEffect.data;
36-
if (data.nullifiers.length === 0) {
37-
throw new Error(`Tx effect for ${txHash} has no nullifiers`);
38-
}
35+
// Every tx has at least one nullifier (the first nullifier derived from the tx hash). Hitting this condition
36+
// would mean a buggy node, but since we need to access data.nullifiers[0], the defensive check does no harm.
37+
const data = txEffect.data;
38+
if (data.nullifiers.length === 0) {
39+
throw new Error(`Tx effect for ${txHash} has no nullifiers`);
40+
}
3941

40-
return new MessageContext(data.txHash, data.noteHashes, data.nullifiers[0]);
41-
}),
42-
);
42+
return new MessageContext(data.txHash, data.noteHashes, data.nullifiers[0]);
43+
});
4344
}
4445
}

0 commit comments

Comments
 (0)