From 3baed5f7015cbcf729d04dfb1dcb66a1a4aefc11 Mon Sep 17 00:00:00 2001 From: Guilherme Dantas Date: Fri, 24 Nov 2023 15:47:05 -0300 Subject: [PATCH] feat!: refactor `IConsensus` interface --- onchain/rollups/.changeset/three-pigs-flow.md | 16 + onchain/rollups/contracts/common/Proof.sol | 6 +- .../contracts/consensus/AbstractConsensus.sol | 45 ++- .../contracts/consensus/IConsensus.sol | 141 ++++---- .../consensus/authority/Authority.sol | 82 ++--- .../rollups/contracts/dapp/CartesiDApp.sol | 74 ++--- .../rollups/contracts/dapp/ICartesiDApp.sol | 9 + .../contracts/library/LibInputRange.sol | 27 ++ .../rollups/contracts/library/LibProof.sol | 16 + .../consensus/authority/Authority.t.sol | 302 +++++------------- .../test/foundry/dapp/CartesiDApp.t.sol | 244 +++++--------- .../test/foundry/util/LibServerManager.sol | 37 +++ .../test/foundry/util/SimpleConsensus.sol | 10 +- 13 files changed, 444 insertions(+), 565 deletions(-) create mode 100644 onchain/rollups/.changeset/three-pigs-flow.md create mode 100644 onchain/rollups/contracts/library/LibInputRange.sol create mode 100644 onchain/rollups/contracts/library/LibProof.sol diff --git a/onchain/rollups/.changeset/three-pigs-flow.md b/onchain/rollups/.changeset/three-pigs-flow.md new file mode 100644 index 00000000..2d4a9900 --- /dev/null +++ b/onchain/rollups/.changeset/three-pigs-flow.md @@ -0,0 +1,16 @@ +--- +"@cartesi/rollups": major +--- + +Refactored the `IConsensus` interface for better interaction with the Cartesi Rollups node. +Added `InputIndexOutOfRange` error to `ICartesiDApp` interface to improve UX of voucher execution. +Updated the `AbstractConsensus` contract to partially implement the new `IConsensus` interface. +Updated the `Authority` contract to implement the new `IConsensus` interface. +Updated the `CartesiDApp` contract to call `getEpochHash` instead of `getClaim`, and to not call `join`. +Replaced the `bytes context` field from the `Proof` structure with an `InputRange inputRange` field. +Removed the `getHistory`, `setHistory` and `migrateHistoryToConsensus` functions and `NewHistory` event from the `Authority` contract. +Contracts that implemented the old `IConsensus` interface and wish to implement the new one must be adapted. +Contracts that implement the new `IConsensus` interface are not backwards compatible with old `CartesiDApp` contracts, since they expect the consensus to expose a `join` function. +Components that would call the `getClaim` function must now call the `getEpochHash` function while passing an input range instead of a "context" blob. +Components that would call the `join` function should not call it anymore, as it is no longer declared in the new interface. +Components that would listen to the `ApplicationJoined` event should not listen to it anymore, as it is no longer declared in the new interface. diff --git a/onchain/rollups/contracts/common/Proof.sol b/onchain/rollups/contracts/common/Proof.sol index 1444ce63..57c8cf79 100644 --- a/onchain/rollups/contracts/common/Proof.sol +++ b/onchain/rollups/contracts/common/Proof.sol @@ -4,12 +4,12 @@ pragma solidity ^0.8.8; import {OutputValidityProof} from "./OutputValidityProof.sol"; +import {InputRange} from "./InputRange.sol"; /// @notice Data for validating outputs. /// @param validity A validity proof for the output -/// @param context Data for querying the right claim from the current consensus contract -/// @dev The encoding of `context` might vary depending on the implementation of the consensus contract. +/// @param inputRange The range of inputs accepted during the epoch struct Proof { OutputValidityProof validity; - bytes context; + InputRange inputRange; } diff --git a/onchain/rollups/contracts/consensus/AbstractConsensus.sol b/onchain/rollups/contracts/consensus/AbstractConsensus.sol index f28e45c8..5435aa51 100644 --- a/onchain/rollups/contracts/consensus/AbstractConsensus.sol +++ b/onchain/rollups/contracts/consensus/AbstractConsensus.sol @@ -4,12 +4,47 @@ pragma solidity ^0.8.8; import {IConsensus} from "./IConsensus.sol"; +import {InputRange} from "../common/InputRange.sol"; +import {LibInputRange} from "../library/LibInputRange.sol"; -/// @title Abstract Consensus -/// @notice An abstract contract that partially implements `IConsensus`. +/// @notice Stores epoch hashes for several DApps and input ranges. +/// @dev This contract was designed to be inherited by implementations of the `IConsensus` interface +/// that only need a simple mechanism of storage and retrieval of epoch hashes. abstract contract AbstractConsensus is IConsensus { - /// @notice Emits an `ApplicationJoined` event with the message sender. - function join() external override { - emit ApplicationJoined(msg.sender); + using LibInputRange for InputRange; + + /// @notice Indexes epoch hashes by DApp address, first input index and last input index. + mapping(address => mapping(uint256 => mapping(uint256 => bytes32))) + private _epochHashes; + + /// @notice Get the epoch hash for a certain DApp and input range. + /// @param dapp The DApp contract address + /// @param r The input range + /// @return epochHash The epoch hash + /// @dev For claimed epochs, returns the epoch hash of the last accepted claim. + /// @dev For unclaimed epochs, returns `bytes32(0)`. + function getEpochHash( + address dapp, + InputRange calldata r + ) public view override returns (bytes32 epochHash) { + epochHash = _epochHashes[dapp][r.firstInputIndex][r.lastInputIndex]; + } + + /// @notice Accept a claim. + /// @param dapp The DApp contract address + /// @param r The input range + /// @param epochHash The epoch hash + /// @dev Raises an `InputRangeIsEmptySet` error if `r` represents the empty set. + /// @dev On successs, emits a `ClaimAcceptance` event. + function _acceptClaim( + address dapp, + InputRange calldata r, + bytes32 epochHash + ) internal { + if (r.isEmptySet()) { + revert InputRangeIsEmptySet(dapp, r, epochHash); + } + _epochHashes[dapp][r.firstInputIndex][r.lastInputIndex] = epochHash; + emit ClaimAcceptance(dapp, r, epochHash); } } diff --git a/onchain/rollups/contracts/consensus/IConsensus.sol b/onchain/rollups/contracts/consensus/IConsensus.sol index 3034e7cf..460264cf 100644 --- a/onchain/rollups/contracts/consensus/IConsensus.sol +++ b/onchain/rollups/contracts/consensus/IConsensus.sol @@ -3,74 +3,81 @@ pragma solidity ^0.8.8; -/// @title Consensus interface -/// -/// @notice This contract defines a generic interface for consensuses. -/// We use the word "consensus" to designate a contract that provides claims -/// in the base layer regarding the state of off-chain machines running in -/// the execution layer. How this contract is able to reach consensus, who is -/// able to submit claims, and how are claims stored in the base layer are -/// some of the implementation details left unspecified by this interface. -/// -/// From the point of view of a DApp, these claims are necessary to validate -/// on-chain action allowed by the off-chain machine in the form of vouchers -/// and notices. Each claim is composed of three parts: an epoch hash, a first -/// index, and a last index. We'll explain each of these parts below. -/// -/// First, let us define the word "epoch". For finality reasons, we need to -/// divide the stream of inputs being fed into the off-chain machine into -/// batches of inputs, which we call "epoches". At the end of every epoch, -/// we summarize the state of the off-chain machine in a single hash, called -/// "epoch hash". Please note that this interface does not define how this -/// stream of inputs is being chopped up into epoches. -/// -/// The other two parts are simply the indices of the first and last inputs -/// accepted during the epoch. Logically, the first index MUST BE less than -/// or equal to the last index. As a result, every epoch MUST accept at least -/// one input. This assumption stems from the fact that the state of a machine -/// can only change after an input is fed into it. -/// -/// Examples of possible implementations of this interface include: -/// -/// * An authority consensus, controlled by a single address who has full -/// control over epoch boundaries, claim submission, asset management, etc. -/// -/// * A quorum consensus, controlled by a limited set of validators, that -/// vote on the state of the machine at the end of every epoch. Also, epoch -/// boundaries are determined by the timestamp in the base layer, and assets -/// are split equally amongst the validators. -/// -/// * An NxN consensus, which allows anyone to submit and dispute claims -/// in the base layer. Epoch boundaries are determined in the same fashion -/// as in the quorum example. -/// +import {InputRange} from "../common/InputRange.sol"; + +/// @notice Provides epoch hashes for DApps. +/// @notice An epoch hash is produced after the machine processes a range of inputs and the epoch is finalized. +/// This hash can be later used to prove that any given output was produced by the machine during the epoch. +/// @notice After an epoch is finalized, a validator may submit a claim containing: the address of the DApp contract, +/// the range of inputs accepted during the epoch, and the epoch hash. +/// @notice Input ranges cannot represent the empty set, since at least one input is necessary to advance the state of the machine. +/// @notice Validators may synchronize epoch finalization, but such mechanism is not specified by this interface. +/// @notice A validator should be able to save transaction fees by not submitting a claim if it was... +/// - already submitted by the validator (see the `ClaimSubmission` event) or; +/// - already accepted by the consensus (see the `ClaimAcceptance` event). +/// @notice The acceptance criteria for claims may depend on the type of consensus, and is not specified by this interface. +/// For example, a claim may be accepted if it was... +/// - submitted by an authority or; +/// - submitted by the majority of a quorum or; +/// - submitted and not proven wrong after some period of time. interface IConsensus { - /// @notice An application has joined the consensus' validation set. - /// @param application The application - /// @dev MUST be triggered on a successful call to `join`. - event ApplicationJoined(address application); + /// @notice Tried to submit a claim with an input range that represents the empty set. + /// @param inputRange The input range + /// @dev An input range represents the empty set if, and only if, the first input index is + /// greater than the last input index. + error InputRangeIsEmptySet( + address dapp, + InputRange inputRange, + bytes32 epochHash + ); + + /// @notice A claim was submitted to the consensus. + /// @param submitter The submitter address + /// @param dapp The DApp contract address + /// @param inputRange The input range + /// @param epochHash The epoch hash + /// @dev The input range MUST NOT represent the empty set. + /// @dev Overwrites any previous submissions regarding `submitter`, `dapp` and `inputRange`. + event ClaimSubmission( + address indexed submitter, + address indexed dapp, + InputRange inputRange, + bytes32 epochHash + ); + + /// @notice A claim was accepted by the consensus. + /// @param dapp The DApp contract address + /// @param inputRange The input range + /// @param epochHash The epoch hash + /// @dev The input range MUST NOT represent the empty set. + /// @dev MUST be triggered after some `ClaimSubmission` event regarding `dapp`, `inputRange` and `epochHash`. + /// @dev Overwrites any previous acceptances regarding `dapp` and `inputRange`. + event ClaimAcceptance( + address indexed dapp, + InputRange inputRange, + bytes32 epochHash + ); - /// @notice Get a specific claim regarding a specific DApp. - /// The encoding of `_proofContext` might vary - /// depending on the implementation. - /// @param _dapp The DApp address - /// @param _proofContext Data for retrieving the desired claim - /// @return epochHash_ The claimed epoch hash - /// @return firstInputIndex_ The index of the first input of the epoch in the input box - /// @return lastInputIndex_ The index of the last input of the epoch in the input box - function getClaim( - address _dapp, - bytes calldata _proofContext - ) - external - view - returns ( - bytes32 epochHash_, - uint256 firstInputIndex_, - uint256 lastInputIndex_ - ); + /// @notice Submit a claim to the consensus. + /// @param dapp The DApp contract address + /// @param inputRange The input range + /// @param epochHash The epoch hash + /// @dev MAY raise an `InputRangeIsEmptySet` error if the input range represents the empty set. + /// @dev On success, MUST trigger a `ClaimSubmission` event. + function submitClaim( + address dapp, + InputRange calldata inputRange, + bytes32 epochHash + ) external; - /// @notice Signal the consensus that the message sender wants to join its validation set. - /// @dev MUST fire an `ApplicationJoined` event with the message sender as argument. - function join() external; + /// @notice Get the epoch hash for a certain DApp and input range. + /// @param dapp The DApp contract address + /// @param inputRange The input range + /// @return epochHash The epoch hash + /// @dev For claimed epochs, must return the epoch hash of the last accepted claim. + /// @dev For unclaimed epochs, MUST either revert or return `bytes32(0)`. + function getEpochHash( + address dapp, + InputRange calldata inputRange + ) external view returns (bytes32 epochHash); } diff --git a/onchain/rollups/contracts/consensus/authority/Authority.sol b/onchain/rollups/contracts/consensus/authority/Authority.sol index 9ceb3b27..386218e5 100644 --- a/onchain/rollups/contracts/consensus/authority/Authority.sol +++ b/onchain/rollups/contracts/consensus/authority/Authority.sol @@ -7,70 +7,28 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IConsensus} from "../IConsensus.sol"; import {AbstractConsensus} from "../AbstractConsensus.sol"; -import {IHistory} from "../../history/IHistory.sol"; +import {InputRange} from "../../common/InputRange.sol"; -/// @title Authority consensus -/// @notice A consensus model controlled by a single address, the owner. -/// Claims are stored in an auxiliary contract called `History`. -/// @dev This contract inherits `AbstractConsensus` and OpenZeppelin's `Ownable` contract. +/// @notice A consensus contract controlled by a single address, the owner. +/// @dev This contract inherits from OpenZeppelin's `Ownable` contract. /// For more information on `Ownable`, please consult OpenZeppelin's official documentation. contract Authority is AbstractConsensus, Ownable { - /// @notice The current history contract. - /// @dev See the `getHistory` and `setHistory` functions. - IHistory internal history; - - /// @notice A new history contract is used to store claims. - /// @param history The new history contract - /// @dev MUST be triggered on a successful call to `setHistory`. - event NewHistory(IHistory history); - - /// @notice Constructs an `Authority` contract. - /// @param _initialOwner The initial contract owner - constructor(address _initialOwner) Ownable(_initialOwner) {} - - /// @notice Submits a claim to the current history contract. - /// The encoding of `_claimData` might vary depending on the - /// implementation of the current history contract. - /// @param _claimData Data for submitting a claim - /// @dev Can only be called by the `Authority` owner, - /// and the `Authority` contract must have ownership over - /// its current history contract. - function submitClaim(bytes calldata _claimData) external onlyOwner { - history.submitClaim(_claimData); - } - - /// @notice Transfer ownership over the current history contract to `_consensus`. - /// @param _consensus The new owner of the current history contract - /// @dev Can only be called by the `Authority` owner, - /// and the `Authority` contract must have ownership over - /// its current history contract. - function migrateHistoryToConsensus(address _consensus) external onlyOwner { - history.migrateToConsensus(_consensus); - } - - /// @notice Make `Authority` point to another history contract. - /// @param _history The new history contract - /// @dev Emits a `NewHistory` event. - /// Can only be called by the `Authority` owner. - function setHistory(IHistory _history) external onlyOwner { - history = _history; - emit NewHistory(_history); - } - - /// @notice Get the current history contract. - /// @return The current history contract - function getHistory() external view returns (IHistory) { - return history; - } - - /// @notice Get a claim from the current history. - /// The encoding of `_proofContext` might vary depending on the - /// implementation of the current history contract. - /// @inheritdoc IConsensus - function getClaim( - address _dapp, - bytes calldata _proofContext - ) external view override returns (bytes32, uint256, uint256) { - return history.getClaim(_dapp, _proofContext); + /// @param initialOwner The initial contract owner + constructor(address initialOwner) Ownable(initialOwner) {} + + /// @notice Submit a claim. + /// @param dapp The DApp contract address + /// @param inputRange The input range + /// @param epochHash The epoch hash + /// @dev The input range MUST NOT represent the empty set. + /// @dev On success, triggers a `ClaimSubmission` event and a `ClaimAcceptance` event. + /// @dev Can only be called by the owner. + function submitClaim( + address dapp, + InputRange calldata inputRange, + bytes32 epochHash + ) external override onlyOwner { + emit ClaimSubmission(msg.sender, dapp, inputRange, epochHash); + _acceptClaim(dapp, inputRange, epochHash); } } diff --git a/onchain/rollups/contracts/dapp/CartesiDApp.sol b/onchain/rollups/contracts/dapp/CartesiDApp.sol index fac152ff..a2019613 100644 --- a/onchain/rollups/contracts/dapp/CartesiDApp.sol +++ b/onchain/rollups/contracts/dapp/CartesiDApp.sol @@ -10,6 +10,9 @@ import {IInputRelay} from "../inputs/IInputRelay.sol"; import {LibOutputValidation} from "../library/LibOutputValidation.sol"; import {OutputValidityProof} from "../common/OutputValidityProof.sol"; import {Proof} from "../common/Proof.sol"; +import {LibProof} from "../library/LibProof.sol"; +import {InputRange} from "../common/InputRange.sol"; +import {LibInputRange} from "../library/LibInputRange.sol"; import {Bitmask} from "@cartesi/util/contracts/Bitmask.sol"; @@ -72,6 +75,8 @@ contract CartesiDApp is { using Bitmask for mapping(uint256 => uint256); using LibOutputValidation for OutputValidityProof; + using LibProof for Proof; + using LibInputRange for InputRange; using Address for address; /// @notice Raised when executing an already executed voucher. @@ -110,7 +115,6 @@ contract CartesiDApp is /// @param _inputRelays The input relays /// @param _initialOwner The initial DApp owner /// @param _templateHash The initial machine state hash - /// @dev Calls the `join` function on `_consensus`. constructor( IConsensus _consensus, IInputBox _inputBox, @@ -124,8 +128,6 @@ contract CartesiDApp is for (uint256 i; i < _inputRelays.length; ++i) { inputRelays.push(_inputRelays[i]); } - - _consensus.join(); } function supportsInterface( @@ -142,20 +144,13 @@ contract CartesiDApp is bytes calldata _payload, Proof calldata _proof ) external override nonReentrant { - bytes32 epochHash; - uint256 firstInputIndex; - uint256 lastInputIndex; - uint256 inputIndex; - - // query the current consensus for the desired claim - (epochHash, firstInputIndex, lastInputIndex) = getClaim(_proof.context); - - // validate input index range and calculate the input index - // based on the input index range provided by the consensus - inputIndex = _proof.validity.validateInputIndexRange( - firstInputIndex, - lastInputIndex - ); + uint256 inputIndex = _proof.calculateInputIndex(); + + if (!_proof.inputRange.contains(inputIndex)) { + revert InputIndexOutOfRange(inputIndex, _proof.inputRange); + } + + bytes32 epochHash = getEpochHash(_proof.inputRange); // reverts if proof isn't valid _proof.validity.validateVoucher(_destination, _payload, epochHash); @@ -199,43 +194,22 @@ contract CartesiDApp is bytes calldata _notice, Proof calldata _proof ) external view override { - bytes32 epochHash; - uint256 firstInputIndex; - uint256 lastInputIndex; - - // query the current consensus for the desired claim - (epochHash, firstInputIndex, lastInputIndex) = getClaim(_proof.context); - - // validate the epoch input index based on the input index range - // provided by the consensus - _proof.validity.validateInputIndexRange( - firstInputIndex, - lastInputIndex - ); + uint256 inputIndex = _proof.calculateInputIndex(); + + if (!_proof.inputRange.contains(inputIndex)) { + revert InputIndexOutOfRange(inputIndex, _proof.inputRange); + } + + bytes32 epochHash = getEpochHash(_proof.inputRange); // reverts if proof isn't valid _proof.validity.validateNotice(_notice, epochHash); } - /// @notice Retrieve a claim about the DApp from the current consensus. - /// The encoding of `_proofContext` might vary depending on the implementation. - /// @param _proofContext Data for retrieving the desired claim - /// @return The claimed epoch hash - /// @return The index of the first input of the epoch in the input box - /// @return The index of the last input of the epoch in the input box - function getClaim( - bytes calldata _proofContext - ) internal view returns (bytes32, uint256, uint256) { - return consensus.getClaim(address(this), _proofContext); - } - function migrateToConsensus( IConsensus _newConsensus ) external override onlyOwner { consensus = _newConsensus; - - _newConsensus.join(); - emit NewConsensus(_newConsensus); } @@ -282,4 +256,14 @@ contract CartesiDApp is revert EtherTransferFailed(); } } + + /// @notice Get the epoch hash regarding the given input range + /// and the DApp from the current consensus. + /// @param inputRange The input range + /// @return The epoch hash + function getEpochHash( + InputRange calldata inputRange + ) internal view returns (bytes32) { + return consensus.getEpochHash(address(this), inputRange); + } } diff --git a/onchain/rollups/contracts/dapp/ICartesiDApp.sol b/onchain/rollups/contracts/dapp/ICartesiDApp.sol index 427897e8..e93b02b5 100644 --- a/onchain/rollups/contracts/dapp/ICartesiDApp.sol +++ b/onchain/rollups/contracts/dapp/ICartesiDApp.sol @@ -11,9 +11,18 @@ import {IInputBox} from "../inputs/IInputBox.sol"; import {IInputRelay} from "../inputs/IInputRelay.sol"; import {OutputValidityProof} from "../common/OutputValidityProof.sol"; import {Proof} from "../common/Proof.sol"; +import {InputRange} from "../common/InputRange.sol"; /// @title Cartesi DApp interface interface ICartesiDApp is IERC721Receiver, IERC1155Receiver { + // Errors + + /// @notice Could not validate an output because + /// the input that generated it is outside the given input range. + /// @param inputIndex The input index + /// @param inputRange The input range + error InputIndexOutOfRange(uint256 inputIndex, InputRange inputRange); + // Events /// @notice The DApp has migrated to another consensus contract. diff --git a/onchain/rollups/contracts/library/LibInputRange.sol b/onchain/rollups/contracts/library/LibInputRange.sol new file mode 100644 index 00000000..dbfe6ace --- /dev/null +++ b/onchain/rollups/contracts/library/LibInputRange.sol @@ -0,0 +1,27 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +import {InputRange} from "../common/InputRange.sol"; + +library LibInputRange { + /// @notice Check if an input range represents the empty set. + /// @param r The input range + /// @return Whether the input range represents the empty set. + function isEmptySet(InputRange calldata r) internal pure returns (bool) { + return r.firstInputIndex > r.lastInputIndex; + } + + /// @notice Check if an input range contains an input. + /// @param r The input range + /// @param inputIndex The input index + /// @return Whether the input range contains the input. + function contains( + InputRange calldata r, + uint256 inputIndex + ) internal pure returns (bool) { + return + r.firstInputIndex <= inputIndex && inputIndex <= r.lastInputIndex; + } +} diff --git a/onchain/rollups/contracts/library/LibProof.sol b/onchain/rollups/contracts/library/LibProof.sol new file mode 100644 index 00000000..a1ba697c --- /dev/null +++ b/onchain/rollups/contracts/library/LibProof.sol @@ -0,0 +1,16 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +import {Proof} from "../common/Proof.sol"; + +library LibProof { + function calculateInputIndex( + Proof calldata proof + ) internal pure returns (uint256 inputIndex) { + inputIndex = + proof.inputRange.firstInputIndex + + proof.validity.inputIndexWithinEpoch; + } +} diff --git a/onchain/rollups/test/foundry/consensus/authority/Authority.t.sol b/onchain/rollups/test/foundry/consensus/authority/Authority.t.sol index 7a17bf8c..dd57ac41 100644 --- a/onchain/rollups/test/foundry/consensus/authority/Authority.t.sol +++ b/onchain/rollups/test/foundry/consensus/authority/Authority.t.sol @@ -4,55 +4,52 @@ /// @title Authority Test pragma solidity ^0.8.8; -import {TestBase} from "../../util/TestBase.sol"; -import {Authority} from "contracts/consensus/authority/Authority.sol"; -import {IHistory} from "contracts/history/IHistory.sol"; import {Vm} from "forge-std/Vm.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -contract HistoryReverts is IHistory { - function submitClaim(bytes calldata) external pure override { - revert(); - } - - function migrateToConsensus(address) external pure override { - revert(); - } +import {Authority} from "contracts/consensus/authority/Authority.sol"; +import {IConsensus} from "contracts/consensus/IConsensus.sol"; +import {InputRange} from "contracts/common/InputRange.sol"; +import {LibInputRange} from "contracts/library/LibInputRange.sol"; - function getClaim( - address, - bytes calldata - ) external pure override returns (bytes32, uint256, uint256) { - revert(); - } -} +import {TestBase} from "../../util/TestBase.sol"; contract AuthorityTest is TestBase { - Authority authority; + using LibInputRange for InputRange; - // events event OwnershipTransferred( address indexed previousOwner, address indexed newOwner ); - event NewHistory(IHistory history); - event ApplicationJoined(address application); - function testConstructor(address _owner) public { - vm.assume(_owner != address(0)); + event ClaimSubmission( + address indexed submitter, + address indexed dapp, + InputRange inputRange, + bytes32 epochHash + ); + + event ClaimAcceptance( + address indexed dapp, + InputRange inputRange, + bytes32 epochHash + ); + + function testConstructor(address owner) public { + vm.assume(owner != address(0)); vm.expectEmit(true, true, false, false); - emit OwnershipTransferred(address(0), _owner); + emit OwnershipTransferred(address(0), owner); vm.recordLogs(); - authority = new Authority(_owner); + Authority authority = new Authority(owner); Vm.Log[] memory entries = vm.getRecordedLogs(); assertEq(entries.length, 1, "number of events"); - assertEq(authority.owner(), _owner, "authority owner"); + assertEq(authority.owner(), owner, "authority owner"); } function testRevertsOwnerAddressZero() public { @@ -65,227 +62,96 @@ contract AuthorityTest is TestBase { new Authority(address(0)); } - function testMigrateHistory( - address _owner, - IHistory _history, - address _newConsensus - ) public isMockable(address(_history)) { - vm.assume(_owner != address(0)); - vm.assume(_owner != address(this)); - vm.assume(_newConsensus != address(0)); - - authority = new Authority(_owner); - - vm.prank(_owner); - authority.setHistory(_history); - - vm.assume(address(_history) != address(authority)); - vm.mockCall( - address(_history), - abi.encodeWithSelector( - IHistory.migrateToConsensus.selector, - _newConsensus - ), - "" - ); - - // will fail as not called from owner - vm.expectRevert( - abi.encodeWithSelector( - Ownable.OwnableUnauthorizedAccount.selector, - address(this) - ) - ); - authority.migrateHistoryToConsensus(_newConsensus); - - vm.expectCall( - address(_history), - abi.encodeWithSelector( - IHistory.migrateToConsensus.selector, - _newConsensus - ) - ); + function testSubmitClaimRevertsCallerNotOwner( + address owner, + address notOwner, + address dapp, + InputRange calldata inputRange, + bytes32 epochHash + ) public { + vm.assume(owner != address(0)); + vm.assume(owner != notOwner); + vm.assume(!inputRange.isEmptySet()); - // can only be called by owner - vm.prank(_owner); - authority.migrateHistoryToConsensus(_newConsensus); - } + Authority authority = new Authority(owner); - function testSubmitClaim( - address _owner, - IHistory _history, - bytes calldata _claim - ) public isMockable(address(_history)) { - vm.assume(_owner != address(0)); - vm.assume(_owner != address(this)); - - authority = new Authority(_owner); - - vm.prank(_owner); - authority.setHistory(_history); - - vm.assume(address(_history) != address(authority)); - vm.mockCall( - address(_history), - abi.encodeWithSelector(IHistory.submitClaim.selector, _claim), - "" - ); - - // will fail as not called from owner vm.expectRevert( abi.encodeWithSelector( Ownable.OwnableUnauthorizedAccount.selector, - address(this) + notOwner ) ); - authority.submitClaim(_claim); - - vm.expectCall( - address(_history), - abi.encodeWithSelector(IHistory.submitClaim.selector, _claim) - ); - // can only be called by owner - vm.prank(_owner); - authority.submitClaim(_claim); + vm.prank(notOwner); + authority.submitClaim(dapp, inputRange, epochHash); } - function testSetHistory( - address _owner, - IHistory _history, - IHistory _newHistory + function testSubmitClaimRevertsInputRangeIsEmptySet( + address owner, + address dapp, + InputRange calldata inputRange, + bytes32 epochHash ) public { - vm.assume(_owner != address(0)); - vm.assume(_owner != address(this)); + vm.assume(owner != address(0)); + vm.assume(inputRange.isEmptySet()); - authority = new Authority(_owner); + Authority authority = new Authority(owner); - vm.prank(_owner); - vm.expectEmit(false, false, false, true); - emit NewHistory(_history); - authority.setHistory(_history); - - // before setting new history - assertEq(address(authority.getHistory()), address(_history)); - - // set new history - // will fail as not called from owner vm.expectRevert( abi.encodeWithSelector( - Ownable.OwnableUnauthorizedAccount.selector, - address(this) + IConsensus.InputRangeIsEmptySet.selector, + dapp, + inputRange, + epochHash ) ); - authority.setHistory(_newHistory); - - // can only be called by owner - vm.prank(_owner); - // expect event NewHistory - vm.expectEmit(false, false, false, true); - emit NewHistory(_newHistory); - authority.setHistory(_newHistory); - // after setting new history - assertEq(address(authority.getHistory()), address(_newHistory)); + vm.prank(owner); + authority.submitClaim(dapp, inputRange, epochHash); } - function testGetClaim( - address _owner, - IHistory _history, - address _dapp, - bytes calldata _proofContext, - bytes32 _r0, - uint256 _r1, - uint256 _r2 - ) public isMockable(address(_history)) { - vm.assume(_owner != address(0)); - vm.assume(_owner != address(this)); - - authority = new Authority(_owner); - - vm.prank(_owner); - authority.setHistory(_history); - - // mocking history - vm.assume(address(_history) != address(authority)); - vm.mockCall( - address(_history), - abi.encodeWithSelector( - IHistory.getClaim.selector, - _dapp, - _proofContext - ), - abi.encode(_r0, _r1, _r2) - ); - - vm.expectCall( - address(_history), - abi.encodeWithSelector( - IHistory.getClaim.selector, - _dapp, - _proofContext - ) - ); - - // perform call - (bytes32 r0, uint256 r1, uint256 r2) = authority.getClaim( - _dapp, - _proofContext - ); - - // check result - assertEq(_r0, r0); - assertEq(_r1, r1); - assertEq(_r2, r2); - } - - // test behaviors when history reverts - function testHistoryReverts( - address _owner, - IHistory _newHistory, - address _dapp, - bytes calldata _claim, - address _consensus, - bytes calldata _proofContext + function testSubmitClaim( + address owner, + address dapp, + InputRange calldata inputRange, + bytes32 epochHash1, + bytes32 epochHash2 ) public { - vm.assume(_owner != address(0)); - - HistoryReverts historyR = new HistoryReverts(); + vm.assume(owner != address(0)); + vm.assume(!inputRange.isEmptySet()); - authority = new Authority(_owner); + Authority authority = new Authority(owner); - vm.prank(_owner); - authority.setHistory(historyR); - assertEq(address(authority.getHistory()), address(historyR)); + // First claim - vm.expectRevert(); - vm.prank(_owner); - authority.submitClaim(_claim); + expectClaimEvents(authority, owner, dapp, inputRange, epochHash1); - vm.expectRevert(); - vm.prank(_owner); - authority.migrateHistoryToConsensus(_consensus); + vm.prank(owner); + authority.submitClaim(dapp, inputRange, epochHash1); - vm.expectRevert(); - authority.getClaim(_dapp, _proofContext); + assertEq(authority.getEpochHash(dapp, inputRange), epochHash1); - vm.prank(_owner); - authority.setHistory(_newHistory); - assertEq(address(authority.getHistory()), address(_newHistory)); - } - - function testJoin(address _owner, IHistory _history, address _dapp) public { - vm.assume(_owner != address(0)); + // Second claim - authority = new Authority(_owner); + expectClaimEvents(authority, owner, dapp, inputRange, epochHash2); - vm.prank(_owner); - authority.setHistory(_history); + vm.prank(owner); + authority.submitClaim(dapp, inputRange, epochHash2); - vm.expectEmit(false, false, false, true); - emit ApplicationJoined(_dapp); + assertEq(authority.getEpochHash(dapp, inputRange), epochHash2); + } - vm.prank(_dapp); - authority.join(); + function expectClaimEvents( + Authority authority, + address owner, + address dapp, + InputRange calldata inputRange, + bytes32 epochHash + ) internal { + vm.expectEmit(true, true, false, true, address(authority)); + emit ClaimSubmission(owner, dapp, inputRange, epochHash); + + vm.expectEmit(true, false, false, true, address(authority)); + emit ClaimAcceptance(dapp, inputRange, epochHash); } } diff --git a/onchain/rollups/test/foundry/dapp/CartesiDApp.t.sol b/onchain/rollups/test/foundry/dapp/CartesiDApp.t.sol index 8430848a..725543b5 100644 --- a/onchain/rollups/test/foundry/dapp/CartesiDApp.t.sol +++ b/onchain/rollups/test/foundry/dapp/CartesiDApp.t.sol @@ -13,8 +13,10 @@ import {IConsensus} from "contracts/consensus/IConsensus.sol"; import {IInputBox} from "contracts/inputs/IInputBox.sol"; import {IInputRelay} from "contracts/inputs/IInputRelay.sol"; import {LibOutputValidation} from "contracts/library/LibOutputValidation.sol"; +import {LibProof} from "contracts/library/LibProof.sol"; import {OutputValidityProof} from "contracts/common/OutputValidityProof.sol"; import {OutputEncoding} from "contracts/common/OutputEncoding.sol"; +import {InputRange} from "contracts/common/InputRange.sol"; import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; @@ -39,6 +41,8 @@ contract EtherReceiver { contract CartesiDAppTest is TestBase { using LibServerManager for LibServerManager.RawFinishEpochResponse; using LibServerManager for LibServerManager.Proof; + using LibServerManager for LibServerManager.Proof[]; + using LibProof for Proof; enum OutputName { DummyNotice, @@ -192,16 +196,9 @@ contract CartesiDAppTest is TestBase { // test notices - function testNoticeValidation( - uint256 _inputIndex, - uint256 _numInputsAfter - ) public { + function testNoticeValidation() public { bytes memory notice = getNotice(OutputName.DummyNotice); - Proof memory proof = setupNoticeProof( - OutputName.DummyNotice, - _inputIndex, - _numInputsAfter - ); + Proof memory proof = setupNoticeProof(OutputName.DummyNotice); validateNotice(notice, proof); @@ -215,19 +212,11 @@ contract CartesiDAppTest is TestBase { // test vouchers - function testExecuteVoucherAndEvent( - uint256 _dappInitBalance, - uint256 _inputIndex, - uint256 _numInputsAfter - ) public { + function testExecuteVoucherAndEvent(uint256 _dappInitBalance) public { _dappInitBalance = boundBalance(_dappInitBalance); Voucher memory voucher = getVoucher(OutputName.ERC20TransferVoucher); - Proof memory proof = setupVoucherProof( - OutputName.ERC20TransferVoucher, - _inputIndex, - _numInputsAfter - ); + Proof memory proof = setupVoucherProof(OutputName.ERC20TransferVoucher); // not able to execute voucher because dapp has 0 balance assertEq(erc20Token.balanceOf(address(dapp)), 0); @@ -255,7 +244,7 @@ contract CartesiDAppTest is TestBase { emit VoucherExecuted( LibOutputValidation.getBitMaskPosition( proof.validity.outputIndexWithinInput, - _inputIndex + _calculateInputIndex(proof) ) ); @@ -270,19 +259,11 @@ contract CartesiDAppTest is TestBase { assertEq(erc20Token.balanceOf(recipient), transferAmount); } - function testRevertsReexecution( - uint256 _dappInitBalance, - uint256 _inputIndex, - uint256 _numInputsAfter - ) public { + function testRevertsReexecution(uint256 _dappInitBalance) public { _dappInitBalance = boundBalance(_dappInitBalance); Voucher memory voucher = getVoucher(OutputName.ERC20TransferVoucher); - Proof memory proof = setupVoucherProof( - OutputName.ERC20TransferVoucher, - _inputIndex, - _numInputsAfter - ); + Proof memory proof = setupVoucherProof(OutputName.ERC20TransferVoucher); // fund dapp vm.prank(tokenOwner); @@ -303,23 +284,17 @@ contract CartesiDAppTest is TestBase { assertEq(erc20Token.balanceOf(recipient), transferAmount); } - function testWasVoucherExecuted( - uint256 _dappInitBalance, - uint128 _inputIndex, - uint128 _numInputsAfter - ) public { + function testWasVoucherExecuted(uint256 _dappInitBalance) public { _dappInitBalance = boundBalance(_dappInitBalance); Voucher memory voucher = getVoucher(OutputName.ERC20TransferVoucher); - Proof memory proof = setupVoucherProof( - OutputName.ERC20TransferVoucher, - _inputIndex, - _numInputsAfter - ); + Proof memory proof = setupVoucherProof(OutputName.ERC20TransferVoucher); + + uint256 inputIndex = _calculateInputIndex(proof); // before executing voucher bool executed = dapp.wasVoucherExecuted( - _inputIndex, + inputIndex, proof.validity.outputIndexWithinInput ); assertEq(executed, false); @@ -337,7 +312,7 @@ contract CartesiDAppTest is TestBase { // `wasVoucherExecuted` should still return false executed = dapp.wasVoucherExecuted( - _inputIndex, + inputIndex, proof.validity.outputIndexWithinInput ); assertEq(executed, false); @@ -349,22 +324,15 @@ contract CartesiDAppTest is TestBase { // after executing voucher, `wasVoucherExecuted` should return true executed = dapp.wasVoucherExecuted( - _inputIndex, + inputIndex, proof.validity.outputIndexWithinInput ); assertEq(executed, true); } - function testRevertsEpochHash( - uint256 _inputIndex, - uint256 _numInputsAfter - ) public { + function testRevertsEpochHash() public { Voucher memory voucher = getVoucher(OutputName.ERC20TransferVoucher); - Proof memory proof = setupVoucherProof( - OutputName.ERC20TransferVoucher, - _inputIndex, - _numInputsAfter - ); + Proof memory proof = setupVoucherProof(OutputName.ERC20TransferVoucher); proof.validity.vouchersEpochRootHash = bytes32(uint256(0xdeadbeef)); @@ -372,16 +340,9 @@ contract CartesiDAppTest is TestBase { executeVoucher(voucher, proof); } - function testRevertsOutputsEpochRootHash( - uint256 _inputIndex, - uint256 _numInputsAfter - ) public { + function testRevertsOutputsEpochRootHash() public { Voucher memory voucher = getVoucher(OutputName.ERC20TransferVoucher); - Proof memory proof = setupVoucherProof( - OutputName.ERC20TransferVoucher, - _inputIndex, - _numInputsAfter - ); + Proof memory proof = setupVoucherProof(OutputName.ERC20TransferVoucher); proof.validity.outputHashesRootHash = bytes32(uint256(0xdeadbeef)); @@ -391,16 +352,9 @@ contract CartesiDAppTest is TestBase { executeVoucher(voucher, proof); } - function testRevertsOutputHashesRootHash( - uint256 _inputIndex, - uint256 _numInputsAfter - ) public { + function testRevertsOutputHashesRootHash() public { Voucher memory voucher = getVoucher(OutputName.ERC20TransferVoucher); - Proof memory proof = setupVoucherProof( - OutputName.ERC20TransferVoucher, - _inputIndex, - _numInputsAfter - ); + Proof memory proof = setupVoucherProof(OutputName.ERC20TransferVoucher); proof.validity.outputIndexWithinInput = 0xdeadbeef; @@ -410,62 +364,40 @@ contract CartesiDAppTest is TestBase { executeVoucher(voucher, proof); } - function testRevertsInputIndexOOB(uint256 _inputIndex) public { - Voucher memory voucher = getVoucher(OutputName.ERC20TransferVoucher); - Proof memory proof = setupVoucherProof( - OutputName.ERC20TransferVoucher, - _inputIndex, - 0 - ); + function testRevertsInputIndexOutOfRange() public { + OutputName outputName = OutputName.ERC20TransferVoucher; + Voucher memory voucher = getVoucher(outputName); + Proof memory proof = getVoucherProof(uint256(outputName)); + uint256 inputIndex = _calculateInputIndex(proof); - // If the input index within epoch were 0, then there would be no way for the - // input index in input box to be out of bounds because every claim is non-empty, + // If the input index were 0, then there would be no way for the input index + // in input box to be out of bounds because every claim is non-empty, // as it must contain at least one input - assert(proof.validity.inputIndexWithinEpoch > 0); + require(inputIndex >= 1, "cannot test with input index less than 1"); - // This assumption aims to avoid an integer overflow in the CartesiDApp - vm.assume( - _inputIndex <= - type(uint256).max - proof.validity.inputIndexWithinEpoch - ); - - // Calculate epoch hash from proof - bytes32 epochHash = calculateEpochHash(proof.validity); - - // Mock consensus again to return a claim that spans only 1 input, - // but we are registering a proof whose epoch input index is 1... - // so the proof would succeed but the input would be out of bounds - vm.mockCall( - address(consensus), - abi.encodeWithSelector( - IConsensus.getClaim.selector, - address(dapp), - proof.context - ), - abi.encode(epochHash, _inputIndex, _inputIndex) - ); + // Here we change the input range artificially to make it look like it ends + // before the actual input (which is still provable!). + // The `CartesiDApp` contract, however, will not allow such proof. + proof.inputRange.lastInputIndex = inputIndex - 1; + mockConsensus(proof); vm.expectRevert( - LibOutputValidation.InputIndexOutOfClaimBounds.selector + abi.encodeWithSelector( + ICartesiDApp.InputIndexOutOfRange.selector, + inputIndex, + proof.inputRange + ) ); executeVoucher(voucher, proof); } // test ether transfer - function testEtherTransfer( - uint256 _dappInitBalance, - uint256 _inputIndex, - uint256 _numInputsAfter - ) public { + function testEtherTransfer(uint256 _dappInitBalance) public { _dappInitBalance = boundBalance(_dappInitBalance); Voucher memory voucher = getVoucher(OutputName.ETHWithdrawalVoucher); - Proof memory proof = setupVoucherProof( - OutputName.ETHWithdrawalVoucher, - _inputIndex, - _numInputsAfter - ); + Proof memory proof = setupVoucherProof(OutputName.ETHWithdrawalVoucher); // not able to execute voucher because dapp has 0 balance assertEq(address(dapp).balance, 0); @@ -485,7 +417,7 @@ contract CartesiDAppTest is TestBase { emit VoucherExecuted( LibOutputValidation.getBitMaskPosition( proof.validity.outputIndexWithinInput, - _inputIndex + _calculateInputIndex(proof) ) ); @@ -576,15 +508,10 @@ contract CartesiDAppTest is TestBase { // test NFT transfer - function testWithdrawNFT( - uint256 _inputIndex, - uint256 _numInputsAfter - ) public { + function testWithdrawNFT() public { Voucher memory voucher = getVoucher(OutputName.ERC721TransferVoucher); Proof memory proof = setupVoucherProof( - OutputName.ERC721TransferVoucher, - _inputIndex, - _numInputsAfter + OutputName.ERC721TransferVoucher ); // not able to execute voucher because dapp doesn't have the nft @@ -609,7 +536,7 @@ contract CartesiDAppTest is TestBase { emit VoucherExecuted( LibOutputValidation.getBitMaskPosition( proof.validity.outputIndexWithinInput, - _inputIndex + _calculateInputIndex(proof) ) ); @@ -942,24 +869,20 @@ contract CartesiDAppTest is TestBase { } function setupNoticeProof( - OutputName _outputName, - uint256 _inputIndex, - uint256 _numInputsAfter + OutputName _outputName ) internal returns (Proof memory) { uint256 inputIndexWithinEpoch = uint256(_outputName); Proof memory proof = getNoticeProof(inputIndexWithinEpoch); - mockConsensus(_inputIndex, _numInputsAfter, proof); + mockConsensus(proof); return proof; } function setupVoucherProof( - OutputName _outputName, - uint256 _inputIndex, - uint256 _numInputsAfter + OutputName _outputName ) internal returns (Proof memory) { uint256 inputIndexWithinEpoch = uint256(_outputName); Proof memory proof = getVoucherProof(inputIndexWithinEpoch); - mockConsensus(_inputIndex, _numInputsAfter, proof); + mockConsensus(proof); return proof; } @@ -999,12 +922,21 @@ contract CartesiDAppTest is TestBase { // Format raw finish epoch response LibServerManager.FinishEpochResponse memory response = raw.fmt(vm); - // Find the proof that proves the provided output + // Get the array of proofs LibServerManager.Proof[] memory proofs = response.proofs; + + // Calculate input range from the array of proofs + InputRange memory inputRange = proofs.getInputRange(); + + // Find the proof that proves the provided output for (uint256 i; i < proofs.length; ++i) { LibServerManager.Proof memory proof = proofs[i]; if (proof.proves(outputEnum, inputIndexWithinEpoch, outputIndex)) { - return convert(proof); + return + Proof({ + validity: convert(proof.validity), + inputRange: inputRange + }); } } @@ -1029,44 +961,32 @@ contract CartesiDAppTest is TestBase { }); } - function convert( - LibServerManager.Proof memory p - ) internal pure returns (Proof memory) { - return Proof({validity: convert(p.validity), context: p.context}); - } - - // Mock consensus so that calls to `getClaim` return - // values that can be used to validate the proof. - function mockConsensus( - uint256 _inputIndex, - uint256 _numInputsAfter, - Proof memory _proof - ) internal { - // check if `_inputIndex` and `_numInputsAfter` are valid - vm.assume(_proof.validity.inputIndexWithinEpoch <= _inputIndex); - vm.assume(_numInputsAfter <= type(uint256).max - _inputIndex); - - // calculate epoch hash from proof - bytes32 epochHash = calculateEpochHash(_proof.validity); - - // calculate input index range based on proof and fuzzy variables - uint256 firstInputIndex = _inputIndex - - _proof.validity.inputIndexWithinEpoch; - uint256 lastInputIndex = _inputIndex + _numInputsAfter; - - // mock the consensus contract to return the right epoch hash + // Mock the consensus contract so that calls to `getEpochHash` return + // the epoch hash to be used to validate the proof. + function mockConsensus(Proof memory _proof) internal { vm.mockCall( address(consensus), - abi.encodeWithSelector( - IConsensus.getClaim.selector, - address(dapp), - _proof.context + abi.encodeCall( + IConsensus.getEpochHash, + (address(dapp), _proof.inputRange) ), - abi.encode(epochHash, firstInputIndex, lastInputIndex) + abi.encode(calculateEpochHash(_proof.validity)) ); } function boundBalance(uint256 _balance) internal view returns (uint256) { return bound(_balance, transferAmount, initialSupply); } + + function calculateInputIndex( + Proof calldata _proof + ) external pure returns (uint256) { + return _proof.calculateInputIndex(); + } + + function _calculateInputIndex( + Proof memory _proof + ) internal view returns (uint256) { + return this.calculateInputIndex(_proof); + } } diff --git a/onchain/rollups/test/foundry/util/LibServerManager.sol b/onchain/rollups/test/foundry/util/LibServerManager.sol index 8bcdd100..a3e2566a 100644 --- a/onchain/rollups/test/foundry/util/LibServerManager.sol +++ b/onchain/rollups/test/foundry/util/LibServerManager.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.8; import {Vm} from "forge-std/Vm.sol"; +import {InputRange} from "contracts/common/InputRange.sol"; library LibServerManager { using LibServerManager for string; @@ -170,6 +171,42 @@ library LibServerManager { Proof[] proofs; } + function getInputRange( + Proof[] memory proofs + ) internal pure returns (InputRange memory) { + return + InputRange({ + firstInputIndex: getFirstInputIndex(proofs), + lastInputIndex: getLastInputIndex(proofs) + }); + } + + function getFirstInputIndex( + Proof[] memory proofs + ) internal pure returns (uint256) { + uint256 first = proofs[0].inputIndex; + for (uint256 i = 1; i < proofs.length; ++i) { + Proof memory proof = proofs[i]; + if (proof.inputIndex < first) { + first = proof.inputIndex; + } + } + return first; + } + + function getLastInputIndex( + Proof[] memory proofs + ) internal pure returns (uint256) { + uint256 last = proofs[0].inputIndex; + for (uint256 i = 1; i < proofs.length; ++i) { + Proof memory proof = proofs[i]; + if (proof.inputIndex > last) { + last = proof.inputIndex; + } + } + return last; + } + function proves( Proof memory p, OutputEnum outputEnum, diff --git a/onchain/rollups/test/foundry/util/SimpleConsensus.sol b/onchain/rollups/test/foundry/util/SimpleConsensus.sol index 2d267b9d..41e30611 100644 --- a/onchain/rollups/test/foundry/util/SimpleConsensus.sol +++ b/onchain/rollups/test/foundry/util/SimpleConsensus.sol @@ -5,10 +5,14 @@ pragma solidity ^0.8.8; import {AbstractConsensus} from "contracts/consensus/AbstractConsensus.sol"; +import {InputRange} from "contracts/common/InputRange.sol"; contract SimpleConsensus is AbstractConsensus { - function getClaim( + function submitClaim( address, - bytes calldata - ) external view returns (bytes32, uint256, uint256) {} + InputRange calldata, + bytes32 + ) external pure override { + revert("SimpleConsensus: cannot submit claim"); + } }