diff --git a/packages/protocol/contracts/layer2/core/Anchor.sol b/packages/protocol/contracts/layer2/core/Anchor.sol index 1f1b1cc6e6..606d3aa7ea 100644 --- a/packages/protocol/contracts/layer2/core/Anchor.sol +++ b/packages/protocol/contracts/layer2/core/Anchor.sol @@ -47,10 +47,14 @@ contract Anchor is EssentialContract { } /// @notice Metadata that will be required for slashing violations of permissionless preconfs. + /// @dev `stored` marks an existing entry so callers can distinguish a recorded block whose + /// `anchorBlockNumber` is 0 (a valid "skip checkpoint" value) from a block that was never + /// recorded. It packs into the same slot as the preceding `uint48` fields. struct PreconfMetadata { uint48 anchorBlockNumber; uint48 submissionWindowEnd; uint48 parentSubmissionWindowEnd; + bool stored; bytes32 rawTxListHash; bytes32 parentRawTxListHash; } @@ -205,7 +209,7 @@ contract Anchor is EssentialContract { returns (PreconfMetadata memory) { PreconfMetadata memory preconfMetadata = _preconfMetadata[_blockNumber]; - require(preconfMetadata.anchorBlockNumber != 0, InvalidBlockNumber()); + require(preconfMetadata.stored, InvalidBlockNumber()); return preconfMetadata; } @@ -251,6 +255,7 @@ contract Anchor is EssentialContract { anchorBlockNumber: _blockParams.anchorBlockNumber, submissionWindowEnd: _proposalParams.submissionWindowEnd, parentSubmissionWindowEnd: parentPreconfMetadata.submissionWindowEnd, + stored: true, rawTxListHash: _blockParams.rawTxListHash, parentRawTxListHash: parentPreconfMetadata.rawTxListHash }); diff --git a/packages/protocol/test/layer2/core/Anchor.t.sol b/packages/protocol/test/layer2/core/Anchor.t.sol index f698201e83..bd250c9df0 100644 --- a/packages/protocol/test/layer2/core/Anchor.t.sol +++ b/packages/protocol/test/layer2/core/Anchor.t.sol @@ -105,6 +105,30 @@ contract AnchorTest is Test { assertEq(checkpointStore.getCheckpoint(staleBlockParams.anchorBlockNumber).blockNumber, 0); } + function test_getPreconfMetadata_recordsBlockAnchoredAtZero() external { + // A block may legitimately be anchored with `anchorBlockNumber == 0` ("skip checkpoint"). + // Its preconf metadata must still be retrievable so the slasher can validate faults; the + // `stored` marker distinguishes it from a block that was never recorded. + Anchor.ProposalParams memory proposalParams = + Anchor.ProposalParams({ submissionWindowEnd: 7 }); + Anchor.BlockParams memory blockParams = _blockParams(0, 0x1234, 0x5678); + + vm.roll(SHASTA_FORK_HEIGHT); + vm.prank(GOLDEN_TOUCH); + anchor.anchorV4(proposalParams, blockParams); + + Anchor.PreconfMetadata memory meta = anchor.getPreconfMetadata(block.number); + assertTrue(meta.stored); + assertEq(meta.anchorBlockNumber, 0); + assertEq(meta.submissionWindowEnd, 7); + } + + function test_getPreconfMetadata_RevertWhen_BlockNotRecorded() external { + vm.roll(SHASTA_FORK_HEIGHT); + vm.expectRevert(Anchor.InvalidBlockNumber.selector); + anchor.getPreconfMetadata(SHASTA_FORK_HEIGHT + 999); + } + // --------------------------------------------------------------- // Helpers // ---------------------------------------------------------------