Skip to content

Commit c92c474

Browse files
authored
fix: resolve merge conflicts from v4 into v4-next (#22276)
## Summary Resolves merge conflicts in `sync/v4-into-v4-next-20260402-121022` branch (merge from v4 into v4-next). Three conflicts resolved: - **`yarn-project/p2p/src/client/factory.ts`**: Import conflict — combined both sides to include `createCheckAllowedSetupCalls`, `createTxValidatorForReqResponseReceivedTxs`, `createTxValidatorForTransactionsEnteringPendingTxPool`, and `getDefaultAllowedSetupFunctions` - **`yarn-project/validator-client/src/validator.test.ts`** (type): Used v4's `ReturnType<typeof generatePrivateKey>[]` over inline `0x${string}[]` for consistency with the imported `generatePrivateKey` - **`yarn-project/validator-client/src/validator.test.ts`** (method name): Used `getProposalHandler()` which matches the actual method defined in `validator.ts` ClaudeBox log: https://claudebox.work/s/24854bae46b9a696?run=2
2 parents 2821640 + 84a0680 commit c92c474

File tree

21 files changed

+755
-386
lines changed

21 files changed

+755
-386
lines changed

.github/workflows/deploy-staging-public.yml

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,40 +26,39 @@ jobs:
2626
token: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }}
2727
fetch-depth: 0
2828

29-
- name: Read version from manifest
30-
id: manifest
31-
run: |
32-
VERSION=$(jq -r '."."' .release-please-manifest.json)
33-
echo "version=$VERSION"
34-
echo "version=$VERSION" >> $GITHUB_OUTPUT
35-
36-
- name: Poll for tag at HEAD
29+
- name: Poll for semver tag at HEAD
3730
id: poll-tag
3831
run: |
39-
# wait for tag to be pushed (either RC or stable release)
40-
VERSION="${{ steps.manifest.outputs.version }}"
4132
HEAD_SHA=$(git rev-parse HEAD)
4233
MAX_ATTEMPTS=60
43-
echo "Looking for tag matching v${VERSION} or v${VERSION}-rc.* at HEAD ($HEAD_SHA)"
34+
echo "Looking for any semver tag at HEAD ($HEAD_SHA)"
4435
4536
for i in $(seq 1 $MAX_ATTEMPTS); do
4637
git fetch --tags --force
4738
48-
TAG=$(git tag --points-at HEAD | grep -E "^v${VERSION}(-rc\.[0-9]+)?$" | sort -V | tail -n 1 || true)
39+
# Collect all valid semver tags pointing at HEAD
40+
SEMVER_TAGS=()
41+
for t in $(git tag --points-at HEAD); do
42+
if ci3/semver check "$t"; then
43+
SEMVER_TAGS+=("$t")
44+
fi
45+
done
4946
50-
if [ -n "$TAG" ]; then
47+
# If we found valid semver tags, pick the highest
48+
if [ ${#SEMVER_TAGS[@]} -gt 0 ]; then
49+
TAG=$(ci3/semver sort "${SEMVER_TAGS[@]}" | tail -n 1)
5150
echo "Found tag: $TAG"
5251
SEMVER="${TAG#v}"
5352
echo "tag=$TAG" >> $GITHUB_OUTPUT
5453
echo "semver=$SEMVER" >> $GITHUB_OUTPUT
5554
exit 0
5655
fi
5756
58-
echo "Attempt $i/$MAX_ATTEMPTS: No matching tag yet, waiting 10s..."
57+
echo "Attempt $i/$MAX_ATTEMPTS: No semver tag yet, waiting 10s..."
5958
sleep 10
6059
done
6160
62-
echo "Error: No tag found for v${VERSION} at HEAD after 10 minutes"
61+
echo "Error: No semver tag found at HEAD after 10 minutes"
6362
exit 1
6463
6564
wait-for-ci3:

yarn-project/archiver/src/archiver-sync.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,56 @@ describe('Archiver Sync', () => {
12231223

12241224
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(2));
12251225
}, 15_000);
1226+
1227+
it('handles L1 reorg that moves a checkpoint to a later L1 block', async () => {
1228+
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(0));
1229+
1230+
// Sync checkpoints 1 and 2
1231+
await fake.addCheckpoint(CheckpointNumber(1), {
1232+
l1BlockNumber: 70n,
1233+
messagesL1BlockNumber: 50n,
1234+
numL1ToL2Messages: 3,
1235+
});
1236+
const { checkpoint: cp2 } = await fake.addCheckpoint(CheckpointNumber(2), {
1237+
l1BlockNumber: 80n,
1238+
messagesL1BlockNumber: 60n,
1239+
numL1ToL2Messages: 3,
1240+
});
1241+
1242+
fake.setL1BlockNumber(90n);
1243+
await archiver.syncImmediate();
1244+
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(2));
1245+
1246+
// Verify checkpoint 2's blocks are stored
1247+
const lastBlockNumber = cp2.blocks.at(-1)!.number;
1248+
const tips = await archiver.getL2Tips();
1249+
expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(2));
1250+
expect(tips.checkpointed.block.number).toEqual(lastBlockNumber);
1251+
1252+
// Simulate L1 reorg: checkpoint 2 moves from L1 block 80 to L1 block 85.
1253+
// The checkpoint content (blocks, archive) stays the same — only the L1 block changes.
1254+
// This causes the archiver to re-discover checkpoint 2 when scanning from block 81 onward.
1255+
fake.moveCheckpointToL1Block(CheckpointNumber(2), 85n);
1256+
1257+
// Advance L1 and sync. The archiver's sync point is at L1 block 80 (from checkpoint 2's
1258+
// original insertion). The scan starts from 81, finds checkpoint 2 at block 85, and must
1259+
// accept it as a duplicate with updated L1 info rather than throwing.
1260+
fake.setL1BlockNumber(95n);
1261+
await archiver.syncImmediate();
1262+
1263+
// The archiver should still be at checkpoint 2 and healthy
1264+
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(2));
1265+
1266+
// Add checkpoint 3 to verify the archiver can continue syncing after the duplicate
1267+
await fake.addCheckpoint(CheckpointNumber(3), {
1268+
l1BlockNumber: 100n,
1269+
messagesL1BlockNumber: 90n,
1270+
numL1ToL2Messages: 3,
1271+
});
1272+
fake.setL1BlockNumber(110n);
1273+
await archiver.syncImmediate();
1274+
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(3));
1275+
}, 15_000);
12261276
});
12271277

12281278
describe('finalized checkpoint', () => {

yarn-project/archiver/src/store/block_store.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,21 +227,34 @@ export class BlockStore {
227227
}
228228

229229
return await this.db.transactionAsync(async () => {
230-
// Check that the checkpoint immediately before the first block to be added is present in the store.
231230
const firstCheckpointNumber = checkpoints[0].checkpoint.number;
232231
const previousCheckpointNumber = await this.getLatestCheckpointNumber();
233232

234-
if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
233+
// Handle already-stored checkpoints at the start of the batch.
234+
// This can happen after an L1 reorg re-includes a checkpoint in a different L1 block.
235+
// We accept them if archives match (same content) and update their L1 metadata.
236+
if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
237+
checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
238+
if (checkpoints.length === 0) {
239+
return true;
240+
}
241+
// Re-check sequentiality after skipping
242+
const newFirstNumber = checkpoints[0].checkpoint.number;
243+
if (previousCheckpointNumber !== newFirstNumber - 1) {
244+
throw new InitialCheckpointNumberNotSequentialError(newFirstNumber, previousCheckpointNumber);
245+
}
246+
} else if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
235247
throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
236248
}
237249

238250
// Extract the previous checkpoint if there is one
251+
const currentFirstCheckpointNumber = checkpoints[0].checkpoint.number;
239252
let previousCheckpointData: CheckpointData | undefined = undefined;
240-
if (previousCheckpointNumber !== INITIAL_CHECKPOINT_NUMBER - 1) {
253+
if (currentFirstCheckpointNumber - 1 !== INITIAL_CHECKPOINT_NUMBER - 1) {
241254
// There should be a previous checkpoint
242-
previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
255+
previousCheckpointData = await this.getCheckpointData(CheckpointNumber(currentFirstCheckpointNumber - 1));
243256
if (previousCheckpointData === undefined) {
244-
throw new CheckpointNotFoundError(previousCheckpointNumber);
257+
throw new CheckpointNotFoundError(CheckpointNumber(currentFirstCheckpointNumber - 1));
245258
}
246259
}
247260

@@ -331,6 +344,50 @@ export class BlockStore {
331344
});
332345
}
333346

347+
/**
348+
* Handles checkpoints at the start of a batch that are already stored (e.g. due to L1 reorg).
349+
* Verifies the archive root matches, updates L1 metadata, and returns only the new checkpoints.
350+
*/
351+
private async skipOrUpdateAlreadyStoredCheckpoints(
352+
checkpoints: PublishedCheckpoint[],
353+
latestStored: CheckpointNumber,
354+
): Promise<PublishedCheckpoint[]> {
355+
let i = 0;
356+
for (; i < checkpoints.length && checkpoints[i].checkpoint.number <= latestStored; i++) {
357+
const incoming = checkpoints[i];
358+
const stored = await this.getCheckpointData(incoming.checkpoint.number);
359+
if (!stored) {
360+
// Should not happen if latestStored is correct, but be safe
361+
break;
362+
}
363+
// Verify the checkpoint content matches (archive root)
364+
if (!stored.archive.root.equals(incoming.checkpoint.archive.root)) {
365+
throw new Error(
366+
`Checkpoint ${incoming.checkpoint.number} already exists in store but with a different archive root. ` +
367+
`Stored: ${stored.archive.root}, incoming: ${incoming.checkpoint.archive.root}`,
368+
);
369+
}
370+
// Update L1 metadata and attestations for the already-stored checkpoint
371+
this.#log.warn(
372+
`Checkpoint ${incoming.checkpoint.number} already stored, updating L1 info ` +
373+
`(L1 block ${stored.l1.blockNumber} -> ${incoming.l1.blockNumber})`,
374+
);
375+
await this.#checkpoints.set(incoming.checkpoint.number, {
376+
header: incoming.checkpoint.header.toBuffer(),
377+
archive: incoming.checkpoint.archive.toBuffer(),
378+
checkpointOutHash: incoming.checkpoint.getCheckpointOutHash().toBuffer(),
379+
l1: incoming.l1.toBuffer(),
380+
attestations: incoming.attestations.map(a => a.toBuffer()),
381+
checkpointNumber: incoming.checkpoint.number,
382+
startBlock: incoming.checkpoint.blocks[0].number,
383+
blockCount: incoming.checkpoint.blocks.length,
384+
});
385+
// Update the sync point to reflect the new L1 block
386+
await this.#lastSynchedL1Block.set(incoming.l1.blockNumber);
387+
}
388+
return checkpoints.slice(i);
389+
}
390+
334391
private async addBlockToDatabase(block: L2Block, checkpointNumber: number, indexWithinCheckpoint: number) {
335392
const blockHash = await block.hash();
336393

yarn-project/archiver/src/store/kv_archiver_store.test.ts

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
makeInboxMessage,
5959
makeInboxMessages,
6060
makeInboxMessagesWithFullBlocks,
61+
makeL1PublishedData,
6162
makePrivateLog,
6263
makePrivateLogTag,
6364
makePublicLog,
@@ -134,10 +135,56 @@ describe('KVArchiverDataStore', () => {
134135
await expect(store.addCheckpoints(publishedCheckpoints)).resolves.toBe(true);
135136
});
136137

137-
it('throws on duplicate checkpoints', async () => {
138-
await store.addCheckpoints(publishedCheckpoints);
139-
await expect(store.addCheckpoints(publishedCheckpoints)).rejects.toThrow(
140-
InitialCheckpointNumberNotSequentialError,
138+
it('accepts duplicate checkpoints with matching archives and updates L1 info', async () => {
139+
// Add first 3 checkpoints
140+
const first3 = publishedCheckpoints.slice(0, 3);
141+
await store.addCheckpoints(first3);
142+
143+
// Verify initial L1 block number for checkpoint 3
144+
const beforeData = await store.getCheckpointData(CheckpointNumber(3));
145+
expect(beforeData).toBeDefined();
146+
const originalL1Block = beforeData!.l1.blockNumber;
147+
148+
// Re-add checkpoint 3 with the same content but different L1 published data
149+
// This simulates an L1 reorg that moved the checkpoint to a different L1 block
150+
const cp3WithNewL1 = new PublishedCheckpoint(
151+
first3[2].checkpoint,
152+
makeL1PublishedData(999),
153+
first3[2].attestations,
154+
);
155+
// Also add checkpoint 4 (the next one) in the same batch
156+
await store.addCheckpoints([cp3WithNewL1, publishedCheckpoints[3]]);
157+
158+
// Checkpoint 3's L1 info should be updated
159+
const afterData = await store.getCheckpointData(CheckpointNumber(3));
160+
expect(afterData).toBeDefined();
161+
expect(afterData!.l1.blockNumber).toEqual(999n);
162+
expect(afterData!.l1.blockNumber).not.toEqual(originalL1Block);
163+
164+
// Checkpoint 4 should be stored
165+
expect(await store.getSynchedCheckpointNumber()).toEqual(CheckpointNumber(4));
166+
});
167+
168+
it('accepts a batch that is entirely already-stored checkpoints', async () => {
169+
const first3 = publishedCheckpoints.slice(0, 3);
170+
await store.addCheckpoints(first3);
171+
172+
// Re-add the same 3 checkpoints — should succeed without error
173+
await expect(store.addCheckpoints(first3)).resolves.toBe(true);
174+
});
175+
176+
it('throws on duplicate checkpoints with mismatching archives', async () => {
177+
const first3 = publishedCheckpoints.slice(0, 3);
178+
await store.addCheckpoints(first3);
179+
180+
// Create a fake checkpoint 3 with a different archive root (content mismatch)
181+
const differentCheckpoint3 = await Checkpoint.random(CheckpointNumber(3), {
182+
numBlocks: 1,
183+
startBlockNumber: 3,
184+
});
185+
const mismatchedCp3 = makePublishedCheckpoint(differentCheckpoint3, 999);
186+
await expect(store.addCheckpoints([mismatchedCp3])).rejects.toThrow(
187+
'already exists in store but with a different archive',
141188
);
142189
});
143190

@@ -274,7 +321,7 @@ describe('KVArchiverDataStore', () => {
274321
await expect(store.addCheckpoints([publishedCheckpoint])).resolves.toBe(true);
275322
});
276323

277-
it('throws on duplicate initial checkpoint', async () => {
324+
it('throws on duplicate checkpoint with different content', async () => {
278325
const block1 = await L2Block.random(BlockNumber(1), {
279326
checkpointNumber: CheckpointNumber(1),
280327
indexWithinCheckpoint: IndexWithinCheckpoint(0),
@@ -303,7 +350,7 @@ describe('KVArchiverDataStore', () => {
303350

304351
await expect(store.addCheckpoints([publishedCheckpoint])).resolves.toBe(true);
305352
await expect(store.addCheckpoints([publishedCheckpoint2])).rejects.toThrow(
306-
InitialCheckpointNumberNotSequentialError,
353+
'already exists in store but with a different archive',
307354
);
308355
});
309356
});

yarn-project/archiver/src/test/fake_l1_state.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,21 @@ export class FakeL1State {
331331
this.updatePendingCheckpointNumber();
332332
}
333333

334+
/**
335+
* Moves a checkpoint to a different L1 block number (simulates L1 reorg that
336+
* re-includes the same checkpoint transaction in a different block).
337+
* The checkpoint content stays the same — only the L1 metadata changes.
338+
* Auto-updates pending status.
339+
*/
340+
moveCheckpointToL1Block(checkpointNumber: CheckpointNumber, newL1BlockNumber: bigint): void {
341+
for (const cpData of this.checkpoints) {
342+
if (cpData.checkpointNumber === checkpointNumber) {
343+
cpData.l1BlockNumber = newL1BlockNumber;
344+
}
345+
}
346+
this.updatePendingCheckpointNumber();
347+
}
348+
334349
/**
335350
* Removes messages after a given total index (simulates L1 reorg).
336351
* Auto-updates rolling hash.

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ import {
108108
FullNodeCheckpointsBuilder,
109109
NodeKeystoreAdapter,
110110
ValidatorClient,
111-
createBlockProposalHandler,
111+
createProposalHandler,
112112
createValidatorClient,
113113
} from '@aztec/validator-client';
114114
import type { SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
@@ -393,19 +393,21 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
393393
}
394394
}
395395

396-
// If there's no validator client, create a BlockProposalHandler to handle block proposals
396+
// If there's no validator client, create a ProposalHandler to handle block and checkpoint proposals
397397
// for monitoring or reexecution. Reexecution (default) allows us to follow the pending chain,
398398
// while non-reexecution is used for validating the proposals and collecting their txs.
399+
// Checkpoint proposals are handled if the blob client can upload blobs.
399400
if (!validatorClient) {
400401
const reexecute = !!config.alwaysReexecuteBlockProposals;
401-
log.info(`Setting up block proposal handler` + (reexecute ? ' with reexecution of proposals' : ''));
402-
createBlockProposalHandler(config, {
402+
log.info(`Setting up proposal handler` + (reexecute ? ' with reexecution of proposals' : ''));
403+
createProposalHandler(config, {
403404
checkpointsBuilder: validatorCheckpointsBuilder,
404405
worldState: worldStateSynchronizer,
405406
epochCache,
406407
blockSource: archiver,
407408
l1ToL2MessageSource: archiver,
408409
p2pClient,
410+
blobClient,
409411
dateProvider,
410412
telemetry,
411413
}).register(p2pClient, reexecute);

yarn-project/bootstrap.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ function test_cmds {
224224
# Aztec CLI tests
225225
aztec/bootstrap.sh test_cmds
226226

227-
if [[ "${TARGET_BRANCH:-}" =~ ^v[0-9]+$ ]]; then
227+
if [[ "${TARGET_BRANCH:-}" =~ ^(v[0-9]+(-next)?|backport-to-v[0-9]+-(staging|next))$ ]]; then
228228
echo "$hash yarn-project/scripts/run_test.sh aztec/src/testnet_compatibility.test.ts"
229229
echo "$hash yarn-project/scripts/run_test.sh aztec/src/mainnet_compatibility.test.ts"
230230
fi

0 commit comments

Comments
 (0)