From d23ae01a20279b80fb85cc5a82c83c4a463ac619 Mon Sep 17 00:00:00 2001 From: Aztec Bot <49558828+AztecBot@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:31:21 -0400 Subject: [PATCH 1/5] chore: merge v4 into v4-next (resolve conflicts) (#21702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Merges v4 into v4-next, resolving the conflict in `getBlockHashMembershipWitness` in `server.ts`. The conflict was between v4-next's simple passthrough to `getWorldState(referenceBlock)` and v4's off-by-one bugfix that uses `getWorldState(BlockNumber(referenceBlockNumber - 1))`. The v4 fix is correct: the Noir circuit checks the archive membership proof against `anchor_block_header.last_archive.root`, which is the archive tree root BEFORE the anchor block was added, so we need the world state at block N-1. Resolution: took the v4 bugfix, adapted to use v4-next's `getWorldState` (protected) instead of v4's `#getWorldState` (private). Replaces https://github.com/AztecProtocol/aztec-packages/pull/21493 which had stale conflict markers. ClaudeBox log: https://claudebox.work/s/d4b96d5b355d7fba?run=1 Co-authored-by: Jan Beneš Co-authored-by: PhilWindle <60546371+PhilWindle@users.noreply.github.com> Co-authored-by: Phil Windle Co-authored-by: Santiago Palladino Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: ludamad Co-authored-by: Alex Gherghisan --- .../cached_content_addressed_tree_store.hpp | 8 +- .../aztec-nr/aztec/src/oracle/block_header.nr | 102 +++++++++++++++++- spartan/environments/staging-public.env | 1 + .../aztec-node/src/aztec-node/server.ts | 25 ++++- .../src/block_proposal_handler.ts | 4 +- .../server_world_state_synchronizer.ts | 18 ++++ .../world-state/src/test/integration.test.ts | 38 +++++++ 7 files changed, 187 insertions(+), 9 deletions(-) diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_content_addressed_tree_store.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_content_addressed_tree_store.hpp index 01888967c45b..8093d2884049 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_content_addressed_tree_store.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_content_addressed_tree_store.hpp @@ -867,6 +867,10 @@ void ContentAddressedCachedTreeStore::advance_finalized_block(con ReadTransactionPtr readTx = create_read_transaction(); get_meta(uncommittedMeta); get_meta(committedMeta, *readTx, false); + // do nothing if the block is already finalized + if (committedMeta.finalizedBlockHeight >= blockNumber) { + return; + } if (!dataStore_->read_block_data(blockNumber, blockPayload, *readTx)) { throw std::runtime_error(format("Unable to advance finalized block: ", blockNumber, @@ -874,10 +878,6 @@ void ContentAddressedCachedTreeStore::advance_finalized_block(con forkConstantData_.name_)); } } - // do nothing if the block is already finalized - if (committedMeta.finalizedBlockHeight >= blockNumber) { - return; - } // can currently only finalize up to the unfinalized block height if (committedMeta.finalizedBlockHeight > committedMeta.unfinalizedBlockHeight) { diff --git a/noir-projects/aztec-nr/aztec/src/oracle/block_header.nr b/noir-projects/aztec-nr/aztec/src/oracle/block_header.nr index d1f7b81243e2..b213b73e4a7b 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/block_header.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/block_header.nr @@ -75,11 +75,107 @@ fn constrain_get_block_header_at_internal( } mod test { + use crate::protocol::traits::Hash; use crate::test::helpers::test_environment::TestEnvironment; - use super::{constrain_get_block_header_at_internal, get_block_header_at_internal}; + use super::{constrain_get_block_header_at_internal, get_block_header_at, get_block_header_at_internal}; - #[test(should_fail_with = "Proving membership of a block in archive failed")] - unconstrained fn fetching_header_with_mismatched_block_number_should_fail() { + #[test] + unconstrained fn fetching_earliest_block_header_succeeds() { + let env = TestEnvironment::new(); + + env.mine_block(); + env.mine_block(); + env.mine_block(); + env.mine_block(); + + env.private_context(|context| { + let anchor_block_header = context.anchor_block_header; + + let header = get_block_header_at_internal(1); + constrain_get_block_header_at_internal(header, 1, anchor_block_header); + + assert_eq(header.block_number(), 1); + }); + } + + #[test] + unconstrained fn fetching_past_block_header_succeeds() { + let env = TestEnvironment::new(); + + env.mine_block(); + env.mine_block(); + env.mine_block(); + env.mine_block(); + + env.private_context(|context| { + let anchor_block_header = context.anchor_block_header; + let target_block_number = anchor_block_header.block_number() - 2; + + let header = get_block_header_at_internal(target_block_number); + constrain_get_block_header_at_internal(header, target_block_number, anchor_block_header); + + assert_eq(header.block_number(), target_block_number); + }); + } + + #[test] + unconstrained fn fetching_header_immediately_before_anchor_succeeds() { + let env = TestEnvironment::new(); + + env.mine_block(); + env.mine_block(); + env.mine_block(); + env.mine_block(); + + // Block N-1 is the boundary case: last_archive covers exactly up to block N-1. + env.private_context(|context| { + let anchor_block_header = context.anchor_block_header; + let target_block_number = anchor_block_header.block_number() - 1; + + let header = get_block_header_at_internal(target_block_number); + constrain_get_block_header_at_internal(header, target_block_number, anchor_block_header); + + assert_eq(header.block_number(), target_block_number); + }); + } + + #[test] + unconstrained fn fetching_anchor_block_header_works() { + let env = TestEnvironment::new(); + + env.mine_block(); + env.mine_block(); + env.mine_block(); + env.mine_block(); + + env.private_context(|context| { + let anchor_block_number = context.anchor_block_header.block_number(); + + let header = get_block_header_at(anchor_block_number, *context); + + assert_eq(header.block_number(), anchor_block_number); + assert_eq(header.hash(), context.anchor_block_header.hash()); + }); + } + + #[test(should_fail_with = "Last archive block number is smaller than the block number")] + unconstrained fn fetching_future_block_header_fails() { + let env = TestEnvironment::new(); + + env.mine_block(); + env.mine_block(); + env.mine_block(); + env.mine_block(); + + env.private_context(|context| { + let anchor_block_number = context.anchor_block_header.block_number(); + + let _header = get_block_header_at(anchor_block_number + 1, *context); + }); + } + + #[test(should_fail_with = "Block number provided is not the same as the block number from the header hint")] + unconstrained fn fetching_header_with_mismatched_block_number_fails() { let env = TestEnvironment::new(); env.mine_block(); diff --git a/spartan/environments/staging-public.env b/spartan/environments/staging-public.env index fd3c5b52df47..fa894ca74041 100644 --- a/spartan/environments/staging-public.env +++ b/spartan/environments/staging-public.env @@ -31,6 +31,7 @@ SEQ_MAX_TX_PER_CHECKPOINT=7 # 0.1 TPS # Build checkpoint even if block is empty. SEQ_BUILD_CHECKPOINT_IF_EMPTY=true SEQ_BLOCK_DURATION_MS=6000 +SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT=36 CREATE_ROLLUP_CONTRACTS=false P2P_TX_POOL_DELETE_TXS_AFTER_REORG=true diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index cb373bc7d042..30de30bf7966 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1038,7 +1038,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { referenceBlock: BlockParameter, blockHash: BlockHash, ): Promise | undefined> { - const committedDb = await this.getWorldState(referenceBlock); + // The Noir circuit checks the archive membership proof against `anchor_block_header.last_archive.root`, + // which is the archive tree root BEFORE the anchor block was added (i.e. the state after block N-1). + // So we need the world state at block N-1, not block N, to produce a sibling path matching that root. + const referenceBlockNumber = await this.resolveBlockNumber(referenceBlock); + const committedDb = await this.getWorldState(BlockNumber(referenceBlockNumber - 1)); const [pathAndIndex] = await committedDb.findSiblingPaths(MerkleTreeId.ARCHIVE, [blockHash]); return pathAndIndex === undefined ? undefined @@ -1660,6 +1664,25 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return snapshot; } + /** Resolves a block parameter to a block number. */ + protected async resolveBlockNumber(block: BlockParameter): Promise { + if (block === 'latest') { + return BlockNumber(await this.blockSource.getBlockNumber()); + } + if (BlockHash.isBlockHash(block)) { + const initialBlockHash = await this.#getInitialHeaderHash(); + if (block.equals(initialBlockHash)) { + return BlockNumber.ZERO; + } + const header = await this.blockSource.getBlockHeaderByHash(block); + if (!header) { + throw new Error(`Block hash ${block.toString()} not found.`); + } + return header.getBlockNumber(); + } + return block as BlockNumber; + } + /** * Ensure we fully sync the world state * @returns A promise that fulfils once the world state is synced diff --git a/yarn-project/validator-client/src/block_proposal_handler.ts b/yarn-project/validator-client/src/block_proposal_handler.ts index 43c890bdafa8..1582c74b334c 100644 --- a/yarn-project/validator-client/src/block_proposal_handler.ts +++ b/yarn-project/validator-client/src/block_proposal_handler.ts @@ -487,7 +487,9 @@ export class BlockProposalHandler { } private getReexecuteFailureReason(err: any): BlockProposalValidationFailureReason { - if (err instanceof ReExInitialStateMismatchError) { + if (err instanceof TransactionsNotAvailableError) { + return 'txs_not_available'; + } else if (err instanceof ReExInitialStateMismatchError) { return 'initial_state_mismatch'; } else if (err instanceof ReExStateMismatchError) { return 'state_mismatch'; diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts index 511579f3d2e4..37d55fce1eb4 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts @@ -388,6 +388,18 @@ export class ServerWorldStateSynchronizer private async handleChainFinalized(blockNumber: BlockNumber) { this.log.verbose(`Finalized chain is now at block ${blockNumber}`); + // If the finalized block number is older than the oldest available block in world state, + // skip entirely. The finalized block number can jump backwards (e.g. when the finalization + // heuristic changes) and try to read block data that has already been pruned. When this + // happens, there is nothing useful to do — the native world state is already finalized + // past this point and pruning has already happened. + const currentSummary = await this.merkleTreeDb.getStatusSummary(); + if (blockNumber < currentSummary.oldestHistoricalBlock || blockNumber < 1) { + this.log.trace( + `Finalized block ${blockNumber} is older than the oldest available block ${currentSummary.oldestHistoricalBlock}. Skipping.`, + ); + return; + } const summary = await this.merkleTreeDb.setFinalized(blockNumber); if (this.historyToKeep === undefined) { return; @@ -421,6 +433,12 @@ export class ServerWorldStateSynchronizer } // Find the block at the start of the checkpoint and remove blocks up to this one const newHistoricBlock = historicCheckpoint.checkpoint.blocks[0]; + if (newHistoricBlock.number <= currentSummary.oldestHistoricalBlock) { + this.log.debug( + `Historic block ${newHistoricBlock.number} is not newer than oldest available ${currentSummary.oldestHistoricalBlock}. Skipping prune.`, + ); + return; + } this.log.verbose(`Pruning historic blocks to ${newHistoricBlock.number}`); const status = await this.merkleTreeDb.removeHistoricalBlocks(BlockNumber(newHistoricBlock.number)); this.log.debug(`World state summary `, status.summary); diff --git a/yarn-project/world-state/src/test/integration.test.ts b/yarn-project/world-state/src/test/integration.test.ts index 4f75871da279..fd1c096460dc 100644 --- a/yarn-project/world-state/src/test/integration.test.ts +++ b/yarn-project/world-state/src/test/integration.test.ts @@ -252,6 +252,44 @@ describe('world-state integration', () => { await awaitSync(5, 4); await expectSynchedToBlock(5, 4); }); + + it('does not throw when finalized block jumps backwards past pruned blocks', async () => { + // Create 20 blocks and sync them all + await archiver.createBlocks(MAX_CHECKPOINT_COUNT); + await synchronizer.start(); + await awaitSync(MAX_CHECKPOINT_COUNT); + await expectSynchedToBlock(MAX_CHECKPOINT_COUNT); + + // Manually finalize to block 15 and prune historical blocks up to block 10 + // to simulate world-state having pruned old data. + await db.setFinalized(BlockNumber(15)); + await db.removeHistoricalBlocks(BlockNumber(10)); + + const summary = await db.getStatusSummary(); + log.info( + `After manual finalize+prune: oldest=${summary.oldestHistoricalBlock}, finalized=${summary.finalizedBlockNumber}`, + ); + expect(summary.oldestHistoricalBlock).toBe(10); + expect(summary.finalizedBlockNumber).toBe(15); + + // Now simulate the scenario from PR #21597: finalized block jumps backwards + // to a block M that is older than oldestHistoricalBlock. + // This should NOT throw — the clamping logic should handle it. + const backwardsFinalized = BlockNumber(5); + log.info( + `Sending chain-finalized for block ${backwardsFinalized} (below oldest ${summary.oldestHistoricalBlock})`, + ); + await expect( + synchronizer.handleBlockStreamEvent({ + type: 'chain-finalized', + block: { number: backwardsFinalized, hash: '' }, + }), + ).resolves.not.toThrow(); + + // Finalized block should remain at 15 (unchanged by the backwards event) + const afterSummary = await db.getStatusSummary(); + expect(afterSummary.finalizedBlockNumber).toBe(15); + }); }); }); From dd377369b42069ce279db462dfb8348aca0da551 Mon Sep 17 00:00:00 2001 From: Aztec Bot <49558828+AztecBot@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:51:18 -0400 Subject: [PATCH 2/5] chore: merge v4 into v4-next (resolve conflicts) (#21705) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Merges latest `v4` (`bb3ea6f1`) into `v4-next` with a proper **merge commit** to preserve the merge topology. This fixes the issue where PR #21702 was squash-merged, causing git to lose track of which v4 commits were already in v4-next. ## Commits 1. **Merge commit with conflict markers** — raw `git merge origin/v4` showing the conflicts 2. **Fix server.ts** — resolve `getWorldState` vs `#getWorldState` conflict (use protected version), remove duplicate `resolveBlockNumber` method 3. **Fix deploy_network.sh** — resolve pre-existing conflict from old merge (keep default multiplier of 2) ## Important **This PR must be merged with a merge commit (not squashed)** to preserve the v4→v4-next merge topology. Add the `ci-no-squash` label. Without the merge commit, future auto-merges will re-encounter the same conflicts. ClaudeBox log: https://claudebox.work/s/3402fef75fe1ebde?run=4 --------- Co-authored-by: Jan Beneš Co-authored-by: PhilWindle <60546371+PhilWindle@users.noreply.github.com> Co-authored-by: Phil Windle Co-authored-by: Santiago Palladino Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: ludamad Co-authored-by: Alex Gherghisan Co-authored-by: spypsy --- spartan/scripts/deploy_network.sh | 4 -- .../src/modules/data_store_updater.ts | 2 +- .../stdlib/src/p2p/checkpoint_proposal.ts | 43 ++++++++++--------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/spartan/scripts/deploy_network.sh b/spartan/scripts/deploy_network.sh index 9a5300e83796..2973265afd68 100755 --- a/spartan/scripts/deploy_network.sh +++ b/spartan/scripts/deploy_network.sh @@ -107,11 +107,7 @@ PROVER_FAILED_PROOF_STORE=${PROVER_FAILED_PROOF_STORE:-} SEQ_MIN_TX_PER_BLOCK=${SEQ_MIN_TX_PER_BLOCK:-1} SEQ_MAX_TX_PER_BLOCK=${SEQ_MAX_TX_PER_BLOCK:-null} SEQ_MAX_TX_PER_CHECKPOINT=${SEQ_MAX_TX_PER_CHECKPOINT:-8} -<<<<<<< HEAD -SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER=${SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER:-2} -======= SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER=${SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER:-} ->>>>>>> origin/v4 SEQ_BLOCK_DURATION_MS=${SEQ_BLOCK_DURATION_MS:-} SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT=${SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT:-} SEQ_BUILD_CHECKPOINT_IF_EMPTY=${SEQ_BUILD_CHECKPOINT_IF_EMPTY:-} diff --git a/yarn-project/archiver/src/modules/data_store_updater.ts b/yarn-project/archiver/src/modules/data_store_updater.ts index 6bf2975b3c3f..40875ecec4e6 100644 --- a/yarn-project/archiver/src/modules/data_store_updater.ts +++ b/yarn-project/archiver/src/modules/data_store_updater.ts @@ -446,7 +446,7 @@ export class ArchiverDataStoreUpdater { if (validFnCount > 0) { this.log.verbose(`Storing ${validFnCount} functions for contract class ${contractClassId.toString()}`); } - return await this.store.addFunctions(contractClassId, validPrivateFns, validUtilityFns); + await this.store.addFunctions(contractClassId, validPrivateFns, validUtilityFns); } return true; } diff --git a/yarn-project/stdlib/src/p2p/checkpoint_proposal.ts b/yarn-project/stdlib/src/p2p/checkpoint_proposal.ts index ae84e24204d7..aa837388211c 100644 --- a/yarn-project/stdlib/src/p2p/checkpoint_proposal.ts +++ b/yarn-project/stdlib/src/p2p/checkpoint_proposal.ts @@ -178,29 +178,32 @@ export class CheckpointProposal extends Gossipable { blockNumber: lastBlockInfo?.blockHeader?.globalVariables.blockNumber ?? BlockNumber(0), dutyType: DutyType.CHECKPOINT_PROPOSAL, }; - const checkpointSignature = await payloadSigner(checkpointHash, checkpointContext); - if (!lastBlockInfo) { - return new CheckpointProposal(checkpointHeader, archiveRoot, feeAssetPriceModifier, checkpointSignature); + if (lastBlockInfo) { + // Sign block proposal before signing checkpoint proposal to ensure HA protection + const lastBlockProposal = await BlockProposal.createProposalFromSigner( + lastBlockInfo.blockHeader, + lastBlockInfo.indexWithinCheckpoint, + checkpointHeader.inHash, + archiveRoot, + lastBlockInfo.txHashes, + lastBlockInfo.txs, + payloadSigner, + ); + + const checkpointSignature = await payloadSigner(checkpointHash, checkpointContext); + + return new CheckpointProposal(checkpointHeader, archiveRoot, feeAssetPriceModifier, checkpointSignature, { + blockHeader: lastBlockInfo.blockHeader, + indexWithinCheckpoint: lastBlockInfo.indexWithinCheckpoint, + txHashes: lastBlockInfo.txHashes, + signature: lastBlockProposal.signature, + signedTxs: lastBlockProposal.signedTxs, + }); } - const lastBlockProposal = await BlockProposal.createProposalFromSigner( - lastBlockInfo.blockHeader, - lastBlockInfo.indexWithinCheckpoint, - checkpointHeader.inHash, - archiveRoot, - lastBlockInfo.txHashes, - lastBlockInfo.txs, - payloadSigner, - ); - - return new CheckpointProposal(checkpointHeader, archiveRoot, feeAssetPriceModifier, checkpointSignature, { - blockHeader: lastBlockInfo.blockHeader, - indexWithinCheckpoint: lastBlockInfo.indexWithinCheckpoint, - txHashes: lastBlockInfo.txHashes, - signature: lastBlockProposal.signature, - signedTxs: lastBlockProposal.signedTxs, - }); + const checkpointSignature = await payloadSigner(checkpointHash, checkpointContext); + return new CheckpointProposal(checkpointHeader, archiveRoot, feeAssetPriceModifier, checkpointSignature); } /** From 08cb6df0a00e4854d540f4047dbafa11b8978a70 Mon Sep 17 00:00:00 2001 From: Amin Sammara <84764772+aminsammara@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:38:35 +0000 Subject: [PATCH 3/5] chore: bump minor version to 4.2.0 (#21776) ## Summary - Bump `.release-please-manifest.json` from `4.1.0` to `4.2.0` in preparation for the next release cycle. --- .release-please-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 411256bcab3d..34a3350a9a92 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "4.1.0" + ".": "4.2.0" } From 518aa57dd3e17e8a268b3a84f761fffd9bb107bc Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 19 Mar 2026 23:19:11 -0300 Subject: [PATCH 4/5] cherry-pick: 562e53d2 fix: sync dateProvider from anvil stdout on every mined block (with conflicts) --- .../aztec/src/testing/anvil_test_watcher.ts | 2 +- yarn-project/aztec/src/testing/cheat_codes.ts | 2 +- .../end-to-end/src/e2e_cheat_codes.test.ts | 8 +- .../src/e2e_crowdfunding_and_claim.test.ts | 2 +- .../end-to-end/src/e2e_p2p/add_rollup.test.ts | 2 +- .../end-to-end/src/e2e_synching.test.ts | 2 +- yarn-project/end-to-end/src/fixtures/setup.ts | 20 ++-- .../src/simulators/lending_simulator.ts | 6 +- .../ethereum/src/test/eth_cheat_codes.ts | 10 +- .../ethereum/src/test/start_anvil.test.ts | 37 ++++++ yarn-project/ethereum/src/test/start_anvil.ts | 113 ++++++++++++++++++ 11 files changed, 182 insertions(+), 22 deletions(-) diff --git a/yarn-project/aztec/src/testing/anvil_test_watcher.ts b/yarn-project/aztec/src/testing/anvil_test_watcher.ts index 44ef6662a9c8..4eabc6583949 100644 --- a/yarn-project/aztec/src/testing/anvil_test_watcher.ts +++ b/yarn-project/aztec/src/testing/anvil_test_watcher.ts @@ -130,7 +130,7 @@ export class AnvilTestWatcher { return; } - const l1Time = (await this.cheatcodes.timestamp()) * 1000; + const l1Time = (await this.cheatcodes.lastBlockTimestamp()) * 1000; const wallTime = this.dateProvider.now(); if (l1Time > wallTime) { this.logger.warn(`L1 is ahead of wall time. Syncing wall time to L1 time`); diff --git a/yarn-project/aztec/src/testing/cheat_codes.ts b/yarn-project/aztec/src/testing/cheat_codes.ts index 94ebe0b87046..14413fd4ddd3 100644 --- a/yarn-project/aztec/src/testing/cheat_codes.ts +++ b/yarn-project/aztec/src/testing/cheat_codes.ts @@ -72,7 +72,7 @@ export class CheatCodes { * @param duration - The duration to advance time by (in seconds) */ async warpL2TimeAtLeastBy(sequencerClient: SequencerClient, node: AztecNode, duration: bigint | number) { - const currentTimestamp = await this.eth.timestamp(); + const currentTimestamp = await this.eth.lastBlockTimestamp(); const targetTimestamp = BigInt(currentTimestamp) + BigInt(duration); await this.warpL2TimeAtLeastTo(sequencerClient, node, targetTimestamp); } diff --git a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts index 7cb9093a28ca..0ed7ee58d828 100644 --- a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts +++ b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts @@ -46,19 +46,19 @@ describe('e2e_cheat_codes', () => { it.each([100, 42, 99])(`setNextBlockTimestamp by %i`, async increment => { const blockNumber = await ethCheatCodes.blockNumber(); - const timestamp = await ethCheatCodes.timestamp(); + const timestamp = await ethCheatCodes.lastBlockTimestamp(); await ethCheatCodes.setNextBlockTimestamp(timestamp + increment); - expect(await ethCheatCodes.timestamp()).toBe(timestamp); + expect(await ethCheatCodes.lastBlockTimestamp()).toBe(timestamp); await ethCheatCodes.mine(); expect(await ethCheatCodes.blockNumber()).toBe(blockNumber + 1); - expect(await ethCheatCodes.timestamp()).toBe(timestamp + increment); + expect(await ethCheatCodes.lastBlockTimestamp()).toBe(timestamp + increment); }); it('setNextBlockTimestamp to a past timestamp throws', async () => { - const timestamp = await ethCheatCodes.timestamp(); + const timestamp = await ethCheatCodes.lastBlockTimestamp(); const pastTimestamp = timestamp - 1000; await expect(async () => await ethCheatCodes.setNextBlockTimestamp(pastTimestamp)).rejects.toThrow( 'Timestamp error', diff --git a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts index 68711711b23b..5668ba4e3e77 100644 --- a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts +++ b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts @@ -60,7 +60,7 @@ describe('e2e_crowdfunding_and_claim', () => { } = await setup(3)); // We set the deadline to a week from now - deadline = (await cheatCodes.eth.timestamp()) + 7 * 24 * 60 * 60; + deadline = (await cheatCodes.eth.lastBlockTimestamp()) + 7 * 24 * 60 * 60; ({ contract: donationToken } = await TokenContract.deploy( wallet, diff --git a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts index c885c8a4745c..0db4c4f28055 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts @@ -518,7 +518,7 @@ describe('e2e_p2p_add_rollup', () => { const futureEpoch = EpochNumber.fromBigInt(500n + BigInt(await newRollup.getCurrentEpochNumber())); const futureSlot = SlotNumber.fromBigInt(BigInt(futureEpoch) * BigInt(t.ctx.aztecNodeConfig.aztecEpochDuration)); const time = await newRollup.getTimestampForSlot(futureSlot); - if (time > BigInt(await t.ctx.cheatCodes.eth.timestamp())) { + if (time > BigInt(await t.ctx.cheatCodes.eth.lastBlockTimestamp())) { await t.ctx.cheatCodes.eth.warp(Number(time)); await waitL1Block(); } diff --git a/yarn-project/end-to-end/src/e2e_synching.test.ts b/yarn-project/end-to-end/src/e2e_synching.test.ts index e03737b2669a..eeaa159b9c2e 100644 --- a/yarn-project/end-to-end/src/e2e_synching.test.ts +++ b/yarn-project/end-to-end/src/e2e_synching.test.ts @@ -466,7 +466,7 @@ describe('e2e_synching', () => { for (const checkpoint of checkpoints) { const lastBlock = checkpoint.blocks.at(-1)!; const targetTime = Number(lastBlock.header.globalVariables.timestamp) - ETHEREUM_SLOT_DURATION; - while ((await cheatCodes.eth.timestamp()) < targetTime) { + while ((await cheatCodes.eth.lastBlockTimestamp()) < targetTime) { await cheatCodes.eth.mine(); } // If it breaks here, first place you should look is the pruning. diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index 630173060ab8..5ae0ea0cc506 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -298,6 +298,8 @@ export async function setup( config.dataDirectory = directoryToCleanup; } + const dateProvider = new TestDateProvider(); + if (!config.l1RpcUrls?.length) { if (!isAnvilTestChain(chain.id)) { throw new Error(`No ETHEREUM_HOSTS set but non anvil chain requested`); @@ -306,6 +308,11 @@ export async function setup( l1BlockTime: opts.ethereumSlotDuration, accounts: opts.anvilAccounts, port: opts.anvilPort ?? (process.env.ANVIL_PORT ? parseInt(process.env.ANVIL_PORT) : undefined), +<<<<<<< HEAD +======= + slotsInAnEpoch: opts.anvilSlotsInAnEpoch, + dateProvider, +>>>>>>> 562e53d23d (fix: sync dateProvider from anvil stdout on every mined block) }); anvil = res.anvil; config.l1RpcUrls = [res.rpcUrl]; @@ -317,8 +324,6 @@ export async function setup( logger.info(`Logging metrics to ${filename}`); setupMetricsLogger(filename); } - - const dateProvider = new TestDateProvider(); const ethCheatCodes = new EthCheatCodesWithState(config.l1RpcUrls, dateProvider); if (opts.stateLoad) { @@ -414,11 +419,12 @@ export async function setup( await ethCheatCodes.setIntervalMining(config.ethereumSlotDuration); } - // Always sync dateProvider to L1 time after deploying L1 contracts, regardless of mining mode. - // In compose mode, L1 time may have drifted ahead of system time due to the local-network watcher - // warping time forward on each filled slot. Without this sync, the sequencer computes the wrong - // slot from its dateProvider and cannot propose blocks. - dateProvider.setTime((await ethCheatCodes.timestamp()) * 1000); + // In compose mode (no local anvil), sync dateProvider to L1 time since it may have drifted + // ahead of system time due to the local-network watcher warping time forward on each filled slot. + // When running with a local anvil, the dateProvider is kept in sync via the stdout listener. + if (!anvil) { + dateProvider.setTime((await ethCheatCodes.lastBlockTimestamp()) * 1000); + } if (opts.l2StartTime) { await ethCheatCodes.warp(opts.l2StartTime, { resetBlockInterval: true }); diff --git a/yarn-project/end-to-end/src/simulators/lending_simulator.ts b/yarn-project/end-to-end/src/simulators/lending_simulator.ts index 404bb3d5ad8d..ae299b31e249 100644 --- a/yarn-project/end-to-end/src/simulators/lending_simulator.ts +++ b/yarn-project/end-to-end/src/simulators/lending_simulator.ts @@ -94,7 +94,9 @@ export class LendingSimulator { async prepare() { this.accumulator = BASE; - const slot = await this.rollup.getSlotAt(BigInt(await this.cc.eth.timestamp()) + BigInt(this.ethereumSlotDuration)); + const slot = await this.rollup.getSlotAt( + BigInt(await this.cc.eth.lastBlockTimestamp()) + BigInt(this.ethereumSlotDuration), + ); this.time = Number(await this.rollup.getTimestampForSlot(slot)); } @@ -103,7 +105,7 @@ export class LendingSimulator { return; } - const slot = await this.rollup.getSlotAt(BigInt(await this.cc.eth.timestamp())); + const slot = await this.rollup.getSlotAt(BigInt(await this.cc.eth.lastBlockTimestamp())); const targetSlot = SlotNumber(slot + diff); const ts = Number(await this.rollup.getTimestampForSlot(targetSlot)); const timeDiff = ts - this.time; diff --git a/yarn-project/ethereum/src/test/eth_cheat_codes.ts b/yarn-project/ethereum/src/test/eth_cheat_codes.ts index d62b194c0241..b1cbdd199d6f 100644 --- a/yarn-project/ethereum/src/test/eth_cheat_codes.ts +++ b/yarn-project/ethereum/src/test/eth_cheat_codes.ts @@ -85,10 +85,12 @@ export class EthCheatCodes { } /** - * Get the current timestamp - * @returns The current timestamp + * Get the timestamp of the latest mined L1 block. + * Note: this is NOT the current time — it's the discrete timestamp of the last block. + * Between blocks, the actual chain time advances but no new block reflects it. + * @returns The latest block timestamp in seconds */ - public async timestamp(): Promise { + public async lastBlockTimestamp(): Promise { const res = await this.doRpcCall('eth_getBlockByNumber', ['latest', true]); return parseInt(res.timestamp, 16); } @@ -552,7 +554,7 @@ export class EthCheatCodes { } public async syncDateProvider() { - const timestamp = await this.timestamp(); + const timestamp = await this.lastBlockTimestamp(); if ('setTime' in this.dateProvider) { this.dateProvider.setTime(timestamp * 1000); } diff --git a/yarn-project/ethereum/src/test/start_anvil.test.ts b/yarn-project/ethereum/src/test/start_anvil.test.ts index d958ac55bbd7..5d01f6a45030 100644 --- a/yarn-project/ethereum/src/test/start_anvil.test.ts +++ b/yarn-project/ethereum/src/test/start_anvil.test.ts @@ -1,5 +1,6 @@ import { type Logger, createLogger } from '@aztec/foundation/log'; import { sleep } from '@aztec/foundation/sleep'; +import { TestDateProvider } from '@aztec/foundation/timer'; import type { Anvil } from '@viem/anvil'; import { createPublicClient, http, parseAbiItem } from 'viem'; @@ -54,4 +55,40 @@ describe('start_anvil', () => { stopWatching(); }, 500); }); + + it('syncs dateProvider to anvil block time on each mined block', async () => { + // Stop the default anvil instance (no dateProvider). + await anvil.stop(); + + const dateProvider = new TestDateProvider(); + const res = await startAnvil({ dateProvider }); + anvil = res.anvil; + rpcUrl = res.rpcUrl; + + const publicClient = createPublicClient({ transport: http(rpcUrl, { batch: false }) }); + + // Mine a block so anvil emits a "Block Time" line. + await publicClient.request({ method: 'evm_mine', params: [] } as any); + // Give the stdout listener time to fire. + await sleep(200); + + const block = await publicClient.getBlock({ blockTag: 'latest' }); + const blockTimeMs = Number(block.timestamp) * 1000; + // The dateProvider should now be within 2 seconds of the anvil block time. + // TestDateProvider.now() = Date.now() + offset, and setTime sets offset = blockTimeMs - Date.now(), + // so subsequent now() calls return blockTimeMs + elapsed. We check the difference is small. + expect(Math.abs(dateProvider.now() - blockTimeMs)).toBeLessThan(2000); + + // Warp anvil forward by 1000 seconds and verify the dateProvider follows. + const futureTimestamp = Number(block.timestamp) + 1000; + await publicClient.request({ + method: 'evm_setNextBlockTimestamp', + params: [futureTimestamp], + } as any); + await publicClient.request({ method: 'evm_mine', params: [] } as any); + await sleep(200); + + const futureTimeMs = futureTimestamp * 1000; + expect(Math.abs(dateProvider.now() - futureTimeMs)).toBeLessThan(2000); + }); }); diff --git a/yarn-project/ethereum/src/test/start_anvil.ts b/yarn-project/ethereum/src/test/start_anvil.ts index 8a70b29fc12f..6d80d4e0c567 100644 --- a/yarn-project/ethereum/src/test/start_anvil.ts +++ b/yarn-project/ethereum/src/test/start_anvil.ts @@ -1,5 +1,6 @@ import { createLogger } from '@aztec/foundation/log'; import { makeBackoff, retry } from '@aztec/foundation/retry'; +import type { TestDateProvider } from '@aztec/foundation/timer'; import { fileURLToPath } from '@aztec/foundation/url'; import { type Anvil, createAnvil } from '@viem/anvil'; @@ -18,6 +19,22 @@ export async function startAnvil( chainId?: number; /** The hardfork to use - note: @viem/anvil types are out of date but 'cancun' and 'latest' work */ hardfork?: string; +<<<<<<< HEAD +======= + /** + * Number of slots per epoch used by anvil to compute the 'finalized' and 'safe' block tags. + * Anvil reports `finalized = latest - slotsInAnEpoch * 2`. + * Defaults to 1 so the finalized block advances immediately, making tests that check + * L1-finality-based logic work without needing hundreds of mined blocks. + */ + slotsInAnEpoch?: number; + /** + * If provided, the date provider will be synced to anvil's block time on every mined block. + * This keeps the dateProvider in lockstep with anvil's chain time, avoiding drift between + * the wall clock and the L1 chain when computing L1 slot timestamps. + */ + dateProvider?: TestDateProvider; +>>>>>>> 562e53d23d (fix: sync dateProvider from anvil stdout on every mined block) } = {}, ): Promise<{ anvil: Anvil; methodCalls?: string[]; rpcUrl: string; stop: () => Promise }> { const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh'); @@ -50,9 +67,30 @@ export async function startAnvil( port = parseInt(message.match(/Listening on ([^:]+):(\d+)/)![2]); } }); +<<<<<<< HEAD await anvil.start(); if (!logger && !opts.captureMethodCalls) { removeHandler(); +======= + + // Continue piping for logging, method-call capture, and/or dateProvider sync after startup. + if (logger || opts.captureMethodCalls || opts.dateProvider) { + child.stdout?.on('data', (data: Buffer) => { + const text = data.toString(); + logger?.debug(text.trim()); + methodCalls?.push(...(text.match(/eth_[^\s]+/g) || [])); + if (opts.dateProvider) { + syncDateProviderFromAnvilOutput(text, opts.dateProvider); + } + }); + child.stderr?.on('data', (data: Buffer) => { + logger?.debug(data.toString().trim()); + }); + } else { + // Consume streams so the child process doesn't block on full pipe buffers. + child.stdout?.resume(); + child.stderr?.resume(); +>>>>>>> 562e53d23d (fix: sync dateProvider from anvil stdout on every mined block) } return anvil; @@ -65,7 +103,82 @@ export async function startAnvil( throw new Error('Failed to start anvil'); } +<<<<<<< HEAD // Monkeypatch the anvil instance to include the actually assigned port // Object.defineProperty(anvil, 'port', { value: port, writable: false }); return { anvil, methodCalls, stop: () => anvil.stop(), rpcUrl: `http://127.0.0.1:${port}` }; +======= + const port = detectedPort; + let status: 'listening' | 'idle' = 'listening'; + + anvil.once('close', () => { + status = 'idle'; + }); + + const stop = async () => { + if (status === 'idle') { + return; + } + await killChild(anvil); + }; + + const anvilObj: Anvil = { + port, + host: '127.0.0.1', + get status() { + return status; + }, + stop, + }; + + return { anvil: anvilObj, methodCalls, stop, rpcUrl: `http://127.0.0.1:${port}` }; +} + +/** Extracts block time from anvil stdout and syncs the dateProvider. */ +function syncDateProviderFromAnvilOutput(text: string, dateProvider: TestDateProvider): void { + // Anvil logs mined blocks as: + // Block Time: "Fri, 20 Mar 2026 02:10:46 +0000" + const match = text.match(/Block Time:\s*"([^"]+)"/); + if (match) { + const blockTimeMs = new Date(match[1]).getTime(); + if (!isNaN(blockTimeMs)) { + dateProvider.setTime(blockTimeMs); + } + } +} + +/** Send SIGTERM, wait up to 5 s, then SIGKILL. All timers are always cleared. */ +function killChild(child: ChildProcess): Promise { + return new Promise(resolve => { + if (child.exitCode !== null || child.killed) { + child.stdout?.destroy(); + child.stderr?.destroy(); + resolve(); + return; + } + + let killTimer: NodeJS.Timeout | undefined; + + const onClose = () => { + if (killTimer !== undefined) { + clearTimeout(killTimer); + } + // Destroy stdio streams so their PipeWrap handles don't keep the event loop alive. + child.stdout?.destroy(); + child.stderr?.destroy(); + resolve(); + }; + + child.once('close', onClose); + child.kill('SIGTERM'); + + killTimer = setTimeout(() => { + killTimer = undefined; + child.kill('SIGKILL'); + }, 5000); + + // Ensure the timer does not prevent Node from exiting. + killTimer.unref(); + }); +>>>>>>> 562e53d23d (fix: sync dateProvider from anvil stdout on every mined block) } From f87eddf080b41affd861b826adc603f9670a7288 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Fri, 20 Mar 2026 14:05:42 +0000 Subject: [PATCH 5/5] fix: resolve cherry-pick conflicts --- yarn-project/end-to-end/src/fixtures/setup.ts | 4 - yarn-project/ethereum/src/test/start_anvil.ts | 100 +----------------- 2 files changed, 5 insertions(+), 99 deletions(-) diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index 5ae0ea0cc506..27c7c90fe9fe 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -308,11 +308,7 @@ export async function setup( l1BlockTime: opts.ethereumSlotDuration, accounts: opts.anvilAccounts, port: opts.anvilPort ?? (process.env.ANVIL_PORT ? parseInt(process.env.ANVIL_PORT) : undefined), -<<<<<<< HEAD -======= - slotsInAnEpoch: opts.anvilSlotsInAnEpoch, dateProvider, ->>>>>>> 562e53d23d (fix: sync dateProvider from anvil stdout on every mined block) }); anvil = res.anvil; config.l1RpcUrls = [res.rpcUrl]; diff --git a/yarn-project/ethereum/src/test/start_anvil.ts b/yarn-project/ethereum/src/test/start_anvil.ts index 6d80d4e0c567..7ea7e1e253f3 100644 --- a/yarn-project/ethereum/src/test/start_anvil.ts +++ b/yarn-project/ethereum/src/test/start_anvil.ts @@ -19,22 +19,12 @@ export async function startAnvil( chainId?: number; /** The hardfork to use - note: @viem/anvil types are out of date but 'cancun' and 'latest' work */ hardfork?: string; -<<<<<<< HEAD -======= - /** - * Number of slots per epoch used by anvil to compute the 'finalized' and 'safe' block tags. - * Anvil reports `finalized = latest - slotsInAnEpoch * 2`. - * Defaults to 1 so the finalized block advances immediately, making tests that check - * L1-finality-based logic work without needing hundreds of mined blocks. - */ - slotsInAnEpoch?: number; /** * If provided, the date provider will be synced to anvil's block time on every mined block. * This keeps the dateProvider in lockstep with anvil's chain time, avoiding drift between * the wall clock and the L1 chain when computing L1 slot timestamps. */ dateProvider?: TestDateProvider; ->>>>>>> 562e53d23d (fix: sync dateProvider from anvil stdout on every mined block) } = {}, ): Promise<{ anvil: Anvil; methodCalls?: string[]; rpcUrl: string; stop: () => Promise }> { const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh'); @@ -58,7 +48,7 @@ export async function startAnvil( chainId: opts.chainId ?? 31337, }); - // Listen to the anvil output to get the port. + // Listen to the anvil output to get the port and sync dateProvider. const removeHandler = anvil.on('message', (message: string) => { logger?.debug(message.trim()); @@ -66,31 +56,13 @@ export async function startAnvil( if (port === undefined && message.includes('Listening on')) { port = parseInt(message.match(/Listening on ([^:]+):(\d+)/)![2]); } + if (opts.dateProvider) { + syncDateProviderFromAnvilOutput(message, opts.dateProvider); + } }); -<<<<<<< HEAD await anvil.start(); - if (!logger && !opts.captureMethodCalls) { + if (!logger && !opts.captureMethodCalls && !opts.dateProvider) { removeHandler(); -======= - - // Continue piping for logging, method-call capture, and/or dateProvider sync after startup. - if (logger || opts.captureMethodCalls || opts.dateProvider) { - child.stdout?.on('data', (data: Buffer) => { - const text = data.toString(); - logger?.debug(text.trim()); - methodCalls?.push(...(text.match(/eth_[^\s]+/g) || [])); - if (opts.dateProvider) { - syncDateProviderFromAnvilOutput(text, opts.dateProvider); - } - }); - child.stderr?.on('data', (data: Buffer) => { - logger?.debug(data.toString().trim()); - }); - } else { - // Consume streams so the child process doesn't block on full pipe buffers. - child.stdout?.resume(); - child.stderr?.resume(); ->>>>>>> 562e53d23d (fix: sync dateProvider from anvil stdout on every mined block) } return anvil; @@ -103,35 +75,9 @@ export async function startAnvil( throw new Error('Failed to start anvil'); } -<<<<<<< HEAD // Monkeypatch the anvil instance to include the actually assigned port // Object.defineProperty(anvil, 'port', { value: port, writable: false }); return { anvil, methodCalls, stop: () => anvil.stop(), rpcUrl: `http://127.0.0.1:${port}` }; -======= - const port = detectedPort; - let status: 'listening' | 'idle' = 'listening'; - - anvil.once('close', () => { - status = 'idle'; - }); - - const stop = async () => { - if (status === 'idle') { - return; - } - await killChild(anvil); - }; - - const anvilObj: Anvil = { - port, - host: '127.0.0.1', - get status() { - return status; - }, - stop, - }; - - return { anvil: anvilObj, methodCalls, stop, rpcUrl: `http://127.0.0.1:${port}` }; } /** Extracts block time from anvil stdout and syncs the dateProvider. */ @@ -146,39 +92,3 @@ function syncDateProviderFromAnvilOutput(text: string, dateProvider: TestDatePro } } } - -/** Send SIGTERM, wait up to 5 s, then SIGKILL. All timers are always cleared. */ -function killChild(child: ChildProcess): Promise { - return new Promise(resolve => { - if (child.exitCode !== null || child.killed) { - child.stdout?.destroy(); - child.stderr?.destroy(); - resolve(); - return; - } - - let killTimer: NodeJS.Timeout | undefined; - - const onClose = () => { - if (killTimer !== undefined) { - clearTimeout(killTimer); - } - // Destroy stdio streams so their PipeWrap handles don't keep the event loop alive. - child.stdout?.destroy(); - child.stderr?.destroy(); - resolve(); - }; - - child.once('close', onClose); - child.kill('SIGTERM'); - - killTimer = setTimeout(() => { - killTimer = undefined; - child.kill('SIGKILL'); - }, 5000); - - // Ensure the timer does not prevent Node from exiting. - killTimer.unref(); - }); ->>>>>>> 562e53d23d (fix: sync dateProvider from anvil stdout on every mined block) -}