github.com/ethereum-optimism/optimism@v1.7.2/packages/contracts-bedrock/src/L1/DataAvailabilityChallenge.sol (about)

     1  // SPDX-License-Identifier: MIT
     2  pragma solidity 0.8.15;
     3  
     4  import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
     5  import { ISemver } from "src/universal/ISemver.sol";
     6  import { SafeCall } from "src/libraries/SafeCall.sol";
     7  
     8  /// @dev An enum representing the status of a DA challenge.
     9  enum ChallengeStatus {
    10      Uninitialized,
    11      Active,
    12      Resolved,
    13      Expired
    14  }
    15  
    16  /// @dev An enum representing known commitment types.
    17  enum CommitmentType {
    18      Keccak256
    19  }
    20  
    21  /// @dev A struct representing a single DA challenge.
    22  /// @custom:field status The status of the challenge.
    23  /// @custom:field challenger The address that initiated the challenge.
    24  /// @custom:field startBlock The block number at which the challenge was initiated.
    25  struct Challenge {
    26      address challenger;
    27      uint256 lockedBond;
    28      uint256 startBlock;
    29      uint256 resolvedBlock;
    30  }
    31  
    32  /// @title DataAvailabilityChallenge
    33  /// @notice This contract enables data availability of a data commitment at a given block number to be challenged.
    34  ///         To challenge a commitment, the challenger must first post a bond (bondSize).
    35  ///         Challenging a commitment is only possible within a certain block interval (challengeWindow) after the
    36  ///         commitment was made.
    37  ///         If the challenge is not resolved within a certain block interval (resolveWindow), the challenge can be
    38  ///         expired.
    39  ///         If a challenge is expired, the challenger's bond is unlocked and the challenged commitment is added to the
    40  ///         chain of expired challenges.
    41  contract DataAvailabilityChallenge is OwnableUpgradeable, ISemver {
    42      /// @notice Error for when the provided resolver refund percentage exceeds 100%.
    43      error InvalidResolverRefundPercentage(uint256 invalidResolverRefundPercentage);
    44  
    45      /// @notice Error for when the challenger's bond is too low.
    46      error BondTooLow(uint256 balance, uint256 required);
    47  
    48      /// @notice Error for when attempting to challenge a commitment that already has a challenge.
    49      error ChallengeExists();
    50  
    51      /// @notice Error for when attempting to resolve a challenge that is not active.
    52      error ChallengeNotActive();
    53  
    54      /// @notice Error for when attempting to unlock a bond from a challenge that is not expired.
    55      error ChallengeNotExpired();
    56  
    57      /// @notice Error for when attempting to challenge a commitment that is not in the challenge window.
    58      error ChallengeWindowNotOpen();
    59  
    60      /// @notice Error for when the provided input data doesn't match the commitment.
    61      error InvalidInputData(bytes providedDataCommitment, bytes expectedCommitment);
    62  
    63      /// @notice Error for when the call to withdraw a bond failed.
    64      error WithdrawalFailed();
    65  
    66      /// @notice Error for when a the type of a given commitment is unknown
    67      error UnknownCommitmentType(uint8 commitmentType);
    68  
    69      /// @notice Error for when the commitment length does not match the commitment type
    70      error InvalidCommitmentLength(uint8 commitmentType, uint256 expectedLength, uint256 actualLength);
    71  
    72      /// @notice An event that is emitted when the status of a challenge changes.
    73      /// @param challengedCommitment The commitment that is being challenged.
    74      /// @param challengedBlockNumber The block number at which the commitment was made.
    75      /// @param status The new status of the challenge.
    76      event ChallengeStatusChanged(
    77          uint256 indexed challengedBlockNumber, bytes challengedCommitment, ChallengeStatus status
    78      );
    79  
    80      /// @notice An event that is emitted when the bond size required to initiate a challenge changes.
    81      event RequiredBondSizeChanged(uint256 challengeWindow);
    82  
    83      /// @notice An event that is emitted when the percentage of the resolving cost to be refunded to the resolver
    84      /// changes.
    85      event ResolverRefundPercentageChanged(uint256 resolverRefundPercentage);
    86  
    87      /// @notice An event that is emitted when a user's bond balance changes.
    88      event BalanceChanged(address account, uint256 balance);
    89  
    90      /// @notice Semantic version.
    91      /// @custom:semver 1.0.0
    92      string public constant version = "1.0.0";
    93  
    94      /// @notice The fixed cost of resolving a challenge.
    95      /// @dev The value is estimated by measuring the cost of resolving with `bytes(0)`
    96      uint256 public constant fixedResolutionCost = 72925;
    97  
    98      /// @notice The variable cost of resolving a callenge per byte scaled by the variableResolutionCostPrecision.
    99      /// @dev upper limit; The value is estimated by measuring the cost of resolving with variable size data where each
   100      /// byte is non-zero.
   101      uint256 public constant variableResolutionCost = 16640;
   102  
   103      /// @dev The precision of the variable resolution cost.
   104      uint256 public constant variableResolutionCostPrecision = 1000;
   105  
   106      /// @notice The block interval during which a commitment can be challenged.
   107      uint256 public challengeWindow;
   108  
   109      /// @notice The block interval during which a challenge can be resolved.
   110      uint256 public resolveWindow;
   111  
   112      /// @notice The amount required to post a challenge.
   113      uint256 public bondSize;
   114  
   115      /// @notice The percentage of the resolving cost to be refunded to the resolver.
   116      /// @dev There are no decimals, ie a value of 50 corresponds to 50%.
   117      uint256 public resolverRefundPercentage;
   118  
   119      /// @notice A mapping from addresses to their bond balance in the contract.
   120      mapping(address => uint256) public balances;
   121  
   122      /// @notice A mapping from challenged block numbers to challenged commitments to challenges.
   123      mapping(uint256 => mapping(bytes => Challenge)) internal challenges;
   124  
   125      /// @notice Constructs the DataAvailabilityChallenge contract. Cannot set
   126      ///         the owner to `address(0)` due to the Ownable contract's
   127      ///         implementation, so set it to `address(0xdEaD)`.
   128      constructor() OwnableUpgradeable() {
   129          initialize({
   130              _owner: address(0xdEaD),
   131              _challengeWindow: 0,
   132              _resolveWindow: 0,
   133              _bondSize: 0,
   134              _resolverRefundPercentage: 0
   135          });
   136      }
   137  
   138      /// @notice Initializes the contract.
   139      /// @param _owner The owner of the contract.
   140      /// @param _challengeWindow The block interval during which a commitment can be challenged.
   141      /// @param _resolveWindow The block interval during which a challenge can be resolved.
   142      /// @param _bondSize The amount required to post a challenge.
   143      function initialize(
   144          address _owner,
   145          uint256 _challengeWindow,
   146          uint256 _resolveWindow,
   147          uint256 _bondSize,
   148          uint256 _resolverRefundPercentage
   149      )
   150          public
   151          initializer
   152      {
   153          __Ownable_init();
   154          challengeWindow = _challengeWindow;
   155          resolveWindow = _resolveWindow;
   156          setBondSize(_bondSize);
   157          setResolverRefundPercentage(_resolverRefundPercentage);
   158          _transferOwnership(_owner);
   159      }
   160  
   161      /// @notice Sets the bond size.
   162      /// @param _bondSize The amount required to post a challenge.
   163      function setBondSize(uint256 _bondSize) public onlyOwner {
   164          bondSize = _bondSize;
   165          emit RequiredBondSizeChanged(_bondSize);
   166      }
   167  
   168      /// @notice Sets the percentage of the resolving cost to be refunded to the resolver.
   169      /// @dev The function reverts if the provided percentage is above 100, since the refund logic
   170      /// assumes a value smaller or equal to 100%.
   171      /// @param _resolverRefundPercentage The percentage of the resolving cost to be refunded to the resolver.
   172      function setResolverRefundPercentage(uint256 _resolverRefundPercentage) public onlyOwner {
   173          if (_resolverRefundPercentage > 100) {
   174              revert InvalidResolverRefundPercentage(_resolverRefundPercentage);
   175          }
   176          resolverRefundPercentage = _resolverRefundPercentage;
   177      }
   178  
   179      /// @notice Post a bond as prerequisite for challenging a commitment.
   180      receive() external payable {
   181          deposit();
   182      }
   183  
   184      /// @notice Post a bond as prerequisite for challenging a commitment.
   185      function deposit() public payable {
   186          balances[msg.sender] += msg.value;
   187          emit BalanceChanged(msg.sender, balances[msg.sender]);
   188      }
   189  
   190      /// @notice Withdraw a user's unlocked bond.
   191      function withdraw() external {
   192          // get caller's balance
   193          uint256 balance = balances[msg.sender];
   194  
   195          // set caller's balance to 0
   196          balances[msg.sender] = 0;
   197          emit BalanceChanged(msg.sender, 0);
   198  
   199          // send caller's balance to caller
   200          bool success = SafeCall.send(msg.sender, gasleft(), balance);
   201          if (!success) {
   202              revert WithdrawalFailed();
   203          }
   204      }
   205  
   206      /// @notice Checks if the current block is within the challenge window for a given challenged block number.
   207      /// @param challengedBlockNumber The block number at which the commitment was made.
   208      /// @return True if the current block is within the challenge window, false otherwise.
   209      function _isInChallengeWindow(uint256 challengedBlockNumber) internal view returns (bool) {
   210          return (block.number >= challengedBlockNumber && block.number <= challengedBlockNumber + challengeWindow);
   211      }
   212  
   213      /// @notice Checks if the current block is within the resolve window for a given challenge start block number.
   214      /// @param challengeStartBlockNumber The block number at which the challenge was initiated.
   215      /// @return True if the current block is within the resolve window, false otherwise.
   216      function _isInResolveWindow(uint256 challengeStartBlockNumber) internal view returns (bool) {
   217          return block.number <= challengeStartBlockNumber + resolveWindow;
   218      }
   219  
   220      /// @notice Returns a challenge for the given block number and commitment.
   221      /// @dev Unlike with a public `challenges` mapping, we can return a Challenge struct instead of tuple.
   222      /// @param challengedBlockNumber The block number at which the commitment was made.
   223      /// @param challengedCommitment The commitment that is being challenged.
   224      /// @return The challenge struct.
   225      function getChallenge(
   226          uint256 challengedBlockNumber,
   227          bytes calldata challengedCommitment
   228      )
   229          public
   230          view
   231          returns (Challenge memory)
   232      {
   233          return challenges[challengedBlockNumber][challengedCommitment];
   234      }
   235  
   236      /// @notice Returns the status of a challenge for a given challenged block number and challenged commitment.
   237      /// @param challengedBlockNumber The block number at which the commitment was made.
   238      /// @param challengedCommitment The commitment that is being challenged.
   239      /// @return The status of the challenge.
   240      function getChallengeStatus(
   241          uint256 challengedBlockNumber,
   242          bytes calldata challengedCommitment
   243      )
   244          public
   245          view
   246          returns (ChallengeStatus)
   247      {
   248          Challenge memory _challenge = challenges[challengedBlockNumber][challengedCommitment];
   249          // if the address is 0, the challenge is uninitialized
   250          if (_challenge.challenger == address(0)) return ChallengeStatus.Uninitialized;
   251  
   252          // if the challenge has a resolved block, it is resolved
   253          if (_challenge.resolvedBlock != 0) return ChallengeStatus.Resolved;
   254  
   255          // if the challenge's start block is in the resolve window, it is active
   256          if (_isInResolveWindow(_challenge.startBlock)) return ChallengeStatus.Active;
   257  
   258          // if the challenge's start block is not in the resolve window, it is expired
   259          return ChallengeStatus.Expired;
   260      }
   261  
   262      /// @notice Extract the commitment type from a given commitment.
   263      /// @dev The commitment type is located in the first byte of the commitment.
   264      /// @param commitment The commitment from which to extract the commitment type.
   265      /// @return The commitment type of the given commitment.
   266      function _getCommitmentType(bytes calldata commitment) internal pure returns (uint8) {
   267          return uint8(bytes1(commitment));
   268      }
   269  
   270      /// @notice Validate that a given commitment has a known type and the expected length for this type.
   271      /// @dev The type of a commitment is stored in its first byte.
   272      ///      The function reverts with `UnknownCommitmentType` if the type is not known and
   273      ///      with `InvalidCommitmentLength` if the commitment has an unexpected length.
   274      /// @param commitment The commitment for which to check the type.
   275      function validateCommitment(bytes calldata commitment) public pure {
   276          uint8 commitmentType = _getCommitmentType(commitment);
   277          if (commitmentType == uint8(CommitmentType.Keccak256)) {
   278              if (commitment.length != 33) {
   279                  revert InvalidCommitmentLength(uint8(CommitmentType.Keccak256), 33, commitment.length);
   280              }
   281              return;
   282          }
   283  
   284          revert UnknownCommitmentType(commitmentType);
   285      }
   286  
   287      /// @notice Challenge a commitment at a given block number.
   288      /// @dev The block number parameter is necessary for the contract to verify the challenge window,
   289      ///      since the contract cannot access the block number of the commitment.
   290      ///      The function reverts if the commitment type (first byte) is unknown,
   291      ///      if the caller does not have a bond or if the challenge already exists.
   292      /// @param challengedBlockNumber The block number at which the commitment was made.
   293      /// @param challengedCommitment The commitment that is being challenged.
   294      function challenge(uint256 challengedBlockNumber, bytes calldata challengedCommitment) external payable {
   295          // require the commitment type to be known
   296          validateCommitment(challengedCommitment);
   297  
   298          // deposit value sent with the transaction as bond
   299          deposit();
   300  
   301          // require the caller to have a bond
   302          if (balances[msg.sender] < bondSize) {
   303              revert BondTooLow(balances[msg.sender], bondSize);
   304          }
   305  
   306          // require the challenge status to be uninitialized
   307          if (getChallengeStatus(challengedBlockNumber, challengedCommitment) != ChallengeStatus.Uninitialized) {
   308              revert ChallengeExists();
   309          }
   310  
   311          // require the current block to be in the challenge window
   312          if (!_isInChallengeWindow(challengedBlockNumber)) {
   313              revert ChallengeWindowNotOpen();
   314          }
   315  
   316          // reduce the caller's balance
   317          balances[msg.sender] -= bondSize;
   318  
   319          // store the challenger's address, bond size, and start block of the challenge
   320          challenges[challengedBlockNumber][challengedCommitment] =
   321              Challenge({ challenger: msg.sender, lockedBond: bondSize, startBlock: block.number, resolvedBlock: 0 });
   322  
   323          // emit an event to notify that the challenge status is now active
   324          emit ChallengeStatusChanged(challengedBlockNumber, challengedCommitment, ChallengeStatus.Active);
   325      }
   326  
   327      /// @notice Resolve a challenge by providing the data corresponding to the challenged commitment.
   328      /// @dev The function computes a commitment from the provided resolveData and verifies that it matches the
   329      /// challenged commitment.
   330      ///      It reverts if the commitment type is unknown, if the data doesn't match the commitment,
   331      ///      if the challenge is not active or if the resolve window is not open.
   332      /// @param challengedBlockNumber The block number at which the commitment was made.
   333      /// @param challengedCommitment The challenged commitment that is being resolved.
   334      /// @param resolveData The pre-image data corresponding to the challenged commitment.
   335      function resolve(
   336          uint256 challengedBlockNumber,
   337          bytes calldata challengedCommitment,
   338          bytes calldata resolveData
   339      )
   340          external
   341      {
   342          // require the commitment type to be known
   343          validateCommitment(challengedCommitment);
   344  
   345          // require the challenge to be active (started, not resolved, and resolve window still open)
   346          if (getChallengeStatus(challengedBlockNumber, challengedCommitment) != ChallengeStatus.Active) {
   347              revert ChallengeNotActive();
   348          }
   349  
   350          // compute the commitment corresponding to the given resolveData
   351          uint8 commitmentType = _getCommitmentType(challengedCommitment);
   352          bytes memory computedCommitment;
   353          if (commitmentType == uint8(CommitmentType.Keccak256)) {
   354              computedCommitment = computeCommitmentKeccak256(resolveData);
   355          }
   356  
   357          // require the provided input data to correspond to the challenged commitment
   358          if (keccak256(computedCommitment) != keccak256(challengedCommitment)) {
   359              revert InvalidInputData(computedCommitment, challengedCommitment);
   360          }
   361  
   362          // store the block number at which the challenge was resolved
   363          Challenge storage activeChallenge = challenges[challengedBlockNumber][challengedCommitment];
   364          activeChallenge.resolvedBlock = block.number;
   365  
   366          // emit an event to notify that the challenge status is now resolved
   367          emit ChallengeStatusChanged(challengedBlockNumber, challengedCommitment, ChallengeStatus.Resolved);
   368  
   369          // distribute the bond among challenger, resolver and address(0)
   370          _distributeBond(activeChallenge, resolveData.length, msg.sender);
   371      }
   372  
   373      /// @notice Distribute the bond of a resolved challenge among the resolver, challenger and address(0).
   374      ///         The challenger is refunded the bond amount exceeding the resolution cost.
   375      ///         The resolver is refunded a percentage of the resolution cost based on the `resolverRefundPercentage`
   376      ///         state variable.
   377      ///         The remaining bond is burned by sending it to address(0).
   378      /// @dev The resolution cost is approximated based on a fixed cost and variable cost depending on the size of the
   379      ///      pre-image.
   380      ///      The real resolution cost might vary, because calldata is priced differently for zero and non-zero bytes.
   381      ///      Computing the exact cost adds too much gas overhead to be worth the tradeoff.
   382      /// @param resolvedChallenge The resolved challenge in storage.
   383      /// @param preImageLength The size of the pre-image used to resolve the challenge.
   384      /// @param resolver The address of the resolver.
   385      function _distributeBond(Challenge storage resolvedChallenge, uint256 preImageLength, address resolver) internal {
   386          uint256 lockedBond = resolvedChallenge.lockedBond;
   387          address challenger = resolvedChallenge.challenger;
   388  
   389          // approximate the cost of resolving a challenge with the provided pre-image size
   390          uint256 resolutionCost = (
   391              fixedResolutionCost + preImageLength * variableResolutionCost / variableResolutionCostPrecision
   392          ) * block.basefee;
   393  
   394          // refund bond exceeding the resolution cost to the challenger
   395          if (lockedBond > resolutionCost) {
   396              balances[challenger] += lockedBond - resolutionCost;
   397              lockedBond = resolutionCost;
   398              emit BalanceChanged(challenger, balances[challenger]);
   399          }
   400  
   401          // refund a percentage of the resolution cost to the resolver (but not more than the locked bond)
   402          uint256 resolverRefund = resolutionCost * resolverRefundPercentage / 100;
   403          if (resolverRefund > lockedBond) {
   404              resolverRefund = lockedBond;
   405          }
   406          if (resolverRefund > 0) {
   407              balances[resolver] += resolverRefund;
   408              lockedBond -= resolverRefund;
   409              emit BalanceChanged(resolver, balances[resolver]);
   410          }
   411  
   412          // burn the remaining bond
   413          if (lockedBond > 0) {
   414              payable(address(0)).transfer(lockedBond);
   415          }
   416          resolvedChallenge.lockedBond = 0;
   417      }
   418  
   419      /// @notice Unlock the bond associated wth an expired challenge.
   420      /// @dev The function reverts if the challenge is not expired.
   421      ///      If the expiration is successful, the challenger's bond is unlocked.
   422      /// @param challengedBlockNumber The block number at which the commitment was made.
   423      /// @param challengedCommitment The commitment that is being challenged.
   424      function unlockBond(uint256 challengedBlockNumber, bytes calldata challengedCommitment) external {
   425          // require the challenge to be active (started, not resolved, and in the resolve window)
   426          if (getChallengeStatus(challengedBlockNumber, challengedCommitment) != ChallengeStatus.Expired) {
   427              revert ChallengeNotExpired();
   428          }
   429  
   430          // Unlock the bond associated with the challenge
   431          Challenge storage expiredChallenge = challenges[challengedBlockNumber][challengedCommitment];
   432          balances[expiredChallenge.challenger] += expiredChallenge.lockedBond;
   433          expiredChallenge.lockedBond = 0;
   434  
   435          // Emit balance update event
   436          emit BalanceChanged(expiredChallenge.challenger, balances[expiredChallenge.challenger]);
   437      }
   438  }
   439  
   440  /// @notice Compute the expected commitment for a given blob of data.
   441  /// @param data The blob of data to compute a commitment for.
   442  /// @return The commitment for the given blob of data.
   443  function computeCommitmentKeccak256(bytes memory data) pure returns (bytes memory) {
   444      return bytes.concat(bytes1(uint8(CommitmentType.Keccak256)), keccak256(data));
   445  }