diff --git a/packages/protocol/contracts/L1/TaikoL1.sol b/packages/protocol/contracts/L1/TaikoL1.sol index 921546c622e..694de59b79a 100644 --- a/packages/protocol/contracts/L1/TaikoL1.sol +++ b/packages/protocol/contracts/L1/TaikoL1.sol @@ -167,8 +167,13 @@ contract TaikoL1 is /// @notice Important: as this contract doesn't send each block's state root as a signal when /// the block is verified, bridging developers should subscribe to CrossChainSynced events /// to ensure all synced state roots are verifiable using merkle proofs. - function getSyncedSnippet() public view override returns (ICrossChainSync.Snippet memory) { - return LibUtils.getSyncedSnippet(state, getConfig()); + function getSyncedSnippet(uint64 blockId) + public + view + override + returns (ICrossChainSync.Snippet memory) + { + return LibUtils.getSyncedSnippet(state, getConfig(), blockId); } /// @notice Gets the state variables of the TaikoL1 contract. diff --git a/packages/protocol/contracts/L1/libs/LibUtils.sol b/packages/protocol/contracts/L1/libs/LibUtils.sol index dd61849aba7..91e21603f34 100644 --- a/packages/protocol/contracts/L1/libs/LibUtils.sol +++ b/packages/protocol/contracts/L1/libs/LibUtils.sol @@ -55,25 +55,24 @@ library LibUtils { function getSyncedSnippet( TaikoData.State storage state, - TaikoData.Config memory config + TaikoData.Config memory config, + uint64 blockId ) external view returns (ICrossChainSync.Snippet memory) { - uint64 _blockId = state.slotB.lastVerifiedBlockId; + uint64 _blockId = blockId == 0 ? state.slotB.lastVerifiedBlockId : blockId; uint64 slot = _blockId % config.blockRingBufferSize; TaikoData.Block storage blk = state.blocks[slot]; - if (blk.blockId != _blockId) revert L1_BLOCK_MISMATCH(); if (blk.verifiedTransitionId == 0) revert L1_TRANSITION_NOT_FOUND(); - TaikoData.TransitionState storage transition = state.transitions[slot][blk.verifiedTransitionId]; return ICrossChainSync.Snippet({ - blockId: _blockId, + blockId: blockId, blockHash: transition.blockHash, stateRoot: transition.stateRoot }); diff --git a/packages/protocol/contracts/L2/TaikoL2.sol b/packages/protocol/contracts/L2/TaikoL2.sol index 8f70d392cac..db31db47f5e 100644 --- a/packages/protocol/contracts/L2/TaikoL2.sol +++ b/packages/protocol/contracts/L2/TaikoL2.sol @@ -45,15 +45,15 @@ contract TaikoL2 is CrossChainOwned, ICrossChainSync { // Mapping from L2 block numbers to their block hashes. // All L2 block hashes will be saved in this mapping. - mapping(uint256 blockId => bytes32 blockHash) public l2Hashes; // slot 1 + mapping(uint256 blockId => bytes32 blockHash) public l2Hashes; + mapping(uint256 l1height => ICrossChainSync.Snippet) public snippets; // A hash to check the integrity of public inputs. - bytes32 public publicInputHash; // slot 2 - uint64 public gasExcess; // slot 3 + bytes32 public publicInputHash; // slot 3 + uint64 public gasExcess; // slot 4 + uint64 public latestSyncedL1Height; - Snippet private _l1Snippet; // slot 4, 5, 6 - - uint256[144] private __gap; + uint256[146] private __gap; event Anchored(bytes32 parentHash, uint64 gasExcess); @@ -147,15 +147,17 @@ contract TaikoL2 is CrossChainOwned, ICrossChainSync { ownerChainId, "state_root", l1StateRoot ); + emit CrossChainSynced(l1Height, l1BlockHash, l1StateRoot); + // Update state variables l2Hashes[parentId] = blockhash(parentId); + snippets[l1Height] = ICrossChainSync.Snippet({ + blockId: l1Height, + blockHash: l1BlockHash, + stateRoot: l1StateRoot + }); publicInputHash = publicInputHashNew; - - _l1Snippet.blockId = l1Height; - _l1Snippet.blockHash = l1BlockHash; - _l1Snippet.stateRoot = l1StateRoot; - emit CrossChainSynced(l1Height, l1BlockHash, l1StateRoot); - + latestSyncedL1Height = l1Height; emit Anchored(blockhash(parentId), gasExcess); } @@ -170,8 +172,14 @@ contract TaikoL2 is CrossChainOwned, ICrossChainSync { } /// @inheritdoc ICrossChainSync - function getSyncedSnippet() public view returns (ICrossChainSync.Snippet memory) { - return _l1Snippet; + function getSyncedSnippet(uint64 blockId) + public + view + override + returns (ICrossChainSync.Snippet memory) + { + uint256 id = blockId == 0 ? latestSyncedL1Height : blockId; + return snippets[id]; } /// @notice Gets the basefee and gas excess using EIP-1559 configuration for @@ -263,10 +271,10 @@ contract TaikoL2 is CrossChainOwned, ICrossChainSync { // Calculate how much more gas to issue to offset gas excess. // after each L1 block time, config.gasTarget more gas is issued, // the gas excess will be reduced accordingly. - // Note that when _l1Snippet.blockId is zero, we skip this step. + // Note that when latestSyncedL1Height is zero, we skip this step. uint256 numL1Blocks; - if (_l1Snippet.blockId > 0 && l1Height > _l1Snippet.blockId) { - numL1Blocks = l1Height - _l1Snippet.blockId; + if (latestSyncedL1Height > 0 && l1Height > latestSyncedL1Height) { + numL1Blocks = l1Height - latestSyncedL1Height; } if (numL1Blocks > 0) { diff --git a/packages/protocol/contracts/common/ICrossChainSync.sol b/packages/protocol/contracts/common/ICrossChainSync.sol index 43126b51312..b04df85fdde 100644 --- a/packages/protocol/contracts/common/ICrossChainSync.sol +++ b/packages/protocol/contracts/common/ICrossChainSync.sol @@ -34,6 +34,8 @@ interface ICrossChainSync { event CrossChainSynced(uint64 indexed blockId, bytes32 blockHash, bytes32 stateRoot); /// @notice Fetches the hash of a block from the opposite chain. + /// @param blockId The target block id. Specifying 0 retrieves the hash + /// of the latest block. /// @return snippet The block hash and signal root synced. - function getSyncedSnippet() external view returns (Snippet memory snippet); + function getSyncedSnippet(uint64 blockId) external view returns (Snippet memory snippet); } diff --git a/packages/protocol/test/HelperContracts.sol b/packages/protocol/test/HelperContracts.sol index 1b8e93cd1a2..ae002e2d2a0 100644 --- a/packages/protocol/test/HelperContracts.sol +++ b/packages/protocol/test/HelperContracts.sol @@ -61,7 +61,7 @@ contract DummyCrossChainSync is EssentialContract, ICrossChainSync { _snippet.stateRoot = stateRoot; } - function getSyncedSnippet() public view returns (Snippet memory) { + function getSyncedSnippet(uint64 /*blockId*/ ) public view returns (Snippet memory) { return _snippet; } } diff --git a/packages/protocol/test/L1/TaikoL1.t.sol b/packages/protocol/test/L1/TaikoL1.t.sol index 92e3e43f436..86ade3ceea4 100644 --- a/packages/protocol/test/L1/TaikoL1.t.sol +++ b/packages/protocol/test/L1/TaikoL1.t.sol @@ -192,11 +192,13 @@ contract TaikoL1Test is TaikoL1TestBase { } /// @dev getCrossChainBlockHash tests - function test_L1_getCrossChainSnippet_Genesis() external { - ICrossChainSync.Snippet memory snippet = L1.getSyncedSnippet(); - assertEq(snippet.blockId, 0); - assertEq(snippet.blockHash, GENESIS_BLOCK_HASH); - assertEq(snippet.stateRoot, 0); + function test_L1_getCrossChainBlockHash0() external { + bytes32 genHash = L1.getSyncedSnippet(0).blockHash; + assertEq(GENESIS_BLOCK_HASH, genHash); + + // Reverts if block is not yet verified! + vm.expectRevert(TaikoErrors.L1_BLOCK_MISMATCH.selector); + L1.getSyncedSnippet(1); } /// @dev getSyncedSnippet tests @@ -233,13 +235,26 @@ contract TaikoL1Test is TaikoL1TestBase { verifyBlock(Carol, 1); - assertEq(L1.getSyncedSnippet().blockId, blockId); - assertEq(L1.getSyncedSnippet().blockHash, blockHash); - assertEq(L1.getSyncedSnippet().stateRoot, stateRoot); + // Querying written blockhash + assertEq(L1.getSyncedSnippet(blockId).blockHash, blockHash); mine(5); parentHashes[blockId] = blockHash; } + + uint64 queriedBlockId = 1; + bytes32 expectedSR = bytes32(1e9 + uint256(queriedBlockId)); + + assertEq(expectedSR, L1.getSyncedSnippet(queriedBlockId).stateRoot); + + // 2nd + queriedBlockId = 2; + expectedSR = bytes32(1e9 + uint256(queriedBlockId)); + assertEq(expectedSR, L1.getSyncedSnippet(queriedBlockId).stateRoot); + + // Not found -> reverts + vm.expectRevert(TaikoErrors.L1_BLOCK_MISMATCH.selector); + L1.getSyncedSnippet((count + 1)); } function test_L1_deposit_hash_creation() external {