Skip to content

Commit 262640c

Browse files
committed
refactor: more robust tagging index sync as recipient
Fixes #17775 In this PR I implement a new log sync algorithm we've come up with in Buenos Aires that should be fully robust against any kind of private log losses. The algorithm is explained in the body of `loadPrivateLogsForSenderRecipientPair` function. That function should be the entrypoint when reviewing this PR (if checking the utility functions first you would not have enough context). Unfortunately this PR introduces a performance regression that is tracked by [this issue](https://linear.app/aztec-labs/issue/F-229/improve-log-sync-performance). I am fairly certain that the regression is caused by an unreasonable number of requests performed by the `loadLogsForRange` function - that will be tackled in a followup PR. In this PR the algorithm is not yet integrated into the system. That is done in a [PR up the stack](#19125). The directory structure is not yet final - I wanted to keep this PR contained in one place to not have conflicts with Martin's PR. So please ignore that for now (will move stuff around in a final pass). My plan is as follows: 1. Merge this PR, 2. fix the regression by modifying the `getLogsByTag` API such that it gives me all the info I need (now it doesn't give me block timestamp), 3. once that PR is merged most likely wait for Martin's refactor to be merged and then rebase and polish my integrating new log sync PR, 4. move some files around to make it all cuter. ## Update on regression of time in e2e tests Unfortunately realized that this has caused a regression and log sync is now significantly slower. Did 2 runs of the `e2e_l1_with_wall_time` test and on `next` the results are 337 and 334 seconds but with the new code it's 661 and 658 seconds. Will work on dropping that down before merging that "integrating new log sync" PR.
1 parent d3be5a7 commit 262640c

11 files changed

+1019
-11
lines changed

yarn-project/pxe/src/storage/tagging_data_provider/sender_tagging_data_provider.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { openTmpStore } from '@aztec/kv-store/lmdb-v2';
33
import { DirectionalAppTaggingSecret, type PreTag } from '@aztec/stdlib/logs';
44
import { TxHash } from '@aztec/stdlib/tx';
55

6-
import { WINDOW_LEN } from '../../tagging/sync/sync_sender_tagging_indexes.js';
6+
import { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from '../../tagging/sync/sync_sender_tagging_indexes.js';
77
import { SenderTaggingDataProvider } from './sender_tagging_data_provider.js';
88

99
describe('SenderTaggingDataProvider', () => {
@@ -147,7 +147,7 @@ describe('SenderTaggingDataProvider', () => {
147147
const txHash1 = TxHash.random();
148148
const txHash2 = TxHash.random();
149149
const finalizedIndex = 10;
150-
const indexBeyondWindow = finalizedIndex + WINDOW_LEN + 1;
150+
const indexBeyondWindow = finalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN + 1;
151151

152152
// First store and finalize an index
153153
await taggingDataProvider.storePendingIndexes([{ secret: secret1, index: finalizedIndex }], txHash1);
@@ -165,7 +165,7 @@ describe('SenderTaggingDataProvider', () => {
165165
const txHash1 = TxHash.random();
166166
const txHash2 = TxHash.random();
167167
const finalizedIndex = 10;
168-
const indexAtBoundary = finalizedIndex + WINDOW_LEN;
168+
const indexAtBoundary = finalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN;
169169

170170
// First store and finalize an index
171171
await taggingDataProvider.storePendingIndexes([{ secret: secret1, index: finalizedIndex }], txHash1);

yarn-project/pxe/src/storage/tagging_data_provider/sender_tagging_data_provider.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
33
import type { DirectionalAppTaggingSecret, PreTag } from '@aztec/stdlib/logs';
44
import { TxHash } from '@aztec/stdlib/tx';
55

6-
import { WINDOW_LEN as SENDER_TAGGING_INDEXES_SYNC_WINDOW_LEN } from '../../tagging/sync/sync_sender_tagging_indexes.js';
6+
import { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from '../../tagging/sync/sync_sender_tagging_indexes.js';
77

88
/**
99
* Data provider of tagging data used when syncing the sender tagging indexes. The recipient counterpart of this class
@@ -68,10 +68,10 @@ export class SenderTaggingDataProvider {
6868
// First we check that for any secret the highest used index in tx is not further than window length from
6969
// the highest finalized index.
7070
const finalizedIndex = (await this.getLastFinalizedIndex(secret)) ?? 0;
71-
if (index > finalizedIndex + SENDER_TAGGING_INDEXES_SYNC_WINDOW_LEN) {
71+
if (index > finalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN) {
7272
throw new Error(
7373
`Highest used index ${index} is further than window length from the highest finalized index ${finalizedIndex}.
74-
Tagging window length ${SENDER_TAGGING_INDEXES_SYNC_WINDOW_LEN} is configured too low. Contact the Aztec team
74+
Tagging window length ${UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN} is configured too low. Contact the Aztec team
7575
to increase it!`,
7676
);
7777
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import { MAX_INCLUDE_BY_TIMESTAMP_DURATION } from '@aztec/constants';
2+
import { BlockNumber } from '@aztec/foundation/branded-types';
3+
import { Fr } from '@aztec/foundation/curves/bn254';
4+
import { openTmpStore } from '@aztec/kv-store/lmdb-v2';
5+
import { AztecAddress } from '@aztec/stdlib/aztec-address';
6+
import { L2BlockHash } from '@aztec/stdlib/block';
7+
import type { AztecNode } from '@aztec/stdlib/interfaces/server';
8+
import { DirectionalAppTaggingSecret, PrivateLog, TxScopedL2Log } from '@aztec/stdlib/logs';
9+
import { makeBlockHeader } from '@aztec/stdlib/testing';
10+
import { TxHash } from '@aztec/stdlib/tx';
11+
12+
import { type MockProxy, mock } from 'jest-mock-extended';
13+
14+
import { SiloedTag } from '../siloed_tag.js';
15+
import { UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN } from '../sync/sync_sender_tagging_indexes.js';
16+
import { Tag } from '../tag.js';
17+
import { loadPrivateLogsForSenderRecipientPair } from './load_private_logs_for_sender_recipient_pair.js';
18+
import { NewRecipientTaggingDataProvider } from './new_recipient_tagging_data_provider.js';
19+
20+
// In this test suite we don't care about the anchor block behavior as that is sufficiently tested by
21+
// the loadLogsForRange test suite, so we use a high block number to ensure it occurs after all logs.
22+
const NON_INTERFERING_ANCHOR_BLOCK_NUMBER = BlockNumber(100);
23+
24+
describe('loadPrivateLogsForSenderRecipientPair', () => {
25+
let secret: DirectionalAppTaggingSecret;
26+
let app: AztecAddress;
27+
28+
let aztecNode: MockProxy<AztecNode>;
29+
let taggingDataProvider: NewRecipientTaggingDataProvider;
30+
31+
const currentTimestamp = BigInt(Math.floor(Date.now() / 1000));
32+
33+
async function computeSiloedTagForIndex(index: number) {
34+
const tag = await Tag.compute({ secret, index });
35+
return SiloedTag.compute(tag, app);
36+
}
37+
38+
function makeLog(blockHash: Fr, blockNumber: number, tag: Fr) {
39+
return new TxScopedL2Log(
40+
TxHash.random(),
41+
0,
42+
0,
43+
BlockNumber(blockNumber),
44+
L2BlockHash.fromField(blockHash),
45+
PrivateLog.random(tag),
46+
);
47+
}
48+
49+
beforeAll(async () => {
50+
secret = DirectionalAppTaggingSecret.fromString(Fr.random().toString());
51+
app = await AztecAddress.random();
52+
aztecNode = mock<AztecNode>();
53+
});
54+
55+
beforeEach(async () => {
56+
aztecNode.getLogsByTags.mockReset();
57+
aztecNode.getBlockHeaderByHash.mockReset();
58+
aztecNode.getL2Tips.mockReset();
59+
aztecNode.getBlockHeader.mockReset();
60+
taggingDataProvider = new NewRecipientTaggingDataProvider(await openTmpStore('test'));
61+
});
62+
63+
it('returns empty array when no logs found', async () => {
64+
aztecNode.getL2Tips.mockResolvedValue({
65+
finalized: { number: BlockNumber(10) },
66+
} as any);
67+
68+
aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp }));
69+
70+
// no logs found for any tag
71+
aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => {
72+
return Promise.resolve(tags.map((_tag: Fr) => []));
73+
});
74+
75+
const logs = await loadPrivateLogsForSenderRecipientPair(
76+
secret,
77+
app,
78+
aztecNode,
79+
taggingDataProvider,
80+
NON_INTERFERING_ANCHOR_BLOCK_NUMBER,
81+
);
82+
83+
expect(logs).toHaveLength(0);
84+
expect(await taggingDataProvider.getHighestAgedIndex(secret)).toBeUndefined();
85+
expect(await taggingDataProvider.getHighestFinalizedIndex(secret)).toBeUndefined();
86+
});
87+
88+
it('loads log and updates highest finalized index but not highest aged index', async () => {
89+
const finalizedBlockNumber = 10;
90+
91+
const logBlockTimestamp = currentTimestamp - 5000n; // not aged
92+
const logIndex = 5;
93+
const logTag = await computeSiloedTagForIndex(logIndex);
94+
const logBlockHeader = makeBlockHeader(0, { timestamp: logBlockTimestamp });
95+
96+
aztecNode.getL2Tips.mockResolvedValue({
97+
finalized: { number: BlockNumber(finalizedBlockNumber) },
98+
} as any);
99+
100+
aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp }));
101+
102+
// The log is finalized
103+
aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => {
104+
return Promise.all(
105+
tags.map(async (t: Fr) =>
106+
t.equals(logTag.value) ? [makeLog(await logBlockHeader.hash(), finalizedBlockNumber, logTag.value)] : [],
107+
),
108+
);
109+
});
110+
111+
aztecNode.getBlockHeaderByHash.mockImplementation(async (hash: Fr) => {
112+
if (hash.equals(await logBlockHeader.hash())) {
113+
return logBlockHeader;
114+
}
115+
return undefined;
116+
});
117+
118+
const logs = await loadPrivateLogsForSenderRecipientPair(
119+
secret,
120+
app,
121+
aztecNode,
122+
taggingDataProvider,
123+
NON_INTERFERING_ANCHOR_BLOCK_NUMBER,
124+
);
125+
126+
expect(logs).toHaveLength(1);
127+
expect(await taggingDataProvider.getHighestFinalizedIndex(secret)).toBe(logIndex);
128+
expect(await taggingDataProvider.getHighestAgedIndex(secret)).toBeUndefined();
129+
});
130+
131+
it('loads log and updates both highest aged and highest finalized indexes', async () => {
132+
const finalizedBlockNumber = 10;
133+
134+
const logBlockTimestamp = currentTimestamp - BigInt(MAX_INCLUDE_BY_TIMESTAMP_DURATION) - 1000n; // aged
135+
const logIndex = 7;
136+
const logTag = await computeSiloedTagForIndex(logIndex);
137+
const logBlockHeader = makeBlockHeader(0, { timestamp: logBlockTimestamp });
138+
139+
aztecNode.getL2Tips.mockResolvedValue({
140+
finalized: { number: BlockNumber(finalizedBlockNumber) },
141+
} as any);
142+
143+
aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp }));
144+
145+
// The log is finalized
146+
aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => {
147+
return Promise.all(
148+
tags.map(async (t: Fr) =>
149+
t.equals(logTag.value) ? [makeLog(await logBlockHeader.hash(), finalizedBlockNumber, logTag.value)] : [],
150+
),
151+
);
152+
});
153+
154+
aztecNode.getBlockHeaderByHash.mockImplementation(async (hash: Fr) => {
155+
if (hash.equals(await logBlockHeader.hash())) {
156+
return logBlockHeader;
157+
}
158+
return undefined;
159+
});
160+
161+
const logs = await loadPrivateLogsForSenderRecipientPair(
162+
secret,
163+
app,
164+
aztecNode,
165+
taggingDataProvider,
166+
NON_INTERFERING_ANCHOR_BLOCK_NUMBER,
167+
);
168+
169+
expect(logs).toHaveLength(1);
170+
expect(await taggingDataProvider.getHighestAgedIndex(secret)).toBe(logIndex);
171+
expect(await taggingDataProvider.getHighestFinalizedIndex(secret)).toBe(logIndex);
172+
});
173+
174+
it('logs at boundaries are properly loaded, window and highest indexes advance as expected', async () => {
175+
const finalizedBlockNumber = 10;
176+
177+
const log1BlockTimestamp = currentTimestamp - BigInt(MAX_INCLUDE_BY_TIMESTAMP_DURATION) - 1000n; // Aged
178+
const log2BlockTimestamp = currentTimestamp - 5000n; // Not aged
179+
const highestAgedIndex = 3;
180+
const highestFinalizedIndex = 5;
181+
const log1Index = highestAgedIndex + 1; // At the beginning of the range
182+
const log2Index = highestFinalizedIndex + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN; // At the window boundary
183+
const log1Tag = await computeSiloedTagForIndex(log1Index);
184+
const log2Tag = await computeSiloedTagForIndex(log2Index);
185+
const log1BlockHeader = makeBlockHeader(0, { timestamp: log1BlockTimestamp });
186+
const log2BlockHeader = makeBlockHeader(1, { timestamp: log2BlockTimestamp });
187+
188+
// Set existing highest aged index and highest finalized index
189+
await taggingDataProvider.updateHighestAgedIndex(secret, highestAgedIndex);
190+
await taggingDataProvider.updateHighestFinalizedIndex(secret, highestFinalizedIndex);
191+
192+
aztecNode.getL2Tips.mockResolvedValue({
193+
finalized: { number: BlockNumber(finalizedBlockNumber) },
194+
} as any);
195+
196+
aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp }));
197+
198+
// We record the number of queried tags to be able to verify that the window was moved forward correctly.
199+
let numQueriedTags = 0;
200+
201+
aztecNode.getLogsByTags.mockImplementation((tags: Fr[]) => {
202+
numQueriedTags += tags.length;
203+
return Promise.all(
204+
tags.map(async (t: Fr) => {
205+
if (t.equals(log1Tag.value)) {
206+
return [makeLog(await log1BlockHeader.hash(), finalizedBlockNumber, log1Tag.value)];
207+
} else if (t.equals(log2Tag.value)) {
208+
return [makeLog(await log2BlockHeader.hash(), finalizedBlockNumber, log2Tag.value)];
209+
}
210+
return [];
211+
}),
212+
);
213+
});
214+
215+
aztecNode.getBlockHeaderByHash.mockImplementation(async (hash: Fr) => {
216+
if (hash.equals(await log1BlockHeader.hash())) {
217+
return log1BlockHeader;
218+
} else if (hash.equals(await log2BlockHeader.hash())) {
219+
return log2BlockHeader;
220+
}
221+
return undefined;
222+
});
223+
224+
const logs = await loadPrivateLogsForSenderRecipientPair(
225+
secret,
226+
app,
227+
aztecNode,
228+
taggingDataProvider,
229+
NON_INTERFERING_ANCHOR_BLOCK_NUMBER,
230+
);
231+
232+
// Verify that both logs at the boundaries of the range were found and processed
233+
expect(logs).toHaveLength(2);
234+
expect(await taggingDataProvider.getHighestFinalizedIndex(secret)).toBe(log2Index);
235+
expect(await taggingDataProvider.getHighestAgedIndex(secret)).toBe(log1Index);
236+
237+
// Verify that the window was moved forward correctly
238+
// Total range queried: from (highestAgedIndex + 1) to (log2Index + WINDOW_LEN + 1) exclusive
239+
const expectedNumQueriedTags = log2Index + UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN - highestAgedIndex;
240+
expect(numQueriedTags).toBe(expectedNumQueriedTags);
241+
});
242+
});

0 commit comments

Comments
 (0)