diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dbc0e2a..0c837d14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `abi` directory to the `@cartesi/rollups` package, ideal for language bindings. -- `Quorum`: Consensus reached by the majority of signers +- `Quorum`: Consensus reached by the majority of signers. +- `QuorumFactory`: Allows anyone to deploy `Quorum` contracts. Supports deterministic deployment. ### Removed diff --git a/onchain/.yarn/install-state.gz b/onchain/.yarn/install-state.gz new file mode 100644 index 00000000..b395535b Binary files /dev/null and b/onchain/.yarn/install-state.gz differ diff --git a/onchain/rollups/contracts/consensus/quorum/IQuorumFactory.sol b/onchain/rollups/contracts/consensus/quorum/IQuorumFactory.sol new file mode 100644 index 00000000..8d0b77e7 --- /dev/null +++ b/onchain/rollups/contracts/consensus/quorum/IQuorumFactory.sol @@ -0,0 +1,56 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +import {Quorum} from "./Quorum.sol"; +import {IHistory} from "../../history/IHistory.sol"; + + +/// @title Quorum factory interface +interface IQuorumFactory { + // Events + + /// @notice A new quorum was deployed. + /// @param _quorumValidators The initial set of quorum validators + /// @param quorum The quorum + /// @dev MUST be triggered on a successful call to `newQuorum`. + event QuorumCreated(address[] _quorumValidators, Quorum quorum); + + // Permissionless functions + + /// @notice Deploy a new quorum. + /// @param _quorumValidators The initial set of quorum validators + /// @return The quorum + /// @dev On success, MUST emit an `QuorumCreated` event. + function newQuorum( + address[] calldata _quorumValidators, + uint256[] calldata _shares, + IHistory _history + ) external returns (Quorum); + + /// @notice Deploy a new quorum deterministically. + /// @param _quorumValidators The initial set of quorum validators + /// @param _salt The salt used to deterministically generate the quorum address + /// @return The quorum + /// @dev On success, MUST emit an `QuorumCreated` event. + function newQuorum( + address[] calldata _quorumValidators, + uint256[] calldata _shares, + IHistory _history, + bytes32 _salt + ) external returns (Quorum); + + /// @notice Calculate the address of an quorum to be deployed deterministically. + /// @param _quorumValidators The initial set of quorum validators + /// @param _salt The salt used to deterministically generate the quorum address + /// @return The deterministic quorum address + /// @dev Beware that only the `newQuorum` function with the `_salt` parameter + /// is able to deterministically deploy an quorum. + function calculateQuorumAddress( + address[] calldata _quorumValidators, + uint256[] calldata _shares, + IHistory _history, + bytes32 _salt + ) external view returns (address); +} \ No newline at end of file diff --git a/onchain/rollups/contracts/consensus/quorum/QuorumFactory.sol b/onchain/rollups/contracts/consensus/quorum/QuorumFactory.sol new file mode 100644 index 00000000..807072d0 --- /dev/null +++ b/onchain/rollups/contracts/consensus/quorum/QuorumFactory.sol @@ -0,0 +1,57 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +pragma solidity ^0.8.8; + +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; + +import {IQuorumFactory} from "./IQuorumFactory.sol"; +import {Quorum} from "./Quorum.sol"; +import {IHistory} from "../../history/IHistory.sol"; + +/// @title Quorum factory +/// @notice Allows anyone to reliably deploy a new `Quorum` contract. +contract QuorumFactory is IQuorumFactory { + function newQuorum( + address[] calldata _quorumValidators, + uint256[] calldata _shares, + IHistory _history + ) external override returns (Quorum) { + Quorum quorum = new Quorum(_quorumValidators,_shares,_history); + + emit QuorumCreated(_quorumValidators, quorum); + + return quorum; + } + + function newQuorum( + address[] calldata _quorumValidators, + uint256[] calldata _shares, + IHistory _history, + bytes32 _salt + ) external override returns (Quorum) { + Quorum quorum = new Quorum{salt: _salt}(_quorumValidators, _shares, _history); + + emit QuorumCreated(_quorumValidators, quorum); + + return quorum; + } + + function calculateQuorumAddress( + address[] calldata _quorumValidators, + uint256[] calldata _shares, + IHistory _history, + bytes32 _salt + ) external view override returns (address) { + return + Create2.computeAddress( + _salt, + keccak256( + abi.encodePacked( + type(Quorum).creationCode, + abi.encode(_quorumValidators, _shares, _history) + ) + ) + ); + } +} \ No newline at end of file diff --git a/onchain/rollups/deploy/02_factory.ts b/onchain/rollups/deploy/02_factory.ts index b0e85e20..b09fc930 100644 --- a/onchain/rollups/deploy/02_factory.ts +++ b/onchain/rollups/deploy/02_factory.ts @@ -23,6 +23,7 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { Bitmask, MerkleV2 } = await deployments.all(); + await deployments.deploy("QuorumFactory", opts); await deployments.deploy("CartesiDAppFactory", { ...opts, libraries: { diff --git a/onchain/rollups/test/foundry/consensus/quorum/QuorumFactory.t.sol b/onchain/rollups/test/foundry/consensus/quorum/QuorumFactory.t.sol new file mode 100644 index 00000000..79b5e753 --- /dev/null +++ b/onchain/rollups/test/foundry/consensus/quorum/QuorumFactory.t.sol @@ -0,0 +1,124 @@ +// (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 {QuorumFactory} from "contracts/consensus/quorum/QuorumFactory.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 QuorumFactoryTest is TestBase { + + QuorumFactory factory; + + event QuorumCreated(address[] quorumValidators, Quorum quorum); + + function setUp() public { + factory = new QuorumFactory(); + } + + function testNewQuorum( + uint256 _numValidators + ) public { + vm.assume(_numValidators>1); + vm.assume(_numValidators<50); + + address[] memory quorumValidators = generateValidators(_numValidators); + uint256[] memory shares = generateShares(quorumValidators); + + IHistory history = new History(msg.sender); + + vm.recordLogs(); + + Quorum quorum = factory.newQuorum(quorumValidators, shares, history); + + emit QuorumCreated(quorumValidators, quorum); + + //assertEq(quorum.getHistory() == bytes32(history); + //decodeFactoryLogs(quorumValidators, quorum); + } + + function testNewQuorumDeterministic( + uint256 _numValidators, + bytes32 _salt + ) public { + vm.assume(_numValidators>1); + vm.assume(_numValidators<50); + + address[] memory quorumValidators = generateValidators(_numValidators); + uint256[] memory shares = generateShares(quorumValidators); + + IHistory history = new History(msg.sender); + + address precalculatedAddress = factory.calculateQuorumAddress(quorumValidators, shares, history, _salt); + + vm.recordLogs(); + + Quorum quorum = factory.newQuorum(quorumValidators, shares, history, _salt); + + emit QuorumCreated(quorumValidators, quorum); + + // Precalculated address must match actual address + assertEq(precalculatedAddress, address(quorum)); + } + + function testAlreadyDeployedNewQuorumDeterministic( + uint256 _numValidators, + bytes32 _salt + ) public { + vm.assume(_numValidators>1); + vm.assume(_numValidators<50); + + address[] memory quorumValidators = generateValidators(_numValidators); + uint256[] memory shares = generateShares(quorumValidators); + + IHistory history = new History(msg.sender); + + factory.newQuorum(quorumValidators, shares, history, _salt); + + //Deploy already deployed quorum + vm.expectRevert(); + factory.newQuorum(quorumValidators, shares, history, _salt); + } + + // HELPER FUNCTIONS + function generateValidators(uint256 _numValidators) internal returns(address[] memory){ + address[] memory validators = new address[](_numValidators); + for (uint256 i = 0; i < _numValidators; i++) { + validators[i] = vm.addr(i+1); + } + return validators; + } + + function generateShares(address[] memory validators) internal returns(uint256[] memory){ + //generate a random number of shares for each validator + uint256[] memory shares = new uint256[](validators.length); + for (uint256 i; i < shares.length; ++i) { + uint256 share = uint256( + keccak256(abi.encodePacked(i, validators[i]))) % 100; + shares[i] = (share > 0) ? share : validators.length; + } + return shares; + } + + /*function decodeFactoryLogs(address[] memory _quorumValidators, Quorum quorum) internal { + Vm.Log[] memory entries = vm.getRecordedLogs(); + + address[] memory a; + address b; + + (a, b) = abi.decode(entries[0].data, (address[], address)); + + assertEq(_quorumValidators, a); //entry.emitter == address(factory) + assertEq(address(quorum), b); + }*/ +} + +