diff --git a/barretenberg/cpp/src/barretenberg/world_state/world_state.cpp b/barretenberg/cpp/src/barretenberg/world_state/world_state.cpp index 19adc24926b0..93ab14689978 100644 --- a/barretenberg/cpp/src/barretenberg/world_state/world_state.cpp +++ b/barretenberg/cpp/src/barretenberg/world_state/world_state.cpp @@ -587,16 +587,6 @@ WorldStateStatusFull WorldState::sync_block(const StateReference& block_state_re const std::vector& public_writes) { validate_trees_are_equally_synched(); - WorldStateStatusFull status; - if (is_same_state_reference(WorldStateRevision::uncommitted(), block_state_ref) && - is_archive_tip(WorldStateRevision::uncommitted(), block_header_hash)) { - std::pair result = commit(status); - if (!result.first) { - throw std::runtime_error(result.second); - } - populate_status_summary(status); - return status; - } rollback(); Fork::SharedPtr fork = retrieve_fork(CANONICAL_FORK_ID); @@ -658,22 +648,32 @@ WorldStateStatusFull WorldState::sync_block(const StateReference& block_state_re signal.wait_for_level(); - if (!success) { - throw std::runtime_error("Failed to sync block: " + err_message); - } + // Check resulting state and commit if successful + WorldStateStatusFull status; + try { + if (!success) { + throw std::runtime_error("Failed to sync block: " + err_message); + } - if (!is_archive_tip(WorldStateRevision::uncommitted(), block_header_hash)) { - throw std::runtime_error("Can't synch block: block header hash is not the tip of the archive tree"); - } + if (!is_archive_tip(WorldStateRevision::uncommitted(), block_header_hash)) { + throw std::runtime_error("Can't synch block: block header hash is not the tip of the archive tree"); + } - if (!is_same_state_reference(WorldStateRevision::uncommitted(), block_state_ref)) { - throw std::runtime_error("Can't synch block: block state does not match world state"); - } + if (!is_same_state_reference(WorldStateRevision::uncommitted(), block_state_ref)) { + throw std::runtime_error("Can't synch block: block state does not match world state"); + } - std::pair result = commit(status); - if (!result.first) { - throw std::runtime_error(result.second); + std::pair result = commit(status); + if (!result.first) { + throw std::runtime_error(result.second); + } + } catch (const std::exception& e) { + // We failed, rollback any uncommitted state before leaving + rollback(); + throw; } + + // Success return the status populate_status_summary(status); return status; } @@ -726,6 +726,9 @@ WorldStateStatusSummary WorldState::set_finalized_blocks(const block_number_t& t } WorldStateStatusFull WorldState::unwind_blocks(const block_number_t& toBlockNumber) { + // Ensure no uncommitted state + rollback(); + WorldStateRevision revision{ .forkId = CANONICAL_FORK_ID, .blockNumber = 0, .includeUncommitted = false }; std::array responses; get_all_tree_info(revision, responses); diff --git a/yarn-project/world-state/src/native/native_world_state.test.ts b/yarn-project/world-state/src/native/native_world_state.test.ts index 11b386cf5961..2896f702b550 100644 --- a/yarn-project/world-state/src/native/native_world_state.test.ts +++ b/yarn-project/world-state/src/native/native_world_state.test.ts @@ -15,7 +15,7 @@ import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import type { SiblingPath } from '@aztec/foundation/trees'; import { PublicDataWrite } from '@aztec/stdlib/avm'; -import type { L2Block } from '@aztec/stdlib/block'; +import { L2Block } from '@aztec/stdlib/block'; import { DatabaseVersion, DatabaseVersionManager } from '@aztec/stdlib/database-version'; import type { MerkleTreeLeafType, MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server'; import { makeGlobalVariables } from '@aztec/stdlib/testing'; @@ -832,6 +832,80 @@ describe('NativeWorldState', () => { }); }); + describe('Invalid Blocks', () => { + let ws: NativeWorldStateService; + let rollupAddress!: EthAddress; + + beforeEach(async () => { + rollupAddress = EthAddress.random(); + ws = await NativeWorldStateService.new(rollupAddress, dataDir, wsTreeMapSizes); + }); + + afterEach(async () => { + await ws.close(); + }); + + it('handles invalid blocks', async () => { + const fork = await ws.fork(); + + // Insert a few blocks + for (let i = 0; i < 4; i++) { + const blockNumber = i + 1; + const provenBlock = blockNumber - 2; + const { block, messages } = await mockBlock(blockNumber, 1, fork); + const status = await ws.handleL2BlockAndMessages(block, messages); + + expect(status.summary.unfinalizedBlockNumber).toBe(BigInt(blockNumber)); + expect(status.summary.oldestHistoricalBlock).toBe(1n); + + if (provenBlock > 0) { + const provenStatus = await ws.setFinalized(BigInt(provenBlock)); + expect(provenStatus.unfinalizedBlockNumber).toBe(BigInt(blockNumber)); + expect(provenStatus.finalizedBlockNumber).toBe(BigInt(provenBlock)); + expect(provenStatus.oldestHistoricalBlock).toBe(1n); + } else { + expect(status.summary.finalizedBlockNumber).toBe(0n); + } + } + + // Now build an invalid block, see that it is rejected and that we can then insert the correct block + { + const { block: block, messages } = await mockBlock(5, 1, fork); + const invalidBlock = L2Block.fromBuffer(block.toBuffer()); + invalidBlock.header.state.partial.nullifierTree.root = Fr.random(); + + await expect(ws.handleL2BlockAndMessages(invalidBlock, messages)).rejects.toThrow( + "Can't synch block: block state does not match world state", + ); + + // Accepts the correct block + await expect(ws.handleL2BlockAndMessages(block, messages)).resolves.toBeDefined(); + + const summary = await ws.getStatusSummary(); + expect(summary.unfinalizedBlockNumber).toBe(5n); + expect(summary.finalizedBlockNumber).toBe(2n); + expect(summary.oldestHistoricalBlock).toBe(1n); + } + + // Now we push another invalid block, see that it is rejected and check we can unwind to the last proven block + { + const { block: block, messages } = await mockBlock(6, 1, fork); + const invalidBlock = L2Block.fromBuffer(block.toBuffer()); + invalidBlock.header.state.partial.nullifierTree.root = Fr.random(); + + await expect(ws.handleL2BlockAndMessages(invalidBlock, messages)).rejects.toThrow( + "Can't synch block: block state does not match world state", + ); + + // Now we want to unwind to the last proven block + const unwindStatus = await ws.unwindBlocks(2n); + expect(unwindStatus.summary.unfinalizedBlockNumber).toBe(2n); + expect(unwindStatus.summary.finalizedBlockNumber).toBe(2n); + expect(unwindStatus.summary.oldestHistoricalBlock).toBe(1n); + } + }); + }); + describe('Finding leaves', () => { let block: L2Block; let messages: Fr[];