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";
import { Constants } from "src/libraries/Constants.sol";
import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.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 { Predeploys } from "src/libraries/Predeploys.sol";
......
......@@ -4,27 +4,45 @@ pragma solidity 0.8.15;
import "../libraries/DisputeTypes.sol";
import "../libraries/DisputeErrors.sol";
/// @title BlockHashOracle
/// @title BlockOracle
/// @notice Stores a map of block numbers => block hashes for use in dispute resolution
contract BlockHashOracle {
/// @notice Maps block numbers to block hashes
mapping(uint256 => Hash) internal blockHashes;
contract BlockOracle {
/// @notice The BlockInfo struct contains a block's hash and estimated timestamp.
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
/// has been stored in the oracle.
/// @param _blockNumber The block number to load the block hash for.
/// @return blockHash_ The block hash for the given block number.
function load(uint256 _blockNumber) external view returns (Hash blockHash_) {
blockHash_ = blockHashes[_blockNumber];
if (Hash.unwrap(blockHash_) == 0) revert BlockHashNotPresent();
/// @param _blockNumber The block number to load the block hash and timestamp for.
/// @return blockInfo_ The block hash and timestamp for the given block number.
function load(uint256 _blockNumber) external view returns (BlockInfo memory blockInfo_) {
blockInfo_ = blockHashes[_blockNumber];
if (Hash.unwrap(blockInfo_.hash) == 0) revert BlockHashNotPresent();
}
/// @notice Stores a block hash for a given block number, assuming that the block number
/// is within the acceptable range of [tip - 256, tip].
/// @param _blockNumber The block number to persist the block hash for.
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);
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";
import { IBondManager } from "./interfaces/IBondManager.sol";
import { IBigStepper, IPreimageOracle } from "./interfaces/IBigStepper.sol";
import { L2OutputOracle } from "../L1/L2OutputOracle.sol";
import { BlockHashOracle } from "./BlockHashOracle.sol";
import { BlockOracle } from "./BlockOracle.sol";
import { Clone } from "../libraries/Clone.sol";
import { Types } from "../libraries/Types.sol";
......@@ -43,8 +43,8 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
L2OutputOracle public immutable L2_OUTPUT_ORACLE;
/// @notice The block hash oracle, used for loading block hashes further back
/// than the `BLOCKHASH` opcode allows.
BlockHashOracle public immutable BLOCK_HASH_ORACLE;
/// than the `BLOCKHASH` opcode allows as well as their estimated timestamps.
BlockOracle public immutable BLOCK_ORACLE;
/// @notice The root claim's position is always at gindex 1.
Position internal constant ROOT_POSITION = Position.wrap(1);
......@@ -64,6 +64,11 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
/// @notice An append-only array of all claims made during the dispute game.
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.
mapping(ClaimHash => bool) internal claims;
......@@ -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
/// trace.
/// @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
constructor(
Claim _absolutePrestate,
......@@ -80,14 +88,14 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
Duration _gameDuration,
IBigStepper _vm,
L2OutputOracle _l2oo,
BlockHashOracle _blockHashOracle
BlockOracle _blockOracle
) Semver(0, 0, 5) {
ABSOLUTE_PRESTATE = _absolutePrestate;
MAX_GAME_DEPTH = _maxGameDepth;
GAME_DURATION = _gameDuration;
VM = _vm;
L2_OUTPUT_ORACLE = _l2oo;
BLOCK_HASH_ORACLE = _blockHashOracle;
BLOCK_ORACLE = _blockOracle;
}
////////////////////////////////////////////////////////////////
......@@ -422,6 +430,9 @@ contract FaultDisputeGame is IFaultDisputeGame, Clone, Semver {
/// @inheritdoc IInitializable
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
gameStart = Timestamp.wrap(uint64(block.timestamp));
// Set the game status
......@@ -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.
// This call can revert if the block hash oracle does not have information
// about the block number provided to it. This revert will bubble up to the
// DisputeGameFactory and prevent the game from being created.
l1Head = BLOCK_HASH_ORACLE.load(_getArgUint256(0x40));
// Grab the index of the output proposal that commits to the starting L2 head.
// The output disputed is all outputs after this one.
// TODO(clabby): This is 2 calls too many for the information we need. Maybe
// add a function to the L2OO?
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.
......
......@@ -16,7 +16,7 @@ error NoImplementation(GameType gameType);
error GameAlreadyExists(Hash uuid);
////////////////////////////////////////////////////////////////
// `DisputeGame_Fault.sol` Errors //
// `FaultDisputeGame.sol` Errors //
////////////////////////////////////////////////////////////////
/// @notice Thrown when a supplied bond is too low to cover the
......@@ -54,6 +54,10 @@ error InvalidPrestate();
/// @notice Thrown when a step is made that computes the expected post state correctly.
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 //
////////////////////////////////////////////////////////////////
......
......@@ -2,16 +2,19 @@
pragma solidity 0.8.15;
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/DisputeErrors.sol";
contract BlockHashOracle_Test is Test {
BlockHashOracle oracle;
contract BlockOracle_Test is Test {
BlockOracle oracle;
function setUp() public {
oracle = new BlockHashOracle();
oracle = new BlockOracle();
// Roll the chain forward 255 blocks.
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
......@@ -19,7 +22,11 @@ contract BlockHashOracle_Test is Test {
function testFuzz_store_succeeds(uint256 _blockNumber) public {
_blockNumber = bound(_blockNumber, 0, 255);
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
......
......@@ -7,7 +7,7 @@ import { DisputeGameFactory_Init } from "./DisputeGameFactory.t.sol";
import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol";
import { FaultDisputeGame } from "src/dispute/FaultDisputeGame.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/DisputeErrors.sol";
......@@ -35,8 +35,8 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init {
vm.roll(block.number + 1);
// Deploy a new block hash oracle and store the block hash for the genesis block.
BlockHashOracle blockHashOracle = new BlockHashOracle();
blockHashOracle.store(0);
BlockOracle blockOracle = new BlockOracle();
blockOracle.store(0);
// Deploy an implementation of the fault game
gameImpl = new FaultDisputeGame(
......@@ -45,7 +45,7 @@ contract FaultDisputeGame_Init is DisputeGameFactory_Init {
Duration.wrap(7 days),
new AlphabetVM(absolutePrestate),
L2OutputOracle(deployNoop()),
blockHashOracle
blockOracle
);
// Register the game implementation with the factory.
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