Commit d3f3751e authored by clabby's avatar clabby

init L1 head verification

parent ac02d49e
...@@ -24,7 +24,7 @@ import { ResourceMetering } from "src/L1/ResourceMetering.sol"; ...@@ -24,7 +24,7 @@ import { ResourceMetering } from "src/L1/ResourceMetering.sol";
import { Constants } from "src/libraries/Constants.sol"; import { Constants } from "src/libraries/Constants.sol";
import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol"; import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol";
import { BlockHashOracle } from "src/dispute/BlockHashOracle.sol"; import { BlockOracle } from "src/dispute/BlockOracle.sol";
import { L1ERC721Bridge } from "src/L1/L1ERC721Bridge.sol"; import { L1ERC721Bridge } from "src/L1/L1ERC721Bridge.sol";
import { Predeploys } from "src/libraries/Predeploys.sol"; import { Predeploys } from "src/libraries/Predeploys.sol";
......
...@@ -4,27 +4,45 @@ pragma solidity 0.8.15; ...@@ -4,27 +4,45 @@ pragma solidity 0.8.15;
import "../libraries/DisputeTypes.sol"; import "../libraries/DisputeTypes.sol";
import "../libraries/DisputeErrors.sol"; import "../libraries/DisputeErrors.sol";
/// @title BlockHashOracle /// @title BlockOracle
/// @notice Stores a map of block numbers => block hashes for use in dispute resolution /// @notice Stores a map of block numbers => block hashes for use in dispute resolution
contract BlockHashOracle { contract BlockOracle {
/// @notice Maps block numbers to block hashes /// @notice The BlockInfo struct contains a block's hash and estimated timestamp.
mapping(uint256 => Hash) internal blockHashes; struct BlockInfo {
Hash hash;
Timestamp timestamp;
}
/// @notice Maps block numbers to block hashes and timestamps
mapping(uint256 => BlockInfo) internal blockHashes;
/// @notice Loads a block hash for a given block number, assuming that the block number /// @notice Loads a block hash for a given block number, assuming that the block number
/// has been stored in the oracle. /// has been stored in the oracle.
/// @param _blockNumber The block number to load the block hash for. /// @param _blockNumber The block number to load the block hash and timestamp for.
/// @return blockHash_ The block hash for the given block number. /// @return blockInfo_ The block hash and timestamp for the given block number.
function load(uint256 _blockNumber) external view returns (Hash blockHash_) { function load(uint256 _blockNumber) external view returns (BlockInfo memory blockInfo_) {
blockHash_ = blockHashes[_blockNumber]; blockInfo_ = blockHashes[_blockNumber];
if (Hash.unwrap(blockHash_) == 0) revert BlockHashNotPresent(); if (Hash.unwrap(blockInfo_.hash) == 0) revert BlockHashNotPresent();
} }
/// @notice Stores a block hash for a given block number, assuming that the block number /// @notice Stores a block hash for a given block number, assuming that the block number
/// is within the acceptable range of [tip - 256, tip]. /// is within the acceptable range of [tip - 256, tip].
/// @param _blockNumber The block number to persist the block hash for. /// @param _blockNumber The block number to persist the block hash for.
function store(uint256 _blockNumber) external { function store(uint256 _blockNumber) external {
// Fetch the block hash for the given block number and revert if it is out of
// the `BLOCKHASH` opcode's range.
bytes32 blockHash = blockhash(_blockNumber); bytes32 blockHash = blockhash(_blockNumber);
if (blockHash == 0) revert BlockNumberOOB(); if (blockHash == 0) revert BlockNumberOOB();
blockHashes[_blockNumber] = Hash.wrap(blockHash);
// Estimate the timestamp of the block assuming an average block time of 13 seconds.
Timestamp estimatedTimestamp = Timestamp.wrap(
uint64(block.timestamp - ((block.number - _blockNumber) * 13))
);
// Persist the block information.
blockHashes[_blockNumber] = BlockInfo({
hash: Hash.wrap(blockHash),
timestamp: estimatedTimestamp
});
} }
} }
...@@ -7,7 +7,7 @@ import { IInitializable } from "./interfaces/IInitializable.sol"; ...@@ -7,7 +7,7 @@ import { IInitializable } from "./interfaces/IInitializable.sol";
import { IBondManager } from "./interfaces/IBondManager.sol"; import { IBondManager } from "./interfaces/IBondManager.sol";
import { IBigStepper, IPreimageOracle } from "./interfaces/IBigStepper.sol"; import { IBigStepper, IPreimageOracle } from "./interfaces/IBigStepper.sol";
import { L2OutputOracle } from "../L1/L2OutputOracle.sol"; import { L2OutputOracle } from "../L1/L2OutputOracle.sol";
import { BlockHashOracle } from "./BlockHashOracle.sol"; import { BlockOracle } from "./BlockOracle.sol";
import { Clone } from "../libraries/Clone.sol"; import { Clone } from "../libraries/Clone.sol";
import { Types } from "../libraries/Types.sol"; import { Types } from "../libraries/Types.sol";
...@@ -43,8 +43,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -43,8 +43,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
L2OutputOracle public immutable L2_OUTPUT_ORACLE; L2OutputOracle public immutable L2_OUTPUT_ORACLE;
/// @notice The block hash oracle, used for loading block hashes further back /// @notice The block hash oracle, used for loading block hashes further back
/// than the `BLOCKHASH` opcode allows. /// than the `BLOCKHASH` opcode allows as well as their estimated timestamps.
BlockHashOracle public immutable BLOCK_HASH_ORACLE; BlockOracle public immutable BLOCK_ORACLE;
/// @notice The root claim's position is always at gindex 1. /// @notice The root claim's position is always at gindex 1.
Position internal constant ROOT_POSITION = Position.wrap(1); Position internal constant ROOT_POSITION = Position.wrap(1);
...@@ -64,6 +64,11 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -64,6 +64,11 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
/// @notice An append-only array of all claims made during the dispute game. /// @notice An append-only array of all claims made during the dispute game.
ClaimData[] public claimData; ClaimData[] public claimData;
/// @notice The starting and disputed output proposals, which are used to determine where
/// game participants should start executing Cannon and to verify the state transition,
/// respectively.
Types.OutputProposal[2] public proposals;
/// @notice An internal mapping to allow for constant-time lookups of existing claims. /// @notice An internal mapping to allow for constant-time lookups of existing claims.
mapping(ClaimHash => bool) internal claims; mapping(ClaimHash => bool) internal claims;
...@@ -73,6 +78,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -73,6 +78,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
/// @param _vm An onchain VM that performs single instruction steps on a fault proof program /// @param _vm An onchain VM that performs single instruction steps on a fault proof program
/// trace. /// trace.
/// @param _l2oo The trusted L2OutputOracle contract. /// @param _l2oo The trusted L2OutputOracle contract.
/// @param _blockOracle The block oracle, used for loading block hashes further back
/// than the `BLOCKHASH` opcode allows as well as their estimated
/// timestamps.
/// @custom:semver 0.0.5 /// @custom:semver 0.0.5
constructor( constructor(
Claim _absolutePrestate, Claim _absolutePrestate,
...@@ -80,14 +88,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -80,14 +88,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
Duration _gameDuration, Duration _gameDuration,
IBigStepper _vm, IBigStepper _vm,
L2OutputOracle _l2oo, L2OutputOracle _l2oo,
BlockHashOracle _blockHashOracle BlockOracle _blockOracle
) Semver(0, 0, 5) { ) Semver(0, 0, 5) {
ABSOLUTE_PRESTATE = _absolutePrestate; ABSOLUTE_PRESTATE = _absolutePrestate;
MAX_GAME_DEPTH = _maxGameDepth; MAX_GAME_DEPTH = _maxGameDepth;
GAME_DURATION = _gameDuration; GAME_DURATION = _gameDuration;
VM = _vm; VM = _vm;
L2_OUTPUT_ORACLE = _l2oo; L2_OUTPUT_ORACLE = _l2oo;
BLOCK_HASH_ORACLE = _blockHashOracle; BLOCK_ORACLE = _blockOracle;
} }
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
...@@ -422,6 +430,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -422,6 +430,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
/// @inheritdoc IInitializable /// @inheritdoc IInitializable
function initialize() external { function initialize() external {
// SAFETY: Any revert in this function will bubble up to the DisputeGameFactory and
// prevent the game from being created.
// Set the game start // Set the game start
gameStart = Timestamp.wrap(uint64(block.timestamp)); gameStart = Timestamp.wrap(uint64(block.timestamp));
// Set the game status // Set the game status
...@@ -438,11 +449,30 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver { ...@@ -438,11 +449,30 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
}) })
); );
// Set the L1 head hash to the block hash of the L1 block number provided. // Grab the index of the output proposal that commits to the starting L2 head.
// This call can revert if the block hash oracle does not have information // The output disputed is all outputs after this one.
// about the block number provided to it. This revert will bubble up to the // TODO(clabby): This is 2 calls too many for the information we need. Maybe
// DisputeGameFactory and prevent the game from being created. // add a function to the L2OO?
l1Head = BLOCK_HASH_ORACLE.load(_getArgUint256(0x40)); uint256 proposalIdx = L2_OUTPUT_ORACLE.getL2OutputIndexAfter(l2BlockNumber());
Types.OutputProposal memory starting = L2_OUTPUT_ORACLE.getL2Output(proposalIdx);
Types.OutputProposal memory disputed = L2_OUTPUT_ORACLE.getL2Output(proposalIdx + 1);
// SAFETY: This call can revert if the block hash oracle does not have information
// about the block number provided to it.
BlockOracle.BlockInfo memory blockInfo = BLOCK_ORACLE.load(l1BlockNumber());
// INVARIANT: The L1 head must contain the disputed output root. If it does not,
// the game cannot be played.
// TODO(clabby): The block timestamp in the oracle is an estimate that assumes a 13
// second block time. Should we add a buffer here to ensure that the
// estimation has room for error? This invariant cannot break.
if (Timestamp.unwrap(blockInfo.timestamp) < disputed.timestamp) revert L1HeadTooOld();
// Persist the output proposals fetched from the oracle.
// TODO(clabby): Docs on why we do this.
proposals[0] = starting;
proposals[1] = disputed;
// Persist the L1 head hash of the L1 block number provided.
l1Head = blockInfo.hash;
} }
/// @notice Returns the length of the `claimData` array. /// @notice Returns the length of the `claimData` array.
......
...@@ -16,7 +16,7 @@ error NoImplementation(GameType gameType); ...@@ -16,7 +16,7 @@ error NoImplementation(GameType gameType);
error GameAlreadyExists(Hash uuid); error GameAlreadyExists(Hash uuid);
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// `DisputeGame_Fault.sol` Errors // // `FaultDisputeGame.sol` Errors //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
/// @notice Thrown when a supplied bond is too low to cover the /// @notice Thrown when a supplied bond is too low to cover the
...@@ -54,6 +54,10 @@ error InvalidPrestate(); ...@@ -54,6 +54,10 @@ error InvalidPrestate();
/// @notice Thrown when a step is made that computes the expected post state correctly. /// @notice Thrown when a step is made that computes the expected post state correctly.
error ValidStep(); error ValidStep();
/// @notice Thrown when a game is attempted to be initialized with an L1 head that does
/// not contain the disputed output root.
error L1HeadTooOld();
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// `AttestationDisputeGame` Errors // // `AttestationDisputeGame` Errors //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
......
...@@ -2,16 +2,19 @@ ...@@ -2,16 +2,19 @@
pragma solidity 0.8.15; pragma solidity 0.8.15;
import { Test } from "forge-std/Test.sol"; import { Test } from "forge-std/Test.sol";
import { BlockHashOracle } from "src/dispute/BlockHashOracle.sol"; import { BlockOracle } from "src/dispute/BlockOracle.sol";
import "src/libraries/DisputeTypes.sol"; import "src/libraries/DisputeTypes.sol";
import "src/libraries/DisputeErrors.sol"; import "src/libraries/DisputeErrors.sol";
contract BlockHashOracle_Test is Test { contract BlockOracle_Test is Test {
BlockHashOracle oracle; BlockOracle oracle;
function setUp() public { function setUp() public {
oracle = new BlockHashOracle(); oracle = new BlockOracle();
// Roll the chain forward 255 blocks.
vm.roll(block.number + 255); vm.roll(block.number + 255);
// Set the time to a realistic date.
vm.warp(1690906994);
} }
/// @notice Tests that loading a block hash for a block number within the range of the /// @notice Tests that loading a block hash for a block number within the range of the
...@@ -19,7 +22,11 @@ contract BlockHashOracle_Test is Test { ...@@ -19,7 +22,11 @@ contract BlockHashOracle_Test is Test {
function testFuzz_store_succeeds(uint256 _blockNumber) public { function testFuzz_store_succeeds(uint256 _blockNumber) public {
_blockNumber = bound(_blockNumber, 0, 255); _blockNumber = bound(_blockNumber, 0, 255);
oracle.store(_blockNumber); oracle.store(_blockNumber);
assertEq(Hash.unwrap(oracle.load(_blockNumber)), blockhash(_blockNumber));
BlockOracle.BlockInfo memory res = oracle.load(_blockNumber);
assertEq(Hash.unwrap(res.hash), blockhash(_blockNumber));
emit log_uint(block.timestamp - ((block.number - _blockNumber) * 13));
assertEq(Timestamp.unwrap(res.timestamp), block.timestamp - ((block.number - _blockNumber) * 13));
} }
/// @notice Tests that loading a block hash for a block number outside the range of the /// @notice Tests that loading a block hash for a block number outside the range of the
......
...@@ -7,7 +7,7 @@ import { DisputeGameFactory_Init } from "./DisputeGameFactory.t.sol"; ...@@ -7,7 +7,7 @@ import { DisputeGameFactory_Init } from "./DisputeGameFactory.t.sol";
import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol"; import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.sol";
import { L2OutputOracle } from "src/L1/L2OutputOracle.sol"; import { L2OutputOracle } from "src/L1/L2OutputOracle.sol";
import { BlockHashOracle } from "src/dispute/BlockHashOracle.sol"; import { BlockOracle } from "src/dispute/BlockOracle.sol";
import "src/libraries/DisputeTypes.sol"; import "src/libraries/DisputeTypes.sol";
import "src/libraries/DisputeErrors.sol"; import "src/libraries/DisputeErrors.sol";
...@@ -35,8 +35,8 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init { ...@@ -35,8 +35,8 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init {
vm.roll(block.number + 1); vm.roll(block.number + 1);
// Deploy a new block hash oracle and store the block hash for the genesis block. // Deploy a new block hash oracle and store the block hash for the genesis block.
BlockHashOracle blockHashOracle = new BlockHashOracle(); BlockOracle blockOracle = new BlockOracle();
blockHashOracle.store(0); blockOracle.store(0);
// Deploy an implementation of the fault game // Deploy an implementation of the fault game
gameImpl = new FaultDisputeGame( gameImpl = new FaultDisputeGame(
...@@ -45,7 +45,7 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init { ...@@ -45,7 +45,7 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init {
Duration.wrap(7 days), Duration.wrap(7 days),
new AlphabetVM(absolutePrestate), new AlphabetVM(absolutePrestate),
L2OutputOracle(deployNoop()), L2OutputOracle(deployNoop()),
blockHashOracle blockOracle
); );
// Register the game implementation with the factory. // Register the game implementation with the factory.
factory.setImplementation(GAME_TYPE, gameImpl); factory.setImplementation(GAME_TYPE, gameImpl);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment