diff --git a/onchain/rollups/.changeset/lovely-carpets-change.md b/onchain/rollups/.changeset/lovely-carpets-change.md new file mode 100644 index 00000000..0c27fd50 --- /dev/null +++ b/onchain/rollups/.changeset/lovely-carpets-change.md @@ -0,0 +1,5 @@ +--- +"@cartesi/rollups": minor +--- + +Added `Quorum` consensus contract diff --git a/onchain/rollups/contracts/consensus/quorum/Quorum.sol b/onchain/rollups/contracts/consensus/quorum/Quorum.sol new file mode 100644 index 00000000..81349ed5 --- /dev/null +++ b/onchain/rollups/contracts/consensus/quorum/Quorum.sol @@ -0,0 +1,180 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +import {PaymentSplitter} from "@openzeppelin/contracts/finance/PaymentSplitter.sol"; +import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; + +import {AbstractConsensus} from "../AbstractConsensus.sol"; +import {IConsensus} from "../IConsensus.sol"; +import {IHistory} from "../../history/IHistory.sol"; + +/// @title Quorum consensus +/// @notice A consensus model controlled by a small set of addresses, the validators. +/// In this version, the validator set is immutable. +/// Claims are stored in an auxiliary contract called history. +/// @dev Each validator is assigned an identifier that spans from 1 to N, +/// where N is the total number of validators in the quorum. +/// These identifiers are used internally instead of addresses for optimization reasons. +/// This contract uses OpenZeppelin `PaymentSplitter` and `BitMaps`. +/// For more information on those, please consult OpenZeppelin's official documentation. +contract Quorum is AbstractConsensus, PaymentSplitter { + using BitMaps for BitMaps.BitMap; + + /// @notice Get the total number of validators. + uint256 public immutable numOfValidators; + + /// @notice Get the ID of a validator by its address. + /// @dev Only validators have non-zero IDs. + mapping(address => uint256) public validatorId; + + /// @notice Get the address of a validator by its ID. + /// @dev Validator IDs span from 1 to the total number of validators. + /// Invalid IDs are assigned to the zero address. + mapping(uint256 => address) public validatorById; + + /// @notice Voting status of a particular claim. + /// @param inFavorCount the number of validators in favor of the claim + /// @param inFavorById the IDs of validators in favor of the claim in bitmap format + struct VotingStatus { + uint256 inFavorCount; + BitMaps.BitMap inFavorById; + } + + /// @notice The voting status of each claim. + mapping(bytes => VotingStatus) internal votingStatuses; + + /// @notice The history contract. + /// @dev See the `getHistory` function. + IHistory internal immutable history; + + /// @notice Construct a Quorum consensus + /// @param _validators the list of validators + /// @param _shares the list of shares + /// @param _history the history contract + /// @dev PaymentSplitter checks for duplicates in _validators + constructor( + address[] memory _validators, + uint256[] memory _shares, + IHistory _history + ) PaymentSplitter(_validators, _shares) { + numOfValidators = _validators.length; + + uint256 id = 1; + for (uint256 i; i < _validators.length; ++i) { + address validator = _validators[i]; + validatorId[validator] = id; + validatorById[id] = validator; + ++id; + } + + history = _history; + } + + /// @notice Vote for a claim to be submitted. + /// If this is the claim that reaches the majority, then + /// the claim is submitted to the 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 a validator, + /// and the `Quorum` contract must have ownership over + /// its current history contract. + function submitClaim(bytes calldata _claimData) external { + uint256 id = validatorId[msg.sender]; + require(id != 0, "Quorum: sender is not validator"); + + VotingStatus storage votingStatus = votingStatuses[_claimData]; + BitMaps.BitMap storage inFavorById = votingStatus.inFavorById; + + if (!inFavorById.get(id)) { + // If validator hasn't voted yet, cast their vote + inFavorById.set(id); + + // If this claim has now just over half of the quorum's votes, + // then we can submit it to the history contract. + if (++votingStatus.inFavorCount == 1 + numOfValidators / 2) { + history.submitClaim(_claimData); + } + } + } + + /// @notice Get an array with the addresses of all validators. + /// @return Array of addresses of validators + function validators() external view returns (address[] memory) { + address[] memory array = new address[](numOfValidators); + + uint256 id = 1; + for (uint256 i; i < numOfValidators; ++i) { + array[i] = validatorById[id]; + ++id; + } + + return array; + } + + /// @notice Get the number of validator in favor of a claim. + /// @param _claimData Data for submitting a claim + /// @return Number of validator in favor of claim. + function numOfValidatorsInFavorOf( + bytes calldata _claimData + ) external view returns (uint256) { + VotingStatus storage votingStatus = votingStatuses[_claimData]; + return votingStatus.inFavorCount; + } + + /// @notice Check whether a validator is in favor of a claim. + /// @param _validatorId The ID of the validator + /// @param _claimData Data for submitting a claim + /// @return Whether validator is in favor of claim + /// @dev Assumes the provided ID is valid + function isValidatorInFavorOf( + uint256 _validatorId, + bytes calldata _claimData + ) external view returns (bool) { + VotingStatus storage votingStatus = votingStatuses[_claimData]; + BitMaps.BitMap storage inFavorById = votingStatus.inFavorById; + return inFavorById.get(_validatorId); + } + + /// @notice Get an array with the addresses of all validators in favor of a claim. + /// @param _claimData Data for submitting a claim + /// @return Array of addresses of validators in favor of claim + function validatorsInFavorOf( + bytes calldata _claimData + ) external view returns (address[] memory) { + VotingStatus storage votingStatus = votingStatuses[_claimData]; + BitMaps.BitMap storage inFavorById = votingStatus.inFavorById; + + uint256 validatorsLeft = votingStatus.inFavorCount; + address[] memory array = new address[](validatorsLeft); + + uint256 id = 1; + while (validatorsLeft > 0) { + if (inFavorById.get(id)) { + array[--validatorsLeft] = validatorById[id]; + } + ++id; + } + + return array; + } + + /// @notice Get the history contract. + /// @return The 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); + } +} diff --git a/onchain/rollups/test/foundry/consensus/quorum/Quorum.t.sol b/onchain/rollups/test/foundry/consensus/quorum/Quorum.t.sol new file mode 100644 index 00000000..9c1b0e2f --- /dev/null +++ b/onchain/rollups/test/foundry/consensus/quorum/Quorum.t.sol @@ -0,0 +1,48 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +import {Vm} from "forge-std/Vm.sol"; + +import {Quorum} from "contracts/consensus/quorum/Quorum.sol"; +import {History} from "contracts/history/History.sol"; +import {IHistory} from "contracts/history/IHistory.sol"; + +import {TestBase} from "../../util/TestBase.sol"; + +import "forge-std/console.sol"; + +contract QuorumTest is TestBase { + Quorum quorum; + uint256 constant numOfValidators = 3; + address[] validators; + uint256[] shares; + + function setUp() external { + for (uint256 i; i < numOfValidators; ++i) { + validators.push(vm.addr(i + 1)); + shares.push(i + 1); + } + + quorum = new Quorum(validators, shares, IHistory(vm.addr(1))); + } + + function testSubmitClaim( + address _dapp, + History.Claim calldata _claim + ) external { + bytes memory claimData = abi.encode(_dapp, _claim); + + vm.mockCall( + address(quorum.getHistory()), + abi.encodeWithSelector(IHistory.submitClaim.selector, claimData), + abi.encode() + ); + + for (uint256 i; i < numOfValidators; ++i) { + vm.prank(validators[i]); + quorum.submitClaim(claimData); + } + } +}