From f8c25e9fd22c971e395c62ff56befe26405c13d2 Mon Sep 17 00:00:00 2001 From: Guilherme Dantas Date: Fri, 26 Jul 2024 13:22:09 -0300 Subject: [PATCH 1/5] feat!: add last processed block number --- .changeset/little-cheetahs-jump.md | 5 +++ contracts/consensus/AbstractConsensus.sol | 9 +++- contracts/consensus/IConsensus.sol | 38 ++++++++++------ contracts/consensus/authority/Authority.sol | 11 ++++- contracts/consensus/quorum/Quorum.sol | 45 +++++++++++++++---- .../consensus/authority/Authority.t.sol | 28 +++++++++--- test/foundry/consensus/quorum/Quorum.t.sol | 33 ++++++++++---- test/foundry/dapp/Application.t.sol | 2 +- 8 files changed, 130 insertions(+), 41 deletions(-) create mode 100644 .changeset/little-cheetahs-jump.md diff --git a/.changeset/little-cheetahs-jump.md b/.changeset/little-cheetahs-jump.md new file mode 100644 index 00000000..55de232e --- /dev/null +++ b/.changeset/little-cheetahs-jump.md @@ -0,0 +1,5 @@ +--- +"@cartesi/rollups": major +--- + +Added a `lastProcessedBlockNumber` parameter to `IConsensus` functions and events. diff --git a/contracts/consensus/AbstractConsensus.sol b/contracts/consensus/AbstractConsensus.sol index 050b4f33..5eeea7b2 100644 --- a/contracts/consensus/AbstractConsensus.sol +++ b/contracts/consensus/AbstractConsensus.sol @@ -25,10 +25,15 @@ abstract contract AbstractConsensus is IConsensus { /// @notice Accept a claim. /// @param appContract The application contract address + /// @param lastProcessedBlockNumber The number of the last processed block /// @param claim The output Merkle root hash /// @dev Emits a `ClaimAcceptance` event. - function _acceptClaim(address appContract, bytes32 claim) internal { + function _acceptClaim( + address appContract, + uint256 lastProcessedBlockNumber, + bytes32 claim + ) internal { _acceptedClaims[appContract][claim] = true; - emit ClaimAcceptance(appContract, claim); + emit ClaimAcceptance(appContract, lastProcessedBlockNumber, claim); } } diff --git a/contracts/consensus/IConsensus.sol b/contracts/consensus/IConsensus.sol index 826a2d15..d7fa5c9e 100644 --- a/contracts/consensus/IConsensus.sol +++ b/contracts/consensus/IConsensus.sol @@ -3,12 +3,13 @@ pragma solidity ^0.8.8; -/// @notice Provides consensus over the set of valid output Merkle root hashes for applications. -/// @notice The latest output Merkle root hash is available after the machine processes every input. -/// This hash can be later used to prove that any given output was ever produced by the machine. -/// @notice After an epoch is finalized, a validator may submit a claim containing the application contract address, -/// and the output Merkle root hash. -/// @notice Validators may synchronize epoch finalization, but such mechanism is not specified by this interface. +/// @notice Each application has its own stream of inputs. +/// See the `IInputBox` interface for calldata-based on-chain data availability. +/// @notice When an input is fed to the application, it may yield several outputs. +/// @notice Since genesis, a Merkle tree of all outputs ever produced is maintained +/// both inside and outside the Cartesi Machine. +/// @notice The claim that validators may submit to the consensus contract +/// is the root of this Merkle tree after processing all base layer blocks until some height. /// @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). @@ -22,30 +23,41 @@ interface IConsensus { /// @notice MUST trigger when a claim is submitted. /// @param submitter The submitter address /// @param appContract The application contract address - /// @param claim The output Merkle root hash + /// @param lastProcessedBlockNumber The number of the last processed block + /// @param claim The root of the Merkle tree of outputs event ClaimSubmission( address indexed submitter, address indexed appContract, + uint256 lastProcessedBlockNumber, bytes32 claim ); /// @notice MUST trigger when a claim is accepted. /// @param appContract The application contract address - /// @param claim The output Merkle root hash - /// @dev MUST be triggered after some `ClaimSubmission` event regarding `appContract`. - event ClaimAcceptance(address indexed appContract, bytes32 claim); + /// @param lastProcessedBlockNumber The number of the last processed block + /// @param claim The root of the Merkle tree of outputs + event ClaimAcceptance( + address indexed appContract, + uint256 lastProcessedBlockNumber, + bytes32 claim + ); /// @notice Submit a claim to the consensus. /// @param appContract The application contract address - /// @param claim The output Merkle root hash + /// @param lastProcessedBlockNumber The number of the last processed block + /// @param claim The root of the Merkle tree of outputs /// @dev MUST fire a `ClaimSubmission` event. /// @dev MAY fire a `ClaimAcceptance` event, if the acceptance criteria is met. - function submitClaim(address appContract, bytes32 claim) external; + function submitClaim( + address appContract, + uint256 lastProcessedBlockNumber, + bytes32 claim + ) external; /// @notice Check if an output Merkle root hash was ever accepted by the consensus /// for a particular application. /// @param appContract The application contract address - /// @param claim The output Merkle root hash + /// @param claim The root of the Merkle tree of outputs function wasClaimAccepted( address appContract, bytes32 claim diff --git a/contracts/consensus/authority/Authority.sol b/contracts/consensus/authority/Authority.sol index 78b0bc25..2aa25afe 100644 --- a/contracts/consensus/authority/Authority.sol +++ b/contracts/consensus/authority/Authority.sol @@ -17,14 +17,21 @@ contract Authority is AbstractConsensus, Ownable { /// @notice Submit a claim. /// @param appContract The application contract address + /// @param lastProcessedBlockNumber The number of the last processed block /// @param claim The output Merkle root hash /// @dev Fires a `ClaimSubmission` event and a `ClaimAcceptance` event. /// @dev Can only be called by the owner. function submitClaim( address appContract, + uint256 lastProcessedBlockNumber, bytes32 claim ) external onlyOwner { - emit ClaimSubmission(msg.sender, appContract, claim); - _acceptClaim(appContract, claim); + emit ClaimSubmission( + msg.sender, + appContract, + lastProcessedBlockNumber, + claim + ); + _acceptClaim(appContract, lastProcessedBlockNumber, claim); } } diff --git a/contracts/consensus/quorum/Quorum.sol b/contracts/consensus/quorum/Quorum.sol index 2a717850..b1626cf3 100644 --- a/contracts/consensus/quorum/Quorum.sol +++ b/contracts/consensus/quorum/Quorum.sol @@ -40,9 +40,11 @@ contract Quorum is AbstractConsensus { BitMaps.BitMap inFavorById; } - /// @notice Votes indexed by application contract address and claim. + /// @notice Votes indexed by application contract address, + /// last processed block number and claim. /// @dev See the `numOfValidatorsInFavorOf` and `isValidatorInFavorOf` functions. - mapping(address => mapping(bytes32 => Votes)) private _votes; + mapping(address => mapping(uint256 => mapping(bytes32 => Votes))) + private _votes; /// @param validators The array of validator addresses /// @dev Duplicates in the `validators` array are ignored. @@ -61,22 +63,36 @@ contract Quorum is AbstractConsensus { /// @notice Submit a claim. /// @param appContract The application contract address + /// @param lastProcessedBlockNumber The number of the last processed block /// @param claim The output Merkle root hash /// @dev Fires a `ClaimSubmission` event if the message sender is a validator. /// @dev Fires a `ClaimAcceptance` event if the claim reaches a majority. /// @dev Can only be called by a validator. - function submitClaim(address appContract, bytes32 claim) external { + function submitClaim( + address appContract, + uint256 lastProcessedBlockNumber, + bytes32 claim + ) external { uint256 id = _validatorId[msg.sender]; require(id > 0, "Quorum: caller is not validator"); - emit ClaimSubmission(msg.sender, appContract, claim); + emit ClaimSubmission( + msg.sender, + appContract, + lastProcessedBlockNumber, + claim + ); - Votes storage votes = _getVotes(appContract, claim); + Votes storage votes = _getVotes( + appContract, + lastProcessedBlockNumber, + claim + ); if (!votes.inFavorById.get(id)) { votes.inFavorById.set(id); if (++votes.inFavorCount == 1 + _numOfValidators / 2) { - _acceptClaim(appContract, claim); + _acceptClaim(appContract, lastProcessedBlockNumber, claim); } } } @@ -104,37 +120,48 @@ contract Quorum is AbstractConsensus { /// @notice Get the number of validators in favor of a claim. /// @param appContract The application contract address + /// @param lastProcessedBlockNumber The number of the last processed block /// @param claim The output Merkle root hash /// @return Number of validators in favor of claim function numOfValidatorsInFavorOf( address appContract, + uint256 lastProcessedBlockNumber, bytes32 claim ) external view returns (uint256) { - return _getVotes(appContract, claim).inFavorCount; + return + _getVotes(appContract, lastProcessedBlockNumber, claim) + .inFavorCount; } /// @notice Check whether a validator is in favor of a claim. /// @param appContract The application contract address + /// @param lastProcessedBlockNumber The number of the last processed block /// @param claim The output Merkle root hash /// @param id The ID of the validator /// @return Whether validator is in favor of claim /// @dev Assumes the provided ID is valid. function isValidatorInFavorOf( address appContract, + uint256 lastProcessedBlockNumber, bytes32 claim, uint256 id ) external view returns (bool) { - return _getVotes(appContract, claim).inFavorById.get(id); + return + _getVotes(appContract, lastProcessedBlockNumber, claim) + .inFavorById + .get(id); } /// @notice Get a `Votes` structure from storage from a given claim. /// @param appContract The application contract address + /// @param lastProcessedBlockNumber The number of the last processed block /// @param claim The output Merkle root hash /// @return The `Votes` structure related to a given claim function _getVotes( address appContract, + uint256 lastProcessedBlockNumber, bytes32 claim ) internal view returns (Votes storage) { - return _votes[appContract][claim]; + return _votes[appContract][lastProcessedBlockNumber][claim]; } } diff --git a/test/foundry/consensus/authority/Authority.t.sol b/test/foundry/consensus/authority/Authority.t.sol index 397299f2..97ba6107 100644 --- a/test/foundry/consensus/authority/Authority.t.sol +++ b/test/foundry/consensus/authority/Authority.t.sol @@ -61,6 +61,7 @@ contract AuthorityTest is TestBase { address owner, address notOwner, address appContract, + uint256 lastProcessedBlockNumber, bytes32 claim ) public { vm.assume(owner != address(0)); @@ -76,22 +77,29 @@ contract AuthorityTest is TestBase { ); vm.prank(notOwner); - authority.submitClaim(appContract, claim); + authority.submitClaim(appContract, lastProcessedBlockNumber, claim); } function testSubmitClaim( address owner, address appContract, + uint256 lastProcessedBlockNumber, bytes32 claim ) public { vm.assume(owner != address(0)); Authority authority = new Authority(owner); - _expectClaimEvents(authority, owner, appContract, claim); + _expectClaimEvents( + authority, + owner, + appContract, + lastProcessedBlockNumber, + claim + ); vm.prank(owner); - authority.submitClaim(appContract, claim); + authority.submitClaim(appContract, lastProcessedBlockNumber, claim); assertTrue(authority.wasClaimAccepted(appContract, claim)); } @@ -112,12 +120,22 @@ contract AuthorityTest is TestBase { Authority authority, address owner, address appContract, + uint256 lastProcessedBlockNumber, bytes32 claim ) internal { vm.expectEmit(true, true, false, true, address(authority)); - emit IConsensus.ClaimSubmission(owner, appContract, claim); + emit IConsensus.ClaimSubmission( + owner, + appContract, + lastProcessedBlockNumber, + claim + ); vm.expectEmit(true, false, false, true, address(authority)); - emit IConsensus.ClaimAcceptance(appContract, claim); + emit IConsensus.ClaimAcceptance( + appContract, + lastProcessedBlockNumber, + claim + ); } } diff --git a/test/foundry/consensus/quorum/Quorum.t.sol b/test/foundry/consensus/quorum/Quorum.t.sol index 4d8fa454..bd50ae2b 100644 --- a/test/foundry/consensus/quorum/Quorum.t.sol +++ b/test/foundry/consensus/quorum/Quorum.t.sol @@ -13,6 +13,7 @@ import {Vm} from "forge-std/Vm.sol"; struct Claim { address appContract; + uint256 lastProcessedBlockNumber; bytes32 outputHashesRootHash; } @@ -24,6 +25,7 @@ library LibQuorum { return quorum.numOfValidatorsInFavorOf( claim.appContract, + claim.lastProcessedBlockNumber, claim.outputHashesRootHash ); } @@ -36,13 +38,18 @@ library LibQuorum { return quorum.isValidatorInFavorOf( claim.appContract, + claim.lastProcessedBlockNumber, claim.outputHashesRootHash, id ); } function submitClaim(Quorum quorum, Claim calldata claim) internal { - quorum.submitClaim(claim.appContract, claim.outputHashesRootHash); + quorum.submitClaim( + claim.appContract, + claim.lastProcessedBlockNumber, + claim.outputHashesRootHash + ); } function wasClaimAccepted( @@ -249,13 +256,17 @@ contract QuorumTest is TestBase { entry.emitter == address(quorum) && entry.topics[0] == IConsensus.ClaimSubmission.selector ) { - bytes32 outputHashesRootHash = abi.decode( - entry.data, - (bytes32) - ); + ( + uint256 lastProcessedBlockNumber, + bytes32 outputHashesRootHash + ) = abi.decode(entry.data, (uint256, bytes32)); assertEq(entry.topics[1], validator.asTopic()); assertEq(entry.topics[2], claim.appContract.asTopic()); + assertEq( + lastProcessedBlockNumber, + claim.lastProcessedBlockNumber + ); assertEq(outputHashesRootHash, claim.outputHashesRootHash); ++numOfSubmissions; @@ -265,12 +276,16 @@ contract QuorumTest is TestBase { entry.emitter == address(quorum) && entry.topics[0] == IConsensus.ClaimAcceptance.selector ) { - bytes32 outputHashesRootHash = abi.decode( - entry.data, - (bytes32) - ); + ( + uint256 lastProcessedBlockNumber, + bytes32 outputHashesRootHash + ) = abi.decode(entry.data, (uint256, bytes32)); assertEq(entry.topics[1], claim.appContract.asTopic()); + assertEq( + lastProcessedBlockNumber, + claim.lastProcessedBlockNumber + ); assertEq(outputHashesRootHash, claim.outputHashesRootHash); ++numOfAcceptances; diff --git a/test/foundry/dapp/Application.t.sol b/test/foundry/dapp/Application.t.sol index 930e4cc2..ff3bda09 100644 --- a/test/foundry/dapp/Application.t.sol +++ b/test/foundry/dapp/Application.t.sol @@ -487,7 +487,7 @@ contract ApplicationTest is TestBase { function _submitClaim() internal { bytes32 claim = _emulator.getClaim(); vm.prank(_authorityOwner); - _consensus.submitClaim(address(_appContract), claim); + _consensus.submitClaim(address(_appContract), 0, claim); } function _expectEmitOutputExecuted( From fa4193157c11cee82b7521fef63e3251f265c030 Mon Sep 17 00:00:00 2001 From: Guilherme Dantas Date: Wed, 7 Aug 2024 11:57:05 -0300 Subject: [PATCH 2/5] chore: inherit doc --- contracts/consensus/AbstractConsensus.sol | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/consensus/AbstractConsensus.sol b/contracts/consensus/AbstractConsensus.sol index 5eeea7b2..a4ead2fc 100644 --- a/contracts/consensus/AbstractConsensus.sol +++ b/contracts/consensus/AbstractConsensus.sol @@ -12,10 +12,7 @@ abstract contract AbstractConsensus is IConsensus { /// @notice Indexes accepted claims by application contract address. mapping(address => mapping(bytes32 => bool)) private _acceptedClaims; - /// @notice Check if an output Merkle root hash was ever accepted by the consensus - /// for a particular application. - /// @param appContract The application contract address - /// @param claim The output Merkle root hash + /// @inheritdoc IConsensus function wasClaimAccepted( address appContract, bytes32 claim From 3d4089059a97bb408945721e0222e46098f276f5 Mon Sep 17 00:00:00 2001 From: Guilherme Dantas Date: Wed, 7 Aug 2024 11:58:47 -0300 Subject: [PATCH 3/5] refactor!: remove `authorityOwner` from `AuthorityCreated` --- .changeset/pink-teachers-whisper.md | 5 +++++ contracts/consensus/authority/AuthorityFactory.sol | 4 ++-- contracts/consensus/authority/IAuthorityFactory.sol | 3 +-- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 .changeset/pink-teachers-whisper.md diff --git a/.changeset/pink-teachers-whisper.md b/.changeset/pink-teachers-whisper.md new file mode 100644 index 00000000..0a72e8f8 --- /dev/null +++ b/.changeset/pink-teachers-whisper.md @@ -0,0 +1,5 @@ +--- +"@cartesi/rollups": major +--- + +Removed `authorityOwner` parameter from `AuthorityCreated` event. diff --git a/contracts/consensus/authority/AuthorityFactory.sol b/contracts/consensus/authority/AuthorityFactory.sol index c512d7af..74de4e26 100644 --- a/contracts/consensus/authority/AuthorityFactory.sol +++ b/contracts/consensus/authority/AuthorityFactory.sol @@ -16,7 +16,7 @@ contract AuthorityFactory is IAuthorityFactory { ) external override returns (Authority) { Authority authority = new Authority(authorityOwner); - emit AuthorityCreated(authorityOwner, authority); + emit AuthorityCreated(authority); return authority; } @@ -27,7 +27,7 @@ contract AuthorityFactory is IAuthorityFactory { ) external override returns (Authority) { Authority authority = new Authority{salt: salt}(authorityOwner); - emit AuthorityCreated(authorityOwner, authority); + emit AuthorityCreated(authority); return authority; } diff --git a/contracts/consensus/authority/IAuthorityFactory.sol b/contracts/consensus/authority/IAuthorityFactory.sol index d0c0d49b..32cdb4e6 100644 --- a/contracts/consensus/authority/IAuthorityFactory.sol +++ b/contracts/consensus/authority/IAuthorityFactory.sol @@ -10,10 +10,9 @@ interface IAuthorityFactory { // Events /// @notice A new authority was deployed. - /// @param authorityOwner The initial authority owner /// @param authority The authority /// @dev MUST be triggered on a successful call to `newAuthority`. - event AuthorityCreated(address authorityOwner, Authority authority); + event AuthorityCreated(Authority authority); // Permissionless functions From 73fbcea3eee7b3e543d9c8382415bfa864373264 Mon Sep 17 00:00:00 2001 From: Guilherme Dantas Date: Wed, 7 Aug 2024 12:02:27 -0300 Subject: [PATCH 4/5] docs: add documentation about constructor parameters --- contracts/consensus/authority/IAuthorityFactory.sol | 2 ++ contracts/consensus/quorum/IQuorumFactory.sol | 2 ++ contracts/dapp/Application.sol | 1 + contracts/dapp/IApplicationFactory.sol | 2 ++ contracts/dapp/ISelfHostedApplicationFactory.sol | 2 ++ 5 files changed, 9 insertions(+) diff --git a/contracts/consensus/authority/IAuthorityFactory.sol b/contracts/consensus/authority/IAuthorityFactory.sol index 32cdb4e6..7cf3d719 100644 --- a/contracts/consensus/authority/IAuthorityFactory.sol +++ b/contracts/consensus/authority/IAuthorityFactory.sol @@ -20,6 +20,7 @@ interface IAuthorityFactory { /// @param authorityOwner The initial authority owner /// @return The authority /// @dev On success, MUST emit an `AuthorityCreated` event. + /// @dev Reverts if the authority owner address is zero. function newAuthority(address authorityOwner) external returns (Authority); /// @notice Deploy a new authority deterministically. @@ -27,6 +28,7 @@ interface IAuthorityFactory { /// @param salt The salt used to deterministically generate the authority address /// @return The authority /// @dev On success, MUST emit an `AuthorityCreated` event. + /// @dev Reverts if the authority owner address is zero. function newAuthority( address authorityOwner, bytes32 salt diff --git a/contracts/consensus/quorum/IQuorumFactory.sol b/contracts/consensus/quorum/IQuorumFactory.sol index aee21d2a..c1f4ce47 100644 --- a/contracts/consensus/quorum/IQuorumFactory.sol +++ b/contracts/consensus/quorum/IQuorumFactory.sol @@ -20,6 +20,7 @@ interface IQuorumFactory { /// @param validators the list of validators /// @return The quorum /// @dev On success, MUST emit a `QuorumCreated` event. + /// @dev Duplicates in the `validators` array are ignored. function newQuorum(address[] calldata validators) external returns (Quorum); /// @notice Deploy a new quorum deterministically. @@ -27,6 +28,7 @@ interface IQuorumFactory { /// @param salt The salt used to deterministically generate the quorum address /// @return The quorum /// @dev On success, MUST emit a `QuorumCreated` event. + /// @dev Duplicates in the `validators` array are ignored. function newQuorum( address[] calldata validators, bytes32 salt diff --git a/contracts/dapp/Application.sol b/contracts/dapp/Application.sol index f7cb1956..8c171fa9 100644 --- a/contracts/dapp/Application.sol +++ b/contracts/dapp/Application.sol @@ -44,6 +44,7 @@ contract Application is /// @param consensus The initial consensus contract /// @param initialOwner The initial application owner /// @param templateHash The initial machine state hash + /// @dev Reverts if the initial application owner address is zero. constructor( IConsensus consensus, address initialOwner, diff --git a/contracts/dapp/IApplicationFactory.sol b/contracts/dapp/IApplicationFactory.sol index 637bb172..d4cc5c16 100644 --- a/contracts/dapp/IApplicationFactory.sol +++ b/contracts/dapp/IApplicationFactory.sol @@ -31,6 +31,7 @@ interface IApplicationFactory { /// @param templateHash The initial machine state hash /// @return The application /// @dev On success, MUST emit an `ApplicationCreated` event. + /// @dev Reverts if the application owner address is zero. function newApplication( IConsensus consensus, address appOwner, @@ -44,6 +45,7 @@ interface IApplicationFactory { /// @param salt The salt used to deterministically generate the application contract address /// @return The application /// @dev On success, MUST emit an `ApplicationCreated` event. + /// @dev Reverts if the application owner address is zero. function newApplication( IConsensus consensus, address appOwner, diff --git a/contracts/dapp/ISelfHostedApplicationFactory.sol b/contracts/dapp/ISelfHostedApplicationFactory.sol index 02f1486e..cda7a055 100644 --- a/contracts/dapp/ISelfHostedApplicationFactory.sol +++ b/contracts/dapp/ISelfHostedApplicationFactory.sol @@ -28,6 +28,8 @@ interface ISelfHostedApplicationFactory { /// @param salt The salt used to deterministically generate the addresses /// @return The application contract /// @return The authority contract + /// @dev Reverts if the authority owner address is zero. + /// @dev Reverts if the application owner address is zero. function deployContracts( address authorityOwner, address appOwner, From 7f27379533670295fbc2e073adcae8a84c8cb14f Mon Sep 17 00:00:00 2001 From: Guilherme Dantas Date: Wed, 7 Aug 2024 12:21:37 -0300 Subject: [PATCH 5/5] feat!: add epoch length to consensus contracts --- .changeset/popular-ghosts-fix.md | 9 +++ .changeset/six-schools-live.md | 5 ++ contracts/consensus/AbstractConsensus.sol | 15 ++++ contracts/consensus/IConsensus.sol | 5 ++ contracts/consensus/authority/Authority.sol | 7 +- .../consensus/authority/AuthorityFactory.sol | 14 +++- .../consensus/authority/IAuthorityFactory.sol | 12 ++- contracts/consensus/quorum/IQuorumFactory.sol | 12 ++- contracts/consensus/quorum/Quorum.sol | 7 +- contracts/consensus/quorum/QuorumFactory.sol | 11 ++- .../dapp/ISelfHostedApplicationFactory.sol | 5 ++ .../dapp/SelfHostedApplicationFactory.sol | 9 ++- .../consensus/authority/Authority.t.sol | 31 +++++-- .../authority/AuthorityFactory.t.sol | 81 +++++++++++++++---- test/foundry/consensus/quorum/Quorum.t.sol | 77 +++++++++++++----- .../consensus/quorum/QuorumFactory.t.sol | 38 +++++++-- test/foundry/dapp/Application.t.sol | 3 +- .../dapp/SelfHostedApplicationFactory.t.sol | 75 +++++++++++++++++ 18 files changed, 351 insertions(+), 65 deletions(-) create mode 100644 .changeset/popular-ghosts-fix.md create mode 100644 .changeset/six-schools-live.md diff --git a/.changeset/popular-ghosts-fix.md b/.changeset/popular-ghosts-fix.md new file mode 100644 index 00000000..0b0e6bd0 --- /dev/null +++ b/.changeset/popular-ghosts-fix.md @@ -0,0 +1,9 @@ +--- +"@cartesi/rollups": major +--- + +Added an `epochLength` parameter to functions of: + +- `IAuthorityFactory` +- `ISelfHostedApplicationFactory` +- `IQuorumFactory` diff --git a/.changeset/six-schools-live.md b/.changeset/six-schools-live.md new file mode 100644 index 00000000..1d452e53 --- /dev/null +++ b/.changeset/six-schools-live.md @@ -0,0 +1,5 @@ +--- +"@cartesi/rollups": minor +--- + +Added a `getEpochLength` function to `IConsensus` interface. diff --git a/contracts/consensus/AbstractConsensus.sol b/contracts/consensus/AbstractConsensus.sol index a4ead2fc..b854af4d 100644 --- a/contracts/consensus/AbstractConsensus.sol +++ b/contracts/consensus/AbstractConsensus.sol @@ -9,9 +9,19 @@ import {IConsensus} from "./IConsensus.sol"; /// @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 accepted claims. abstract contract AbstractConsensus is IConsensus { + /// @notice The epoch length + uint256 private immutable _epochLength; + /// @notice Indexes accepted claims by application contract address. mapping(address => mapping(bytes32 => bool)) private _acceptedClaims; + /// @param epochLength The epoch length + /// @dev Reverts if the epoch length is zero. + constructor(uint256 epochLength) { + require(epochLength > 0, "epoch length must not be zero"); + _epochLength = epochLength; + } + /// @inheritdoc IConsensus function wasClaimAccepted( address appContract, @@ -20,6 +30,11 @@ abstract contract AbstractConsensus is IConsensus { return _acceptedClaims[appContract][claim]; } + /// @inheritdoc IConsensus + function getEpochLength() public view override returns (uint256) { + return _epochLength; + } + /// @notice Accept a claim. /// @param appContract The application contract address /// @param lastProcessedBlockNumber The number of the last processed block diff --git a/contracts/consensus/IConsensus.sol b/contracts/consensus/IConsensus.sol index d7fa5c9e..6313f2dd 100644 --- a/contracts/consensus/IConsensus.sol +++ b/contracts/consensus/IConsensus.sol @@ -62,4 +62,9 @@ interface IConsensus { address appContract, bytes32 claim ) external view returns (bool); + + /// @notice Get the epoch length, in number of base layer blocks. + /// @dev The epoch number of a block is defined as + /// the integer division of the block number by the epoch length. + function getEpochLength() external view returns (uint256); } diff --git a/contracts/consensus/authority/Authority.sol b/contracts/consensus/authority/Authority.sol index 2aa25afe..ab8fccb0 100644 --- a/contracts/consensus/authority/Authority.sol +++ b/contracts/consensus/authority/Authority.sol @@ -13,7 +13,12 @@ import {AbstractConsensus} from "../AbstractConsensus.sol"; /// For more information on `Ownable`, please consult OpenZeppelin's official documentation. contract Authority is AbstractConsensus, Ownable { /// @param initialOwner The initial contract owner - constructor(address initialOwner) Ownable(initialOwner) {} + /// @param epochLength The epoch length + /// @dev Reverts if the epoch length is zero. + constructor( + address initialOwner, + uint256 epochLength + ) AbstractConsensus(epochLength) Ownable(initialOwner) {} /// @notice Submit a claim. /// @param appContract The application contract address diff --git a/contracts/consensus/authority/AuthorityFactory.sol b/contracts/consensus/authority/AuthorityFactory.sol index 74de4e26..321d55f5 100644 --- a/contracts/consensus/authority/AuthorityFactory.sol +++ b/contracts/consensus/authority/AuthorityFactory.sol @@ -12,9 +12,10 @@ import {Authority} from "./Authority.sol"; /// @notice Allows anyone to reliably deploy a new `Authority` contract. contract AuthorityFactory is IAuthorityFactory { function newAuthority( - address authorityOwner + address authorityOwner, + uint256 epochLength ) external override returns (Authority) { - Authority authority = new Authority(authorityOwner); + Authority authority = new Authority(authorityOwner, epochLength); emit AuthorityCreated(authority); @@ -23,9 +24,13 @@ contract AuthorityFactory is IAuthorityFactory { function newAuthority( address authorityOwner, + uint256 epochLength, bytes32 salt ) external override returns (Authority) { - Authority authority = new Authority{salt: salt}(authorityOwner); + Authority authority = new Authority{salt: salt}( + authorityOwner, + epochLength + ); emit AuthorityCreated(authority); @@ -34,6 +39,7 @@ contract AuthorityFactory is IAuthorityFactory { function calculateAuthorityAddress( address authorityOwner, + uint256 epochLength, bytes32 salt ) external view override returns (address) { return @@ -42,7 +48,7 @@ contract AuthorityFactory is IAuthorityFactory { keccak256( abi.encodePacked( type(Authority).creationCode, - abi.encode(authorityOwner) + abi.encode(authorityOwner, epochLength) ) ) ); diff --git a/contracts/consensus/authority/IAuthorityFactory.sol b/contracts/consensus/authority/IAuthorityFactory.sol index 7cf3d719..e76baf9f 100644 --- a/contracts/consensus/authority/IAuthorityFactory.sol +++ b/contracts/consensus/authority/IAuthorityFactory.sol @@ -18,30 +18,40 @@ interface IAuthorityFactory { /// @notice Deploy a new authority. /// @param authorityOwner The initial authority owner + /// @param epochLength The epoch length /// @return The authority /// @dev On success, MUST emit an `AuthorityCreated` event. /// @dev Reverts if the authority owner address is zero. - function newAuthority(address authorityOwner) external returns (Authority); + /// @dev Reverts if the epoch length is zero. + function newAuthority( + address authorityOwner, + uint256 epochLength + ) external returns (Authority); /// @notice Deploy a new authority deterministically. /// @param authorityOwner The initial authority owner + /// @param epochLength The epoch length /// @param salt The salt used to deterministically generate the authority address /// @return The authority /// @dev On success, MUST emit an `AuthorityCreated` event. /// @dev Reverts if the authority owner address is zero. + /// @dev Reverts if the epoch length is zero. function newAuthority( address authorityOwner, + uint256 epochLength, bytes32 salt ) external returns (Authority); /// @notice Calculate the address of an authority to be deployed deterministically. /// @param authorityOwner The initial authority owner + /// @param epochLength The epoch length /// @param salt The salt used to deterministically generate the authority address /// @return The deterministic authority address /// @dev Beware that only the `newAuthority` function with the `salt` parameter /// is able to deterministically deploy an authority. function calculateAuthorityAddress( address authorityOwner, + uint256 epochLength, bytes32 salt ) external view returns (address); } diff --git a/contracts/consensus/quorum/IQuorumFactory.sol b/contracts/consensus/quorum/IQuorumFactory.sol index c1f4ce47..52dd9890 100644 --- a/contracts/consensus/quorum/IQuorumFactory.sol +++ b/contracts/consensus/quorum/IQuorumFactory.sol @@ -18,30 +18,40 @@ interface IQuorumFactory { /// @notice Deploy a new quorum. /// @param validators the list of validators + /// @param epochLength The epoch length /// @return The quorum /// @dev On success, MUST emit a `QuorumCreated` event. /// @dev Duplicates in the `validators` array are ignored. - function newQuorum(address[] calldata validators) external returns (Quorum); + /// @dev Reverts if the epoch length is zero. + function newQuorum( + address[] calldata validators, + uint256 epochLength + ) external returns (Quorum); /// @notice Deploy a new quorum deterministically. /// @param validators the list of validators + /// @param epochLength The epoch length /// @param salt The salt used to deterministically generate the quorum address /// @return The quorum /// @dev On success, MUST emit a `QuorumCreated` event. /// @dev Duplicates in the `validators` array are ignored. + /// @dev Reverts if the epoch length is zero. function newQuorum( address[] calldata validators, + uint256 epochLength, bytes32 salt ) external returns (Quorum); /// @notice Calculate the address of a quorum to be deployed deterministically. /// @param validators the list of validators + /// @param epochLength The epoch length /// @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 a quorum. function calculateQuorumAddress( address[] calldata validators, + uint256 epochLength, bytes32 salt ) external view returns (address); } diff --git a/contracts/consensus/quorum/Quorum.sol b/contracts/consensus/quorum/Quorum.sol index b1626cf3..bed6011e 100644 --- a/contracts/consensus/quorum/Quorum.sol +++ b/contracts/consensus/quorum/Quorum.sol @@ -47,8 +47,13 @@ contract Quorum is AbstractConsensus { private _votes; /// @param validators The array of validator addresses + /// @param epochLength The epoch length /// @dev Duplicates in the `validators` array are ignored. - constructor(address[] memory validators) { + /// @dev Reverts if the epoch length is zero. + constructor( + address[] memory validators, + uint256 epochLength + ) AbstractConsensus(epochLength) { uint256 n; for (uint256 i; i < validators.length; ++i) { address validator = validators[i]; diff --git a/contracts/consensus/quorum/QuorumFactory.sol b/contracts/consensus/quorum/QuorumFactory.sol index 332d08b1..d8283111 100644 --- a/contracts/consensus/quorum/QuorumFactory.sol +++ b/contracts/consensus/quorum/QuorumFactory.sol @@ -12,9 +12,10 @@ import {Quorum} from "./Quorum.sol"; /// @notice Allows anyone to reliably deploy a new `Quorum` contract. contract QuorumFactory is IQuorumFactory { function newQuorum( - address[] calldata validators + address[] calldata validators, + uint256 epochLength ) external override returns (Quorum) { - Quorum quorum = new Quorum(validators); + Quorum quorum = new Quorum(validators, epochLength); emit QuorumCreated(quorum); @@ -23,9 +24,10 @@ contract QuorumFactory is IQuorumFactory { function newQuorum( address[] calldata validators, + uint256 epochLength, bytes32 salt ) external override returns (Quorum) { - Quorum quorum = new Quorum{salt: salt}(validators); + Quorum quorum = new Quorum{salt: salt}(validators, epochLength); emit QuorumCreated(quorum); @@ -34,6 +36,7 @@ contract QuorumFactory is IQuorumFactory { function calculateQuorumAddress( address[] calldata validators, + uint256 epochLength, bytes32 salt ) external view override returns (address) { return @@ -42,7 +45,7 @@ contract QuorumFactory is IQuorumFactory { keccak256( abi.encodePacked( type(Quorum).creationCode, - abi.encode(validators) + abi.encode(validators, epochLength) ) ) ); diff --git a/contracts/dapp/ISelfHostedApplicationFactory.sol b/contracts/dapp/ISelfHostedApplicationFactory.sol index cda7a055..ef3e81e1 100644 --- a/contracts/dapp/ISelfHostedApplicationFactory.sol +++ b/contracts/dapp/ISelfHostedApplicationFactory.sol @@ -23,6 +23,7 @@ interface ISelfHostedApplicationFactory { /// @notice Deploy new application and authority contracts deterministically. /// @param authorityOwner The initial authority owner + /// @param epochLength The epoch length /// @param appOwner The initial Application owner /// @param templateHash The initial machine state hash /// @param salt The salt used to deterministically generate the addresses @@ -30,8 +31,10 @@ interface ISelfHostedApplicationFactory { /// @return The authority contract /// @dev Reverts if the authority owner address is zero. /// @dev Reverts if the application owner address is zero. + /// @dev Reverts if the epoch length is zero. function deployContracts( address authorityOwner, + uint256 epochLength, address appOwner, bytes32 templateHash, bytes32 salt @@ -40,6 +43,7 @@ interface ISelfHostedApplicationFactory { /// @notice Calculate the addresses of the application and authority contracts /// to be deployed deterministically. /// @param authorityOwner The initial authority owner + /// @param epochLength The epoch length /// @param appOwner The initial Application owner /// @param templateHash The initial machine state hash /// @param salt The salt used to deterministically generate the addresses @@ -47,6 +51,7 @@ interface ISelfHostedApplicationFactory { /// @return The authority address function calculateAddresses( address authorityOwner, + uint256 epochLength, address appOwner, bytes32 templateHash, bytes32 salt diff --git a/contracts/dapp/SelfHostedApplicationFactory.sol b/contracts/dapp/SelfHostedApplicationFactory.sol index bef712ea..874f3c2a 100644 --- a/contracts/dapp/SelfHostedApplicationFactory.sol +++ b/contracts/dapp/SelfHostedApplicationFactory.sol @@ -47,11 +47,16 @@ contract SelfHostedApplicationFactory is ISelfHostedApplicationFactory { function deployContracts( address authorityOwner, + uint256 epochLength, address appOwner, bytes32 templateHash, bytes32 salt ) external returns (Application application, Authority authority) { - authority = _authorityFactory.newAuthority(authorityOwner, salt); + authority = _authorityFactory.newAuthority( + authorityOwner, + epochLength, + salt + ); application = _applicationFactory.newApplication( authority, @@ -63,12 +68,14 @@ contract SelfHostedApplicationFactory is ISelfHostedApplicationFactory { function calculateAddresses( address authorityOwner, + uint256 epochLength, address appOwner, bytes32 templateHash, bytes32 salt ) external view returns (address application, address authority) { authority = _authorityFactory.calculateAuthorityAddress( authorityOwner, + epochLength, salt ); diff --git a/test/foundry/consensus/authority/Authority.t.sol b/test/foundry/consensus/authority/Authority.t.sol index 97ba6107..16aec45c 100644 --- a/test/foundry/consensus/authority/Authority.t.sol +++ b/test/foundry/consensus/authority/Authority.t.sol @@ -16,12 +16,13 @@ import {LibTopic} from "../../util/LibTopic.sol"; contract AuthorityTest is TestBase { using LibTopic for address; - function testConstructor(address owner) public { + function testConstructor(address owner, uint256 epochLength) public { vm.assume(owner != address(0)); + vm.assume(epochLength > 0); vm.recordLogs(); - Authority authority = new Authority(owner); + Authority authority = new Authority(owner, epochLength); Vm.Log[] memory entries = vm.getRecordedLogs(); @@ -45,29 +46,41 @@ contract AuthorityTest is TestBase { assertEq(numOfOwnershipTransferred, 1); assertEq(authority.owner(), owner); + assertEq(authority.getEpochLength(), epochLength); } - function testRevertsOwnerAddressZero() public { + function testRevertsOwnerAddressZero(uint256 epochLength) public { + vm.assume(epochLength > 0); + vm.expectRevert( abi.encodeWithSelector( Ownable.OwnableInvalidOwner.selector, address(0) ) ); - new Authority(address(0)); + new Authority(address(0), epochLength); + } + + function testRevertsEpochLengthZero(address owner) public { + vm.assume(owner != address(0)); + + vm.expectRevert("epoch length must not be zero"); + new Authority(owner, 0); } function testSubmitClaimRevertsCallerNotOwner( address owner, address notOwner, + uint256 epochLength, address appContract, uint256 lastProcessedBlockNumber, bytes32 claim ) public { vm.assume(owner != address(0)); vm.assume(owner != notOwner); + vm.assume(epochLength > 0); - Authority authority = new Authority(owner); + Authority authority = new Authority(owner, epochLength); vm.expectRevert( abi.encodeWithSelector( @@ -82,13 +95,15 @@ contract AuthorityTest is TestBase { function testSubmitClaim( address owner, + uint256 epochLength, address appContract, uint256 lastProcessedBlockNumber, bytes32 claim ) public { vm.assume(owner != address(0)); + vm.assume(epochLength > 0); - Authority authority = new Authority(owner); + Authority authority = new Authority(owner, epochLength); _expectClaimEvents( authority, @@ -106,12 +121,14 @@ contract AuthorityTest is TestBase { function testWasClaimAccepted( address owner, + uint256 epochLength, address appContract, bytes32 claim ) public { vm.assume(owner != address(0)); + vm.assume(epochLength > 0); - Authority authority = new Authority(owner); + Authority authority = new Authority(owner, epochLength); assertFalse(authority.wasClaimAccepted(appContract, claim)); } diff --git a/test/foundry/consensus/authority/AuthorityFactory.t.sol b/test/foundry/consensus/authority/AuthorityFactory.t.sol index 5ee64369..b351e554 100644 --- a/test/foundry/consensus/authority/AuthorityFactory.t.sol +++ b/test/foundry/consensus/authority/AuthorityFactory.t.sol @@ -4,55 +4,103 @@ /// @title Authority Factory Test pragma solidity ^0.8.22; +import {Vm} from "forge-std/Vm.sol"; import {Test} from "forge-std/Test.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + import {AuthorityFactory, IAuthorityFactory} from "contracts/consensus/authority/AuthorityFactory.sol"; import {Authority} from "contracts/consensus/authority/Authority.sol"; -import {Vm} from "forge-std/Vm.sol"; contract AuthorityFactoryTest is Test { AuthorityFactory _factory; - struct AuthorityCreatedEventData { - address authorityOwner; - Authority authority; - } - function setUp() public { _factory = new AuthorityFactory(); } - function testNewAuthority(address authorityOwner) public { + function testRevertsOwnerAddressZero( + uint256 epochLength, + bytes32 salt + ) public { + vm.assume(epochLength > 0); + + vm.expectRevert( + abi.encodeWithSelector( + Ownable.OwnableInvalidOwner.selector, + address(0) + ) + ); + _factory.newAuthority(address(0), epochLength); + + vm.expectRevert( + abi.encodeWithSelector( + Ownable.OwnableInvalidOwner.selector, + address(0) + ) + ); + _factory.newAuthority(address(0), epochLength, salt); + } + + function testRevertsEpochLengthZero( + address authorityOwner, + bytes32 salt + ) public { + vm.assume(authorityOwner != address(0)); + + vm.expectRevert("epoch length must not be zero"); + _factory.newAuthority(authorityOwner, 0); + + vm.expectRevert("epoch length must not be zero"); + _factory.newAuthority(authorityOwner, 0, salt); + } + + function testNewAuthority( + address authorityOwner, + uint256 epochLength + ) public { vm.assume(authorityOwner != address(0)); + vm.assume(epochLength > 0); vm.recordLogs(); - Authority authority = _factory.newAuthority(authorityOwner); + Authority authority = _factory.newAuthority( + authorityOwner, + epochLength + ); - _testNewAuthorityAux(authorityOwner, authority); + _testNewAuthorityAux(authorityOwner, epochLength, authority); } function testNewAuthorityDeterministic( address authorityOwner, + uint256 epochLength, bytes32 salt ) public { vm.assume(authorityOwner != address(0)); + vm.assume(epochLength > 0); address precalculatedAddress = _factory.calculateAuthorityAddress( authorityOwner, + epochLength, salt ); vm.recordLogs(); - Authority authority = _factory.newAuthority(authorityOwner, salt); + Authority authority = _factory.newAuthority( + authorityOwner, + epochLength, + salt + ); - _testNewAuthorityAux(authorityOwner, authority); + _testNewAuthorityAux(authorityOwner, epochLength, authority); // Precalculated address must match actual address assertEq(precalculatedAddress, address(authority)); precalculatedAddress = _factory.calculateAuthorityAddress( authorityOwner, + epochLength, salt ); @@ -61,11 +109,12 @@ contract AuthorityFactoryTest is Test { // Cannot deploy an authority with the same salt twice vm.expectRevert(); - _factory.newAuthority(authorityOwner, salt); + _factory.newAuthority(authorityOwner, epochLength, salt); } function _testNewAuthorityAux( address authorityOwner, + uint256 epochLength, Authority authority ) internal { Vm.Log[] memory entries = vm.getRecordedLogs(); @@ -81,16 +130,14 @@ contract AuthorityFactoryTest is Test { ) { ++numOfAuthorityCreated; - AuthorityCreatedEventData memory eventData; - - eventData = abi.decode(entry.data, (AuthorityCreatedEventData)); + address authorityAddress = abi.decode(entry.data, (address)); - assertEq(authorityOwner, eventData.authorityOwner); - assertEq(address(authority), address(eventData.authority)); + assertEq(address(authority), authorityAddress); } } assertEq(numOfAuthorityCreated, 1); assertEq(authority.owner(), authorityOwner); + assertEq(authority.getEpochLength(), epochLength); } } diff --git a/test/foundry/consensus/quorum/Quorum.t.sol b/test/foundry/consensus/quorum/Quorum.t.sol index bd50ae2b..921a367b 100644 --- a/test/foundry/consensus/quorum/Quorum.t.sol +++ b/test/foundry/consensus/quorum/Quorum.t.sol @@ -68,12 +68,18 @@ contract QuorumTest is TestBase { using LibQuorum for Quorum; using LibTopic for address; - function testConstructor(uint8 numOfValidators) external { + function testConstructor( + uint8 numOfValidators, + uint256 epochLength + ) external { + vm.assume(epochLength > 0); + address[] memory validators = _generateAddresses(numOfValidators); - Quorum quorum = new Quorum(validators); + Quorum quorum = new Quorum(validators, epochLength); assertEq(quorum.numOfValidators(), numOfValidators); + assertEq(quorum.getEpochLength(), epochLength); for (uint256 i; i < numOfValidators; ++i) { address validator = validators[i]; @@ -83,7 +89,14 @@ contract QuorumTest is TestBase { } } - function testConstructorIgnoresDuplicates() external { + function testRevertsEpochLengthZero(uint8 numOfValidators) external { + vm.expectRevert("epoch length must not be zero"); + new Quorum(_generateAddresses(numOfValidators), 0); + } + + function testConstructorIgnoresDuplicates(uint256 epochLength) external { + vm.assume(epochLength > 0); + address[] memory validators = new address[](7); validators[0] = vm.addr(1); @@ -94,7 +107,7 @@ contract QuorumTest is TestBase { validators[5] = vm.addr(1); validators[6] = vm.addr(3); - Quorum quorum = new Quorum(validators); + Quorum quorum = new Quorum(validators, epochLength); assertEq(quorum.numOfValidators(), 3); @@ -104,10 +117,16 @@ contract QuorumTest is TestBase { } } - function testValidatorId(uint8 numOfValidators, address addr) external { + function testValidatorId( + uint8 numOfValidators, + address addr, + uint256 epochLength + ) external { + vm.assume(epochLength > 0); + address[] memory validators = _generateAddresses(numOfValidators); - Quorum quorum = new Quorum(validators); + Quorum quorum = new Quorum(validators, epochLength); uint256 id = quorum.validatorId(addr); @@ -119,39 +138,47 @@ contract QuorumTest is TestBase { } } - function testValidatorByIdZero(uint8 numOfValidators) external { - Quorum quorum = _deployQuorum(numOfValidators); + function testValidatorByIdZero( + uint8 numOfValidators, + uint256 epochLength + ) external { + Quorum quorum = _deployQuorum(numOfValidators, epochLength); assertEq(quorum.validatorById(0), address(0)); } function testValidatorByIdValid( uint8 numOfValidators, - uint256 id + uint256 id, + uint256 epochLength ) external { numOfValidators = uint8(bound(numOfValidators, 1, type(uint8).max)); id = bound(id, 1, numOfValidators); - Quorum quorum = _deployQuorum(numOfValidators); + Quorum quorum = _deployQuorum(numOfValidators, epochLength); address validator = quorum.validatorById(id); assertEq(quorum.validatorId(validator), id); } function testValidatorByIdTooLarge( uint8 numOfValidators, - uint256 id + uint256 id, + uint256 epochLength ) external { id = bound(id, uint256(numOfValidators) + 1, type(uint256).max); - Quorum quorum = _deployQuorum(numOfValidators); + Quorum quorum = _deployQuorum(numOfValidators, epochLength); assertEq(quorum.validatorById(id), address(0)); } function testSubmitClaimRevertsNotValidator( uint8 numOfValidators, + uint256 epochLength, address caller, Claim calldata claim ) external { + vm.assume(epochLength > 0); + address[] memory validators = _generateAddresses(numOfValidators); - Quorum quorum = new Quorum(validators); + Quorum quorum = new Quorum(validators, epochLength); vm.assume(!_contains(validators, caller)); @@ -163,27 +190,30 @@ contract QuorumTest is TestBase { function testNumOfValidatorsInFavorOf( uint8 numOfValidators, + uint256 epochLength, Claim calldata claim ) external { - Quorum quorum = _deployQuorum(numOfValidators); + Quorum quorum = _deployQuorum(numOfValidators, epochLength); assertEq(quorum.numOfValidatorsInFavorOf(claim), 0); } function testIsValidatorInFavorOf( uint8 numOfValidators, + uint256 epochLength, Claim calldata claim, uint256 id ) external { - Quorum quorum = _deployQuorum(numOfValidators); + Quorum quorum = _deployQuorum(numOfValidators, epochLength); assertFalse(quorum.isValidatorInFavorOf(claim, id)); } function testSubmitClaim( uint8 numOfValidators, + uint256 epochLength, Claim calldata claim ) external { numOfValidators = uint8(bound(numOfValidators, 1, 7)); - Quorum quorum = _deployQuorum(numOfValidators); + Quorum quorum = _deployQuorum(numOfValidators, epochLength); bool[] memory inFavorOf = new bool[](numOfValidators + 1); for (uint256 id = 1; id <= numOfValidators; ++id) { _submitClaimAs(quorum, claim, id); @@ -196,10 +226,13 @@ contract QuorumTest is TestBase { /// @dev Each slot has 256 bits, one for each validator ID. /// The first bit is skipped because validator IDs start from 1. /// Therefore, validator ID 256 is the first to use a new slot. - function testSubmitClaim256(Claim calldata claim) external { + function testSubmitClaim256( + Claim calldata claim, + uint256 epochLength + ) external { uint256 numOfValidators = 256; - Quorum quorum = _deployQuorum(numOfValidators); + Quorum quorum = _deployQuorum(numOfValidators, epochLength); uint256 id = numOfValidators; @@ -212,8 +245,12 @@ contract QuorumTest is TestBase { // Internal functions // ------------------ - function _deployQuorum(uint256 numOfValidators) internal returns (Quorum) { - return new Quorum(_generateAddresses(numOfValidators)); + function _deployQuorum( + uint256 numOfValidators, + uint256 epochLength + ) internal returns (Quorum) { + vm.assume(epochLength > 0); + return new Quorum(_generateAddresses(numOfValidators), epochLength); } function _checkSubmitted( diff --git a/test/foundry/consensus/quorum/QuorumFactory.t.sol b/test/foundry/consensus/quorum/QuorumFactory.t.sol index e4217427..d967ae03 100644 --- a/test/foundry/consensus/quorum/QuorumFactory.t.sol +++ b/test/foundry/consensus/quorum/QuorumFactory.t.sol @@ -18,37 +18,58 @@ contract QuorumFactoryTest is TestBase { _factory = new QuorumFactory(); } - function testNewQuorum(uint256 seed) public { + function testRevertsEpochLengthZero(uint256 seed, bytes32 salt) public { + uint256 numOfValidators = bound(seed, 1, _QUORUM_MAX_SIZE); + address[] memory validators = _generateAddresses(numOfValidators); + + vm.expectRevert("epoch length must not be zero"); + _factory.newQuorum(validators, 0); + + vm.expectRevert("epoch length must not be zero"); + _factory.newQuorum(validators, 0, salt); + } + + function testNewQuorum(uint256 seed, uint256 epochLength) public { + vm.assume(epochLength > 0); + uint256 numOfValidators = bound(seed, 1, _QUORUM_MAX_SIZE); address[] memory validators = _generateAddresses(numOfValidators); vm.recordLogs(); - Quorum quorum = _factory.newQuorum(validators); + Quorum quorum = _factory.newQuorum(validators, epochLength); - _testNewQuorumAux(validators, quorum); + _testNewQuorumAux(validators, epochLength, quorum); } - function testNewQuorumDeterministic(uint256 seed, bytes32 salt) public { + function testNewQuorumDeterministic( + uint256 seed, + uint256 epochLength, + bytes32 salt + ) public { + vm.assume(epochLength > 0); + uint256 numOfValidators = bound(seed, 1, _QUORUM_MAX_SIZE); address[] memory validators = _generateAddresses(numOfValidators); address precalculatedAddress = _factory.calculateQuorumAddress( validators, + epochLength, salt ); vm.recordLogs(); - Quorum quorum = _factory.newQuorum(validators, salt); + Quorum quorum = _factory.newQuorum(validators, epochLength, salt); - _testNewQuorumAux(validators, quorum); + _testNewQuorumAux(validators, epochLength, quorum); // Precalculated address must match actual address assertEq(precalculatedAddress, address(quorum)); precalculatedAddress = _factory.calculateQuorumAddress( validators, + epochLength, salt ); @@ -57,11 +78,12 @@ contract QuorumFactoryTest is TestBase { // Cannot deploy a quorum with the same salt twice vm.expectRevert(); - _factory.newQuorum(validators, salt); + _factory.newQuorum(validators, epochLength, salt); } function _testNewQuorumAux( address[] memory validators, + uint256 epochLength, Quorum quorum ) internal { Vm.Log[] memory entries = vm.getRecordedLogs(); @@ -86,5 +108,7 @@ contract QuorumFactoryTest is TestBase { for (uint256 i; i < numOfValidators; ++i) { assertEq(validators[i], quorum.validatorById(i + 1)); } + + assertEq(epochLength, quorum.getEpochLength()); } } diff --git a/test/foundry/dapp/Application.t.sol b/test/foundry/dapp/Application.t.sol index ff3bda09..26095d11 100644 --- a/test/foundry/dapp/Application.t.sol +++ b/test/foundry/dapp/Application.t.sol @@ -54,6 +54,7 @@ contract ApplicationTest is TestBase { uint256[] _transferAmounts; mapping(string => LibEmulator.OutputIndex) _outputIndexByName; + uint256 constant _epochLength = 1; bytes32 constant _templateHash = keccak256("templateHash"); uint256 constant _initialSupply = 1000000000000000000000000000000000000; uint256 constant _tokenId = 88888888; @@ -325,7 +326,7 @@ contract ApplicationTest is TestBase { _tokenIds, _initialSupplies ); - _consensus = new Authority(_authorityOwner); + _consensus = new Authority(_authorityOwner, _epochLength); _appContract = new Application(_consensus, _appOwner, _templateHash); _safeERC20Transfer = new SafeERC20Transfer(); } diff --git a/test/foundry/dapp/SelfHostedApplicationFactory.t.sol b/test/foundry/dapp/SelfHostedApplicationFactory.t.sol index 44d0c928..0c4e8e49 100644 --- a/test/foundry/dapp/SelfHostedApplicationFactory.t.sol +++ b/test/foundry/dapp/SelfHostedApplicationFactory.t.sol @@ -4,6 +4,8 @@ /// @title Self-hosted Application Factory Test pragma solidity ^0.8.22; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + import {IAuthorityFactory} from "contracts/consensus/authority/IAuthorityFactory.sol"; import {AuthorityFactory} from "contracts/consensus/authority/AuthorityFactory.sol"; import {Authority} from "contracts/consensus/authority/Authority.sol"; @@ -12,6 +14,7 @@ import {ApplicationFactory} from "contracts/dapp/ApplicationFactory.sol"; import {Application} from "contracts/dapp/Application.sol"; import {ISelfHostedApplicationFactory} from "contracts/dapp/ISelfHostedApplicationFactory.sol"; import {SelfHostedApplicationFactory} from "contracts/dapp/SelfHostedApplicationFactory.sol"; + import {TestBase} from "../util/TestBase.sol"; contract SelfHostedApplicationFactoryTest is TestBase { @@ -42,20 +45,90 @@ contract SelfHostedApplicationFactoryTest is TestBase { ); } + function testRevertsAuthorityOwnerAddressZero( + uint256 epochLength, + address appOwner, + bytes32 templateHash, + bytes32 salt + ) external { + vm.assume(appOwner != address(0)); + vm.assume(epochLength > 0); + + vm.expectRevert( + abi.encodeWithSelector( + Ownable.OwnableInvalidOwner.selector, + address(0) + ) + ); + factory.deployContracts( + address(0), + epochLength, + appOwner, + templateHash, + salt + ); + } + + function testRevertsEpochLengthZero( + address authorityOwner, + address appOwner, + bytes32 templateHash, + bytes32 salt + ) external { + vm.assume(appOwner != address(0)); + vm.assume(authorityOwner != address(0)); + + vm.expectRevert("epoch length must not be zero"); + factory.deployContracts( + authorityOwner, + 0, + appOwner, + templateHash, + salt + ); + } + + function testRevertsApplicationOwnerAddressZero( + address authorityOwner, + uint256 epochLength, + bytes32 templateHash, + bytes32 salt + ) external { + vm.assume(authorityOwner != address(0)); + vm.assume(epochLength > 0); + + vm.expectRevert( + abi.encodeWithSelector( + Ownable.OwnableInvalidOwner.selector, + address(0) + ) + ); + factory.deployContracts( + authorityOwner, + epochLength, + address(0), + templateHash, + salt + ); + } + function testDeployContracts( address authorityOwner, + uint256 epochLength, address appOwner, bytes32 templateHash, bytes32 salt ) external { vm.assume(appOwner != address(0)); vm.assume(authorityOwner != address(0)); + vm.assume(epochLength > 0); address appAddr; address authorityAddr; (appAddr, authorityAddr) = factory.calculateAddresses( authorityOwner, + epochLength, appOwner, templateHash, salt @@ -66,6 +139,7 @@ contract SelfHostedApplicationFactoryTest is TestBase { (application, authority) = factory.deployContracts( authorityOwner, + epochLength, appOwner, templateHash, salt @@ -75,6 +149,7 @@ contract SelfHostedApplicationFactoryTest is TestBase { assertEq(authorityAddr, address(authority)); assertEq(authority.owner(), authorityOwner); + assertEq(authority.getEpochLength(), epochLength); assertEq(address(application.getConsensus()), authorityAddr); assertEq(application.owner(), appOwner);