-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Special thanks @ZzzzHui for gas optimizations!
- Loading branch information
Showing
3 changed files
with
233 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@cartesi/rollups": minor | ||
--- | ||
|
||
Added `Quorum` consensus contract |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
onchain/rollups/test/foundry/consensus/quorum/Quorum.t.sol
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
} |