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 }