diff --git a/onchain/rollups/.changeset/nervous-hairs-flow.md b/onchain/rollups/.changeset/nervous-hairs-flow.md new file mode 100644 index 00000000..fd2069a7 --- /dev/null +++ b/onchain/rollups/.changeset/nervous-hairs-flow.md @@ -0,0 +1,6 @@ +--- +"@cartesi/rollups": major +--- + +Removed `History`. +This contract is no longer be necessary for the new `Authority` contract. diff --git a/onchain/rollups/contracts/history/History.sol b/onchain/rollups/contracts/history/History.sol deleted file mode 100644 index ce02247e..00000000 --- a/onchain/rollups/contracts/history/History.sol +++ /dev/null @@ -1,157 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -pragma solidity ^0.8.8; - -import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; - -import {IHistory} from "./IHistory.sol"; - -/// @title Simple History -/// -/// @notice This contract stores claims for each DApp individually. -/// This means that, for each DApp, the contract stores an array of -/// `Claim` entries, where each `Claim` is composed of: -/// -/// * An epoch hash (`bytes32`) -/// * A closed interval of input indices (`uint128`, `uint128`) -/// -/// The contract guarantees that the first interval starts at index 0, -/// and that the following intervals don't have gaps or overlaps. -/// -/// Furthermore, claims can only be submitted by the contract owner -/// through `submitClaim`, but can be retrieved by anyone with `getClaim`. -/// -/// @dev This contract inherits OpenZeppelin's `Ownable` contract. -/// For more information on `Ownable`, please consult OpenZeppelin's official documentation. -contract History is IHistory, Ownable { - struct Claim { - bytes32 epochHash; - uint128 firstIndex; - uint128 lastIndex; - } - - /// @notice Mapping from DApp address to number of claims. - mapping(address => uint256) internal numClaims; - - /// @notice Mapping from DApp address and claim index to claim. - /// @dev See the `getClaim` and `submitClaim` functions. - mapping(address => mapping(uint256 => Claim)) internal claims; - - /// @notice A new claim regarding a specific DApp was submitted. - /// @param dapp The address of the DApp - /// @param claim The newly-submitted claim - /// @dev MUST be triggered on a successful call to `submitClaim`. - event NewClaimToHistory(address indexed dapp, Claim claim); - - /// @notice Raised when one tries to submit a claim whose first input index - /// is not less than or equal to its last input index. - error InvalidInputIndices(); - - /// @notice Raised when one tries to submit a claim that skips some input. - /// For example, when the 1st claim starts at index 5 (instead of 0) - /// or when the 1st claim ends at index 20 but the 2nd claim starts at - /// index 22 (instead of 21). - error UnclaimedInputs(); - - /// @notice Raised when one tries to retrieve a claim with an invalid index. - error InvalidClaimIndex(); - - /// @notice Creates a `History` contract. - /// @param _owner The initial owner - constructor(address _owner) { - // constructor in Ownable already called `transferOwnership(msg.sender)`, so - // we only need to call `transferOwnership(_owner)` if _owner != msg.sender - if (_owner != msg.sender) { - transferOwnership(_owner); - } - } - - /// @notice Submit a claim regarding a DApp. - /// There are several requirements for this function to be called successfully. - /// - /// * `_claimData` MUST be well-encoded. In Solidity, it can be constructed - /// as `abi.encode(dapp, claim)`, where `dapp` is the DApp address (type `address`) - /// and `claim` is the claim structure (type `Claim`). - /// - /// * `firstIndex` MUST be less than or equal to `lastIndex`. - /// As a result, every claim MUST encompass AT LEAST one input. - /// - /// * If this is the DApp's first claim, then `firstIndex` MUST be `0`. - /// Otherwise, `firstIndex` MUST be the `lastClaim.lastIndex + 1`. - /// In other words, claims MUST NOT skip inputs. - /// - /// @inheritdoc IHistory - /// @dev Emits a `NewClaimToHistory` event. Should have access control. - /// Incorrect claim input indices could raise two errors: - /// `InvalidInputIndices` if first index is posterior than last index or - /// `UnclaimedInputs` if first index is not the subsequent of previous claimed index or - /// if the first index of the first claim is not zero. - function submitClaim( - bytes calldata _claimData - ) external override onlyOwner { - (address dapp, Claim memory claim) = abi.decode( - _claimData, - (address, Claim) - ); - - if (claim.firstIndex > claim.lastIndex) { - revert InvalidInputIndices(); - } - - uint256 numDAppClaims = numClaims[dapp]; - - if ( - claim.firstIndex != - ( - (numDAppClaims == 0) - ? 0 - : (claims[dapp][numDAppClaims - 1].lastIndex + 1) - ) - ) { - revert UnclaimedInputs(); - } - - claims[dapp][numDAppClaims] = claim; - numClaims[dapp] = numDAppClaims + 1; - - emit NewClaimToHistory(dapp, claim); - } - - /// @notice Get a specific claim regarding a specific DApp. - /// There are several requirements for this function to be called successfully. - /// - /// * `_proofContext` MUST be well-encoded. In Solidity, it can be constructed - /// as `abi.encode(claimIndex)`, where `claimIndex` is the claim index (type `uint256`). - /// - /// * `claimIndex` MUST be inside the interval `[0, n)` where `n` is the number of claims - /// that have been submitted to `_dapp` already. - /// - /// @inheritdoc IHistory - /// @dev If `claimIndex` is not inside the interval `[0, n)`, then - /// an `InvalidClaimIndex` error is raised. - function getClaim( - address _dapp, - bytes calldata _proofContext - ) external view override returns (bytes32, uint256, uint256) { - uint256 claimIndex = abi.decode(_proofContext, (uint256)); - - uint256 numDAppClaims = numClaims[_dapp]; - - if (claimIndex >= numDAppClaims) { - revert InvalidClaimIndex(); - } - - Claim memory claim = claims[_dapp][claimIndex]; - - return (claim.epochHash, claim.firstIndex, claim.lastIndex); - } - - /// @inheritdoc IHistory - /// @dev Emits an `OwnershipTransferred` event. Should have access control. - function migrateToConsensus( - address _consensus - ) external override onlyOwner { - transferOwnership(_consensus); - } -} diff --git a/onchain/rollups/test/foundry/consensus/authority/Authority.t.sol b/onchain/rollups/test/foundry/consensus/authority/Authority.t.sol index a182e3a3..e15bed7b 100644 --- a/onchain/rollups/test/foundry/consensus/authority/Authority.t.sol +++ b/onchain/rollups/test/foundry/consensus/authority/Authority.t.sol @@ -8,7 +8,6 @@ import {Test} from "forge-std/Test.sol"; import {TestBase} from "../../util/TestBase.sol"; import {Authority} from "contracts/consensus/authority/Authority.sol"; import {IHistory} from "contracts/history/IHistory.sol"; -import {History} from "contracts/history/History.sol"; import {Vm} from "forge-std/Vm.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -394,243 +393,3 @@ contract AuthorityTest is TestBase { authority.join(); } } - -contract AuthorityHandler is Test { - struct Claim { - bytes32 epochHash; - uint128 firstIndex; - uint128 lastIndex; - } - struct ClaimContext { - address dapp; - Claim claim; - uint256 proofContext; - } - - Authority immutable authority; - History history; // current history - History[] histories; // histories that have been used - History[] backUpHistories; // new histories that are ready to be used - - mapping(History => ClaimContext[]) claimContext; // history => ClaimContext[] - mapping(History => mapping(address => uint256)) numClaims; // history => dapp => #claims - mapping(History => mapping(address => uint256)) nextIndices; // history => dapp => index of the next input to be processed - - uint128 constant MAXIDX = type(uint128).max; - - constructor( - History _history, - Authority _authority, - History[] memory _backUpHistories - ) { - history = _history; - histories.push(history); - authority = _authority; - backUpHistories = _backUpHistories; - } - - function submitClaim(address _dapp, Claim memory _claim) external { - uint256 firstIndex = nextIndices[history][_dapp]; - - // We need to represent `firstIndex` in a uint128 - if (firstIndex > MAXIDX) return; - - // `lastIndex` needs to be greater than or equal to `firstIndex` and - // also fit in a `uint128` - uint256 lastIndex = bound(_claim.lastIndex, firstIndex, MAXIDX); - - _claim.firstIndex = uint128(firstIndex); - _claim.lastIndex = uint128(lastIndex); - - bytes memory encodedData = abi.encode(_dapp, _claim); - - if (address(authority) != history.owner()) { - vm.expectRevert("Ownable: caller is not the owner"); - authority.submitClaim(encodedData); - return; - } - - authority.submitClaim(encodedData); - - // Get the claim index and increment the number of claims - uint256 claimIndex = numClaims[history][_dapp]++; - - claimContext[history].push(ClaimContext(_dapp, _claim, claimIndex)); - - // Here we are not worried about overflowing 'lastIndex` because - // it is a `uint256` guaranteed to fit in a `uint128` - nextIndices[history][_dapp] = lastIndex + 1; - } - - function migrateHistoryToConsensus(address _consensus) external { - if (address(authority) != history.owner()) { - vm.expectRevert("Ownable: caller is not the owner"); - } else if (_consensus == address(0)) { - vm.expectRevert("Ownable: new owner is the zero address"); - } - authority.migrateHistoryToConsensus(_consensus); - } - - function setNewHistory() external { - // take a back up new history from array - if (backUpHistories.length > 0) { - history = backUpHistories[backUpHistories.length - 1]; - backUpHistories.pop(); - authority.setHistory(history); - histories.push(history); - } - } - - function setSameHistory() external { - authority.setHistory(history); - } - - function setOldHistory(uint256 _index) external { - // pick a random old history - // this should not raise a division-by-zero error because - // the `histories` array is guaranteed to have at least one - // history from construction - history = histories[_index % histories.length]; - - // with 50% chance randomly migrate the history to the authority - // this will help cover the cases where authority is not the owner - // of the history contract - if (_index % 2 == 0) { - vm.prank(history.owner()); - history.migrateToConsensus(address(authority)); - } - - authority.setHistory(history); - } - - function checkHistory() external { - assertEq( - address(history), - address(authority.getHistory()), - "check history" - ); - } - - function checkClaimAux( - ClaimContext memory selectedClaimContext, - bytes32 returnedEpochHash, - uint256 returnedFirstIndex, - uint256 returnedLastIndex - ) internal { - assertEq( - returnedEpochHash, - selectedClaimContext.claim.epochHash, - "check epoch hash" - ); - assertEq( - returnedFirstIndex, - selectedClaimContext.claim.firstIndex, - "check first index" - ); - assertEq( - returnedLastIndex, - selectedClaimContext.claim.lastIndex, - "check last index" - ); - } - - function checkClaim( - uint256 _historyIndex, - uint256 _claimContextIndex - ) external { - // this should not raise a division-by-zero error because - // the `histories` array is guaranteed to have at least one - // history from construction - History selectedHistory = histories[_historyIndex % histories.length]; - - // skip if history has no claim - uint256 numClaimContexts = claimContext[selectedHistory].length; - if (numClaimContexts == 0) return; - - ClaimContext memory selectedClaimContext = claimContext[ - selectedHistory - ][_claimContextIndex % numClaimContexts]; - - bytes32 returnedEpochHash; - uint256 returnedFirstIndex; - uint256 returnedLastIndex; - - ( - returnedEpochHash, - returnedFirstIndex, - returnedLastIndex - ) = selectedHistory.getClaim( - selectedClaimContext.dapp, - abi.encode(selectedClaimContext.proofContext) - ); - - checkClaimAux( - selectedClaimContext, - returnedEpochHash, - returnedFirstIndex, - returnedLastIndex - ); - - if (address(selectedHistory) == address(authority.getHistory())) { - // selected history is the current history - // also check that call through authority returns the same claim - ( - returnedEpochHash, - returnedFirstIndex, - returnedLastIndex - ) = authority.getClaim( - selectedClaimContext.dapp, - abi.encode(selectedClaimContext.proofContext) - ); - - checkClaimAux( - selectedClaimContext, - returnedEpochHash, - returnedFirstIndex, - returnedLastIndex - ); - } - } - - // view functions - function getNumHistories() external view returns (uint256) { - return histories.length; - } - - function getNumClaimContext( - uint256 _historyIndex - ) external view returns (uint256) { - return claimContext[histories[_historyIndex]].length; - } -} - -contract AuthorityInvariantTest is Test { - AuthorityHandler handler; - History[] backUpHistories; - - function setUp() public { - // this setup is only for invariant testing - Authority auth = new Authority(address(this)); - History hist = new History(address(auth)); - auth.setHistory(hist); - - // back up new histories - for (uint256 i; i < 30; ++i) { - backUpHistories.push(new History(address(auth))); - } - - handler = new AuthorityHandler(hist, auth, backUpHistories); - auth.transferOwnership(address(handler)); - - targetContract(address(handler)); - } - - function invariantTests() external { - // check all claims - for (uint256 i; i < handler.getNumHistories(); ++i) { - for (uint256 j; j < handler.getNumClaimContext(i); ++j) { - handler.checkClaim(i, j); - } - } - } -} diff --git a/onchain/rollups/test/foundry/history/History.t.sol b/onchain/rollups/test/foundry/history/History.t.sol deleted file mode 100644 index b6220c09..00000000 --- a/onchain/rollups/test/foundry/history/History.t.sol +++ /dev/null @@ -1,234 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -/// @title History Test -pragma solidity ^0.8.8; - -import {Test, stdError} from "forge-std/Test.sol"; -import {History} from "contracts/history/History.sol"; - -contract HistoryTest is Test { - History history; - - event NewClaimToHistory(address indexed dapp, History.Claim claim); - event OwnershipTransferred( - address indexed previousOwner, - address indexed newOwner - ); - - function setUp() public { - vm.expectEmit(true, true, false, false); - emit OwnershipTransferred(address(0), address(this)); - history = new History(address(this)); - } - - function testOwner(address owner) public { - vm.assume(owner != address(0)); - history = new History(owner); - assertEq(history.owner(), owner); - } - - function testInitialConsensus() public { - assertEq(history.owner(), address(this)); - } - - function testMigrateToConsensus(address consensus) public { - vm.assume(consensus != address(0)); - vm.expectEmit(true, true, false, false, address(history)); - emit OwnershipTransferred(address(this), consensus); - history.migrateToConsensus(consensus); - assertEq(history.owner(), consensus); - } - - function testRenounceOwnership() public { - vm.expectEmit(true, true, false, false, address(history)); - emit OwnershipTransferred(address(this), address(0)); - history.renounceOwnership(); - assertEq(history.owner(), address(0)); - } - - function testRevertsMigrationNotOwner(address alice, address bob) public { - vm.assume(alice != address(this)); - vm.assume(bob != address(0)); - vm.expectRevert("Ownable: caller is not the owner"); - vm.startPrank(alice); - history.migrateToConsensus(bob); - vm.stopPrank(); - testInitialConsensus(); // consensus hasn't changed - } - - function testMigrateToZero() public { - vm.expectRevert("Ownable: new owner is the zero address"); - history.migrateToConsensus(address(0)); - testInitialConsensus(); // consensus hasn't changed - } - - function testRevertsRenouncingNotOwner(address alice) public { - vm.assume(alice != address(this)); - vm.expectRevert("Ownable: caller is not the owner"); - vm.startPrank(alice); - history.renounceOwnership(); - vm.stopPrank(); - testInitialConsensus(); // consensus hasn't changed - } - - function submitClaim( - address dapp, - bytes32 epochHash, - uint128 fi, - uint128 li - ) internal { - vm.expectEmit(true, false, false, true, address(history)); - History.Claim memory claim = History.Claim(epochHash, fi, li); - emit NewClaimToHistory(dapp, claim); - bytes memory encodedClaim = abi.encode(dapp, claim); - history.submitClaim(encodedClaim); - } - - function testSubmitAndGetClaims( - address dapp, - bytes32[3] calldata epochHash, - uint64[3] calldata indexIncreases - ) public { - uint128 fi; - for (uint256 i; i < epochHash.length; ++i) { - uint128 li = fi + indexIncreases[i]; - submitClaim(dapp, epochHash[i], fi, li); - fi = li + 1; - } - fi = 0; - for (uint256 i; i < epochHash.length; ++i) { - uint128 li = fi + indexIncreases[i]; - checkClaim(dapp, i, epochHash[i], fi, li); - fi = li + 1; - } - } - - function testRevertsSubmitNotOwner( - address alice, - address dapp, - bytes32 epochHash, - uint128 li - ) public { - vm.assume(alice != address(this)); - vm.startPrank(alice); - vm.expectRevert("Ownable: caller is not the owner"); - history.submitClaim(abi.encode(dapp, epochHash, 0, li)); - vm.stopPrank(); - } - - function testRevertsMaxUint128( - address dapp, - bytes32 epochHash1, - bytes32 epochHash2 - ) public { - uint128 max = type(uint128).max; - submitClaim(dapp, epochHash1, 0, max); - vm.expectRevert(stdError.arithmeticError); - history.submitClaim(abi.encode(dapp, epochHash2, max, max)); - } - - function testRevertsHeadstart( - address dapp, - bytes32 epochHash, - uint128 fi, - uint128 li - ) public { - vm.assume(fi > 0); - vm.assume(fi <= li); - vm.expectRevert(History.UnclaimedInputs.selector); - history.submitClaim(abi.encode(dapp, epochHash, fi, li)); - } - - function testRevertsOverlap( - address dapp, - bytes32 epochHash1, - bytes32 epochHash2, - uint128 fi2, - uint128 li1, - uint128 li2 - ) public { - vm.assume(li1 < type(uint128).max); - vm.assume(fi2 <= li2); - vm.assume(fi2 <= li1); // overlaps with previous claim - submitClaim(dapp, epochHash1, 0, li1); - vm.expectRevert(History.UnclaimedInputs.selector); - history.submitClaim(abi.encode(dapp, epochHash2, fi2, li2)); - } - - function testRevertsHole( - address dapp, - bytes32 epochHash1, - bytes32 epochHash2, - uint128 fi2, - uint128 li1, - uint128 li2 - ) public { - vm.assume(li1 < type(uint128).max); - vm.assume(fi2 <= li2); - vm.assume(fi2 > li1 + 1); // leaves a hole - submitClaim(dapp, epochHash1, 0, li1); - vm.expectRevert(History.UnclaimedInputs.selector); - history.submitClaim(abi.encode(dapp, epochHash2, fi2, li2)); - } - - function testRevertsInputIndices( - address dapp, - bytes32 epochHash1, - bytes32 epochHash2, - uint128 fi2, - uint128 li1, - uint128 li2 - ) public { - vm.assume(li1 < type(uint128).max); - vm.assume(fi2 > li2); // starts after it ends - vm.assume(fi2 > li1); - submitClaim(dapp, epochHash1, 0, li1); - vm.expectRevert(History.InvalidInputIndices.selector); - history.submitClaim(abi.encode(dapp, epochHash2, fi2, li2)); - } - - function testRevertsSubmitClaimEncoding() public { - vm.expectRevert(); - history.submitClaim(""); - } - - function checkClaim( - address dapp, - uint256 claimIndex, - bytes32 epochHash, - uint256 firstInputIndex, - uint256 lastInputIndex - ) internal { - ( - bytes32 retEpochHash, - uint256 retFirstInputIndex, - uint256 retLastInputIndex - ) = history.getClaim(dapp, abi.encode(claimIndex)); - - assertEq(retEpochHash, epochHash); - assertEq(retFirstInputIndex, firstInputIndex); - assertEq(retLastInputIndex, lastInputIndex); - } - - function testRevertsGetClaimEncoding(address dapp) public { - vm.expectRevert(); - history.getClaim(dapp, ""); - } - - function testRevertsBadClaimIndex( - address dapp, - bytes32[] calldata epochHash, - uint256 claimIndex - ) public { - vm.assume(claimIndex >= epochHash.length); - - // submit several claims with 1 input each - for (uint128 i; i < epochHash.length; ++i) { - submitClaim(dapp, epochHash[i], i, i); - } - - vm.expectRevert(History.InvalidClaimIndex.selector); - history.getClaim(dapp, abi.encode(claimIndex)); - } -}