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" } 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/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/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 2a2355ff28f3..674a5f5e64e3 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1041,7 +1041,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 @@ -1650,6 +1654,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/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..27c7c90fe9fe 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,7 @@ export async function setup( l1BlockTime: opts.ethereumSlotDuration, accounts: opts.anvilAccounts, port: opts.anvilPort ?? (process.env.ANVIL_PORT ? parseInt(process.env.ANVIL_PORT) : undefined), + dateProvider, }); anvil = res.anvil; config.l1RpcUrls = [res.rpcUrl]; @@ -317,8 +320,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 +415,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..7ea7e1e253f3 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,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; + /** + * 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; } = {}, ): Promise<{ anvil: Anvil; methodCalls?: string[]; rpcUrl: string; stop: () => Promise }> { const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh'); @@ -41,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()); @@ -49,9 +56,12 @@ 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); + } }); await anvil.start(); - if (!logger && !opts.captureMethodCalls) { + if (!logger && !opts.captureMethodCalls && !opts.dateProvider) { removeHandler(); } @@ -69,3 +79,16 @@ export async function startAnvil( // Object.defineProperty(anvil, 'port', { value: port, writable: false }); return { anvil, methodCalls, stop: () => anvil.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); + } + } +} 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); } /** diff --git a/yarn-project/validator-client/src/block_proposal_handler.ts b/yarn-project/validator-client/src/block_proposal_handler.ts index d38bb7d9332e..642ec9410144 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); + }); }); });