From 5145bae3ca1b7409ba514101aacf267145fb2136 Mon Sep 17 00:00:00 2001 From: benesjan Date: Tue, 17 Mar 2026 07:00:49 +0000 Subject: [PATCH 1/2] cherry-pick: 35ad0ab94c2 fix: off-by-1 in getBlockHashMembershipWitness archive snapshot (with conflicts) --- .../aztec-nr/aztec/src/oracle/block_header.nr | 102 +++++++++++++++++- .../aztec-node/src/aztec-node/server.ts | 27 +++++ 2 files changed, 126 insertions(+), 3 deletions(-) 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 99fd5edcbc77..2f6bc66c9148 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/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 4ea7c746cb74..4b6edfce9ebe 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1027,7 +1027,15 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { referenceBlock: BlockParameter, blockHash: BlockHash, ): Promise | undefined> { +<<<<<<< HEAD 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)); +>>>>>>> 35ad0ab94c (fix: off-by-1 in getBlockHashMembershipWitness archive snapshot) const [pathAndIndex] = await committedDb.findSiblingPaths(MerkleTreeId.ARCHIVE, [blockHash]); return pathAndIndex === undefined ? undefined @@ -1644,6 +1652,25 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { } } + /** 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 From 691baebe6eaccb7420a5c08bf0f94113f01cb1e0 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Tue, 17 Mar 2026 10:57:10 +0000 Subject: [PATCH 2/2] fix: resolve cherry-pick conflicts Adapted getWorldState call to use v4's private #getWorldState method instead of the public getWorldState used on next. --- yarn-project/aztec-node/src/aztec-node/server.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 4b6edfce9ebe..65c442ae3759 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1027,15 +1027,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { referenceBlock: BlockParameter, blockHash: BlockHash, ): Promise | undefined> { -<<<<<<< HEAD - 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)); ->>>>>>> 35ad0ab94c (fix: off-by-1 in getBlockHashMembershipWitness archive snapshot) + const committedDb = await this.#getWorldState(BlockNumber(referenceBlockNumber - 1)); const [pathAndIndex] = await committedDb.findSiblingPaths(MerkleTreeId.ARCHIVE, [blockHash]); return pathAndIndex === undefined ? undefined