Multisig validator manager contracts & tooling (#334)
* All contracts working together, need to separate local vs remote, clean up natspec, and add tests
* Create Inbox/Outbox specific MultisigValidatorManagers
* New Inbox.sol tests
* MultisigValidatorManager tests
* Comment for getCheckpointSignatures
* Fix build
* InboxMultisigValidatorManager tests
* OutboxMultisigValidatorManager tests
* Prettier
* Test rust-produced domain hashes
* Delete old ValidatorManager, rm IValidatorManager reference in Common.sol
* rm validatorManager test, rm unused AbacusDeployment
* Rm test/index.ts
* Modify TestAbacusDeploy to work with new validator managers
* Add inbox / outbox multisig validator managers to core contracts
* Update core test addresses
* Ensure validator set unenrolling does not violate a quorum threshold
* Prettier
* self nits
* All builds passing, fixed invariant checker
* nits
* Minor fixes
* rm IValidatorManager
* Nit
* ValidatorManager -> validator manager in comments
* more moving away from ValidatorManager
* some more...
* Make domainHash internal as _domainHash, publicly expose in TestMultisigValidatorManager
* Update solidity/core/contracts/validator-manager/InboxMultisigValidatorManager.sol
Co-authored-by: Yorke Rhodes <yorke@useabacus.network>
* Revert "Update solidity/core/contracts/validator-manager/InboxMultisigValidatorManager.sol"
This reverts commit cf54d4b765
.
* some natspec fixes
* PR comments, mostly small renames
* Add backticks to natspec
* Apply suggestions from code review
Co-authored-by: Yorke Rhodes <yorke@useabacus.network>
* PR comments
* Rename quorumThreshold -> threshold
* Add isValidator and validatorCount
* Add validatorCount to enroll/unenroll events
* Remove multisig from sdk / deploy names
* Rename InboxMultisig... and OutboxMultisig... to Inbox... and Outbox...
* Better checking, check threshold
* rm validator managers from TestAbacusDeploy
* clean up comments
* PR comments
Co-authored-by: Yorke Rhodes <yorke@useabacus.network>
pull/340/head
parent
9cc2a4388b
commit
39047bbf9f
@ -1,131 +0,0 @@ |
|||||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
|
||||||
pragma solidity >=0.6.11; |
|
||||||
|
|
||||||
// ============ Internal Imports ============ |
|
||||||
import {IValidatorManager} from "../interfaces/IValidatorManager.sol"; |
|
||||||
import {Outbox} from "./Outbox.sol"; |
|
||||||
// ============ External Imports ============ |
|
||||||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; |
|
||||||
import {ECDSA} from "@openzeppelin/contracts/cryptography/ECDSA.sol"; |
|
||||||
|
|
||||||
/** |
|
||||||
* @title ValidatorManager |
|
||||||
* @author Celo Labs Inc. |
|
||||||
* @notice MVP version of contract that will manage Validator selection and |
|
||||||
* rotataion. |
|
||||||
*/ |
|
||||||
contract ValidatorManager is IValidatorManager, Ownable { |
|
||||||
// Mapping of domain -> validator address. |
|
||||||
mapping(uint32 => address) public validators; |
|
||||||
|
|
||||||
// ============ Events ============ |
|
||||||
|
|
||||||
/** |
|
||||||
* @notice Emitted when a validator is enrolled |
|
||||||
* @param domain The domain for which the validator is being enrolled |
|
||||||
* @param validator The address of the validator |
|
||||||
*/ |
|
||||||
event ValidatorEnrolled(uint32 indexed domain, address indexed validator); |
|
||||||
|
|
||||||
/** |
|
||||||
* @notice Emitted when proof of an improper checkpoint is submitted, |
|
||||||
* which sets the contract to FAILED state |
|
||||||
* @param root Root of the improper checkpoint |
|
||||||
* @param index Index of the improper checkpoint |
|
||||||
* @param signature Signature on `root` and `index` |
|
||||||
*/ |
|
||||||
event ImproperCheckpoint( |
|
||||||
address indexed outbox, |
|
||||||
uint32 indexed domain, |
|
||||||
address indexed validator, |
|
||||||
bytes32 root, |
|
||||||
uint256 index, |
|
||||||
bytes signature |
|
||||||
); |
|
||||||
|
|
||||||
// ============ Constructor ============ |
|
||||||
|
|
||||||
constructor() Ownable() {} |
|
||||||
|
|
||||||
// ============ External Functions ============ |
|
||||||
|
|
||||||
/** |
|
||||||
* @notice Enroll a validator for the given domain |
|
||||||
* @dev only callable by trusted owner |
|
||||||
* @param _domain The domain for which the validator is being set |
|
||||||
* @param _validator The address of the validator |
|
||||||
*/ |
|
||||||
function enrollValidator(uint32 _domain, address _validator) |
|
||||||
external |
|
||||||
onlyOwner |
|
||||||
{ |
|
||||||
validators[_domain] = _validator; |
|
||||||
emit ValidatorEnrolled(_domain, _validator); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @notice Check if an Checkpoint is an Improper Checkpoint; |
|
||||||
* if so, set the provided Outbox contract to FAILED state. |
|
||||||
* |
|
||||||
* An Improper Checkpoint is an checkpoint that was not previously checkpointed. |
|
||||||
* @param _outbox Address of the Outbox contract to set to FAILED. |
|
||||||
* @param _root Merkle root of the improper checkpoint |
|
||||||
* @param _index Index root of the improper checkpoint |
|
||||||
* @param _signature Validator signature on `_root` and `_index` |
|
||||||
* @return TRUE if checkpoint was an Improper Checkpoint (implying Validator was slashed) |
|
||||||
*/ |
|
||||||
function improperCheckpoint( |
|
||||||
address _outbox, |
|
||||||
bytes32 _root, |
|
||||||
uint256 _index, |
|
||||||
bytes calldata _signature |
|
||||||
) external returns (bool) { |
|
||||||
uint32 _domain = Outbox(_outbox).localDomain(); |
|
||||||
require( |
|
||||||
isValidatorSignature(_domain, _root, _index, _signature), |
|
||||||
"!validator sig" |
|
||||||
); |
|
||||||
require(Outbox(_outbox).checkpoints(_root) != _index, "!improper"); |
|
||||||
Outbox(_outbox).fail(); |
|
||||||
emit ImproperCheckpoint( |
|
||||||
_outbox, |
|
||||||
_domain, |
|
||||||
validators[_domain], |
|
||||||
_root, |
|
||||||
_index, |
|
||||||
_signature |
|
||||||
); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
// ============ Public Functions ============ |
|
||||||
|
|
||||||
/** |
|
||||||
* @notice Checks that signature was signed by Validator |
|
||||||
* @param _domain Domain of Outbox contract |
|
||||||
* @param _root Merkle root |
|
||||||
* @param _index Corresponding leaf index |
|
||||||
* @param _signature Signature on `_root` and `_index` |
|
||||||
* @return TRUE iff signature is valid signed by validator |
|
||||||
**/ |
|
||||||
function isValidatorSignature( |
|
||||||
uint32 _domain, |
|
||||||
bytes32 _root, |
|
||||||
uint256 _index, |
|
||||||
bytes calldata _signature |
|
||||||
) public view override returns (bool) { |
|
||||||
bytes32 _digest = keccak256( |
|
||||||
abi.encodePacked(domainHash(_domain), _root, _index) |
|
||||||
); |
|
||||||
_digest = ECDSA.toEthSignedMessageHash(_digest); |
|
||||||
return (ECDSA.recover(_digest, _signature) == validators[_domain]); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @notice Hash of domain concatenated with "ABACUS" |
|
||||||
* @param _domain the domain to hash |
|
||||||
*/ |
|
||||||
function domainHash(uint32 _domain) public pure returns (bytes32) { |
|
||||||
return keccak256(abi.encodePacked(_domain, "ABACUS")); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,27 @@ |
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||||
|
pragma solidity >=0.6.11; |
||||||
|
pragma abicoder v2; |
||||||
|
|
||||||
|
import {MultisigValidatorManager} from "../validator-manager/MultisigValidatorManager.sol"; |
||||||
|
|
||||||
|
/** |
||||||
|
* This contract exists to test MultisigValidatorManager.sol, which is abstract |
||||||
|
* and cannot be deployed directly. |
||||||
|
*/ |
||||||
|
contract TestMultisigValidatorManager is MultisigValidatorManager { |
||||||
|
// solhint-disable-next-line no-empty-blocks |
||||||
|
constructor( |
||||||
|
uint32 _domain, |
||||||
|
address[] memory _validators, |
||||||
|
uint256 _threshold |
||||||
|
) MultisigValidatorManager(_domain, _validators, _threshold) {} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Hash of domain concatenated with "ABACUS". |
||||||
|
* @dev This is a public getter of _domainHash to test with. |
||||||
|
* @param _domain The domain to hash. |
||||||
|
*/ |
||||||
|
function getDomainHash(uint32 _domain) external pure returns (bytes32) { |
||||||
|
return _domainHash(_domain); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||||
|
pragma solidity >=0.6.11; |
||||||
|
|
||||||
|
import {IInbox} from "../../interfaces/IInbox.sol"; |
||||||
|
|
||||||
|
/** |
||||||
|
* Intended for testing Inbox.sol, which requires its validator manager |
||||||
|
* to be a contract. |
||||||
|
*/ |
||||||
|
contract TestValidatorManager { |
||||||
|
function checkpoint( |
||||||
|
IInbox _inbox, |
||||||
|
bytes32 _root, |
||||||
|
uint256 _index |
||||||
|
) external { |
||||||
|
_inbox.checkpoint(_root, _index); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||||
|
pragma solidity >=0.6.11; |
||||||
|
pragma abicoder v2; |
||||||
|
|
||||||
|
// ============ Internal Imports ============ |
||||||
|
import {IInbox} from "../../interfaces/IInbox.sol"; |
||||||
|
import {MultisigValidatorManager} from "./MultisigValidatorManager.sol"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @title InboxValidatorManager |
||||||
|
* @notice Verifies checkpoints are signed by a quorum of validators and submits |
||||||
|
* them to an Inbox. |
||||||
|
*/ |
||||||
|
contract InboxValidatorManager is MultisigValidatorManager { |
||||||
|
// ============ Constructor ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Reverts if `_validators` has any duplicates. |
||||||
|
* @param _remoteDomain The remote domain of the outbox chain. |
||||||
|
* @param _validators The set of validator addresses. |
||||||
|
* @param _threshold The quorum threshold. Must be greater than or equal |
||||||
|
* to the length of `_validators`. |
||||||
|
*/ |
||||||
|
// solhint-disable-next-line no-empty-blocks |
||||||
|
constructor( |
||||||
|
uint32 _remoteDomain, |
||||||
|
address[] memory _validators, |
||||||
|
uint256 _threshold |
||||||
|
) MultisigValidatorManager(_remoteDomain, _validators, _threshold) {} |
||||||
|
|
||||||
|
// ============ External Functions ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Submits a checkpoint signed by a quorum of validators to an Inbox. |
||||||
|
* @dev Reverts if `_signatures` is not a quorum of validator signatures. |
||||||
|
* @dev Reverts if `_signatures` is not sorted in ascending order by the signer |
||||||
|
* address, which is required for duplicate detection. |
||||||
|
* @param _inbox The inbox to submit the checkpoint to. |
||||||
|
* @param _root The merkle root of the checkpoint. |
||||||
|
* @param _index The index of the checkpoint. |
||||||
|
* @param _signatures Signatures over the checkpoint to be checked for a validator |
||||||
|
* quorum. Must be sorted in ascending order by signer address. |
||||||
|
*/ |
||||||
|
function checkpoint( |
||||||
|
IInbox _inbox, |
||||||
|
bytes32 _root, |
||||||
|
uint256 _index, |
||||||
|
bytes[] calldata _signatures |
||||||
|
) external { |
||||||
|
require(isQuorum(_root, _index, _signatures), "!quorum"); |
||||||
|
_inbox.checkpoint(_root, _index); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,253 @@ |
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||||
|
pragma solidity >=0.6.11; |
||||||
|
pragma abicoder v2; |
||||||
|
|
||||||
|
// ============ External Imports ============ |
||||||
|
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; |
||||||
|
import {ECDSA} from "@openzeppelin/contracts/cryptography/ECDSA.sol"; |
||||||
|
import {EnumerableSet} from "@openzeppelin/contracts/utils/EnumerableSet.sol"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @title MultisigValidatorManager |
||||||
|
* @notice Manages an ownable set of validators that ECDSA sign checkpoints to |
||||||
|
* reach a quorum. |
||||||
|
*/ |
||||||
|
abstract contract MultisigValidatorManager is Ownable { |
||||||
|
// ============ Libraries ============ |
||||||
|
|
||||||
|
using EnumerableSet for EnumerableSet.AddressSet; |
||||||
|
|
||||||
|
// ============ Immutables ============ |
||||||
|
|
||||||
|
// The domain of the validator set's outbox chain. |
||||||
|
uint32 public immutable domain; |
||||||
|
|
||||||
|
// The domain hash of the validator set's outbox chain. |
||||||
|
bytes32 public immutable domainHash; |
||||||
|
|
||||||
|
// ============ Mutable Storage ============ |
||||||
|
|
||||||
|
// The minimum threshold of validator signatures to constitute a quorum. |
||||||
|
uint256 public threshold; |
||||||
|
|
||||||
|
// The set of validators. |
||||||
|
EnumerableSet.AddressSet private validatorSet; |
||||||
|
|
||||||
|
// ============ Events ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Emitted when a validator is enrolled in the validator set. |
||||||
|
* @param validator The address of the validator. |
||||||
|
* @param validatorCount The new number of enrolled validators in the validator set. |
||||||
|
*/ |
||||||
|
event EnrollValidator(address indexed validator, uint256 validatorCount); |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Emitted when a validator is unenrolled from the validator set. |
||||||
|
* @param validator The address of the validator. |
||||||
|
* @param validatorCount The new number of enrolled validators in the validator set. |
||||||
|
*/ |
||||||
|
event UnenrollValidator(address indexed validator, uint256 validatorCount); |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Emitted when the quorum threshold is set. |
||||||
|
* @param threshold The new quorum threshold. |
||||||
|
*/ |
||||||
|
event SetThreshold(uint256 threshold); |
||||||
|
|
||||||
|
// ============ Constructor ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Reverts if `_validators` has any duplicates. |
||||||
|
* @param _domain The domain of the outbox the validator set is for. |
||||||
|
* @param _validators The set of validator addresses. |
||||||
|
* @param _threshold The quorum threshold. Must be greater than or equal |
||||||
|
* to the length of `_validators`. |
||||||
|
*/ |
||||||
|
constructor( |
||||||
|
uint32 _domain, |
||||||
|
address[] memory _validators, |
||||||
|
uint256 _threshold |
||||||
|
) Ownable() { |
||||||
|
// Set immutables. |
||||||
|
domain = _domain; |
||||||
|
domainHash = _domainHash(_domain); |
||||||
|
|
||||||
|
// Enroll validators. Reverts if there are any duplicates. |
||||||
|
uint256 _numValidators = _validators.length; |
||||||
|
for (uint256 i = 0; i < _numValidators; i++) { |
||||||
|
_enrollValidator(_validators[i]); |
||||||
|
} |
||||||
|
|
||||||
|
_setThreshold(_threshold); |
||||||
|
} |
||||||
|
|
||||||
|
// ============ External Functions ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Enrolls a validator into the validator set. |
||||||
|
* @dev Reverts if `_validator` is already in the validator set. |
||||||
|
* @param _validator The validator to add to the validator set. |
||||||
|
*/ |
||||||
|
function enrollValidator(address _validator) external onlyOwner { |
||||||
|
_enrollValidator(_validator); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Unenrolls a validator from the validator set. |
||||||
|
* @dev Reverts if `_validator` is not in the validator set. |
||||||
|
* @param _validator The validator to remove from the validator set. |
||||||
|
*/ |
||||||
|
function unenrollValidator(address _validator) external onlyOwner { |
||||||
|
_unenrollValidator(_validator); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Sets the quorum threshold. |
||||||
|
* @param _threshold The new quorum threshold. |
||||||
|
*/ |
||||||
|
function setThreshold(uint256 _threshold) external onlyOwner { |
||||||
|
_setThreshold(_threshold); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Gets the addresses of the current validator set. |
||||||
|
* @dev There are no ordering guarantees due to the semantics of EnumerableSet.AddressSet. |
||||||
|
* @return The addresses of the validator set. |
||||||
|
*/ |
||||||
|
function validators() external view returns (address[] memory) { |
||||||
|
uint256 _numValidators = validatorSet.length(); |
||||||
|
address[] memory _validators = new address[](_numValidators); |
||||||
|
for (uint256 i = 0; i < _numValidators; i++) { |
||||||
|
_validators[i] = validatorSet.at(i); |
||||||
|
} |
||||||
|
return _validators; |
||||||
|
} |
||||||
|
|
||||||
|
// ============ Public Functions ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns whether provided signatures over a checkpoint constitute |
||||||
|
* a quorum of validator signatures. |
||||||
|
* @dev Reverts if `_signatures` is not sorted in ascending order by the signer |
||||||
|
* address, which is required for duplicate detection. |
||||||
|
* @dev Does not revert if a signature's signer is not in the validator set. |
||||||
|
* @param _root The merkle root of the checkpoint. |
||||||
|
* @param _index The index of the checkpoint. |
||||||
|
* @param _signatures Signatures over the checkpoint to be checked for a validator |
||||||
|
* quorum. Must be sorted in ascending order by signer address. |
||||||
|
* @return TRUE iff `_signatures` constitute a quorum of validator signatures over |
||||||
|
* the checkpoint. |
||||||
|
*/ |
||||||
|
function isQuorum( |
||||||
|
bytes32 _root, |
||||||
|
uint256 _index, |
||||||
|
bytes[] calldata _signatures |
||||||
|
) public view returns (bool) { |
||||||
|
uint256 _numSignatures = _signatures.length; |
||||||
|
// If there are fewer signatures provided than the required quorum threshold, |
||||||
|
// this is not a quorum. |
||||||
|
if (_numSignatures < threshold) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
// To identify duplicates, the signers recovered from _signatures |
||||||
|
// must be sorted in ascending order. previousSigner is used to |
||||||
|
// enforce ordering. |
||||||
|
address _previousSigner = address(0); |
||||||
|
uint256 _validatorSignatureCount = 0; |
||||||
|
for (uint256 i = 0; i < _numSignatures; i++) { |
||||||
|
address _signer = _recoverCheckpointSigner( |
||||||
|
_root, |
||||||
|
_index, |
||||||
|
_signatures[i] |
||||||
|
); |
||||||
|
// Revert if the signer violates the required sort order. |
||||||
|
require(_previousSigner < _signer, "!sorted signers"); |
||||||
|
// If the signer is a validator, increment _validatorSignatureCount. |
||||||
|
if (isValidator(_signer)) { |
||||||
|
_validatorSignatureCount++; |
||||||
|
} |
||||||
|
_previousSigner = _signer; |
||||||
|
} |
||||||
|
return _validatorSignatureCount >= threshold; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns if `_validator` is enrolled in the validator set. |
||||||
|
* @param _validator The address of the validator. |
||||||
|
* @return TRUE iff `_validator` is enrolled in the validator set. |
||||||
|
*/ |
||||||
|
function isValidator(address _validator) public view returns (bool) { |
||||||
|
return validatorSet.contains(_validator); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Returns the number of validators enrolled in the validator set. |
||||||
|
* @return The number of validators enrolled in the validator set. |
||||||
|
*/ |
||||||
|
function validatorCount() public view returns (uint256) { |
||||||
|
return validatorSet.length(); |
||||||
|
} |
||||||
|
|
||||||
|
// ============ Internal Functions ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Recovers the signer from a signature of a checkpoint. |
||||||
|
* @param _root The checkpoint's merkle root. |
||||||
|
* @param _index The checkpoint's index. |
||||||
|
* @param _signature Signature on the the checkpoint. |
||||||
|
* @return The signer of the checkpoint signature. |
||||||
|
**/ |
||||||
|
function _recoverCheckpointSigner( |
||||||
|
bytes32 _root, |
||||||
|
uint256 _index, |
||||||
|
bytes calldata _signature |
||||||
|
) internal view returns (address) { |
||||||
|
bytes32 _digest = keccak256( |
||||||
|
abi.encodePacked(domainHash, _root, _index) |
||||||
|
); |
||||||
|
return ECDSA.recover(ECDSA.toEthSignedMessageHash(_digest), _signature); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Enrolls a validator into the validator set. |
||||||
|
* @dev Reverts if `_validator` is already in the validator set. |
||||||
|
* @param _validator The validator to add to the validator set. |
||||||
|
*/ |
||||||
|
function _enrollValidator(address _validator) internal { |
||||||
|
require(validatorSet.add(_validator), "already enrolled"); |
||||||
|
emit EnrollValidator(_validator, validatorCount()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Unenrolls a validator from the validator set. |
||||||
|
* @dev Reverts if the resulting validator set length is less than |
||||||
|
* the quorum threshold. |
||||||
|
* @dev Reverts if `_validator` is not in the validator set. |
||||||
|
* @param _validator The validator to remove from the validator set. |
||||||
|
*/ |
||||||
|
function _unenrollValidator(address _validator) internal { |
||||||
|
require(validatorSet.remove(_validator), "!enrolled"); |
||||||
|
uint256 _numValidators = validatorCount(); |
||||||
|
require(_numValidators >= threshold, "violates quorum threshold"); |
||||||
|
emit UnenrollValidator(_validator, _numValidators); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Sets the quorum threshold. |
||||||
|
* @param _threshold The new quorum threshold. |
||||||
|
*/ |
||||||
|
function _setThreshold(uint256 _threshold) internal { |
||||||
|
require(_threshold > 0 && _threshold <= validatorCount(), "!range"); |
||||||
|
threshold = _threshold; |
||||||
|
emit SetThreshold(_threshold); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Hash of `_domain` concatenated with "ABACUS". |
||||||
|
* @param _domain The domain to hash. |
||||||
|
*/ |
||||||
|
function _domainHash(uint32 _domain) internal pure returns (bytes32) { |
||||||
|
return keccak256(abi.encodePacked(_domain, "ABACUS")); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,73 @@ |
|||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||||
|
pragma solidity >=0.6.11; |
||||||
|
pragma abicoder v2; |
||||||
|
|
||||||
|
// ============ Internal Imports ============ |
||||||
|
import {IOutbox} from "../../interfaces/IOutbox.sol"; |
||||||
|
import {MultisigValidatorManager} from "./MultisigValidatorManager.sol"; |
||||||
|
|
||||||
|
/** |
||||||
|
* @title OutboxValidatorManager |
||||||
|
* @notice Verifies if an improper checkpoint has been signed by a quorum of |
||||||
|
* validators and reports it to an Outbox. |
||||||
|
*/ |
||||||
|
contract OutboxValidatorManager is MultisigValidatorManager { |
||||||
|
// ============ Events ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Emitted when proof of an improper checkpoint is submitted. |
||||||
|
* @dev Observers of this event should filter by the outbox address. |
||||||
|
* @param outbox The outbox. |
||||||
|
* @param root Root of the improper checkpoint. |
||||||
|
* @param index Index of the improper checkpoint. |
||||||
|
* @param signatures A quorum of signatures on the improper checkpoint. |
||||||
|
* May include non-validator signatures. |
||||||
|
*/ |
||||||
|
event ImproperCheckpoint( |
||||||
|
address indexed outbox, |
||||||
|
bytes32 root, |
||||||
|
uint256 index, |
||||||
|
bytes[] signatures |
||||||
|
); |
||||||
|
|
||||||
|
// ============ Constructor ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @dev Reverts if `_validators` has any duplicates. |
||||||
|
* @param _localDomain The local domain. |
||||||
|
* @param _validators The set of validator addresses. |
||||||
|
* @param _threshold The quorum threshold. Must be greater than or equal |
||||||
|
* to the length of `_validators`. |
||||||
|
*/ |
||||||
|
// solhint-disable-next-line no-empty-blocks |
||||||
|
constructor( |
||||||
|
uint32 _localDomain, |
||||||
|
address[] memory _validators, |
||||||
|
uint256 _threshold |
||||||
|
) MultisigValidatorManager(_localDomain, _validators, _threshold) {} |
||||||
|
|
||||||
|
// ============ External Functions ============ |
||||||
|
|
||||||
|
/** |
||||||
|
* @notice Determines if a quorum of validators have signed an improper checkpoint, |
||||||
|
* failing the Outbox if so. |
||||||
|
* @dev Improper checkpoints signed by individual validators are not handled to prevent |
||||||
|
* a single byzantine validator from failing the Outbox. |
||||||
|
* @param _outbox The outbox. |
||||||
|
* @param _root The merkle root of the checkpoint. |
||||||
|
* @param _index The index of the checkpoint. |
||||||
|
* @param _signatures Signatures over the checkpoint to be checked for a validator |
||||||
|
* quorum. Must be sorted in ascending order by signer address. |
||||||
|
*/ |
||||||
|
function improperCheckpoint( |
||||||
|
IOutbox _outbox, |
||||||
|
bytes32 _root, |
||||||
|
uint256 _index, |
||||||
|
bytes[] calldata _signatures |
||||||
|
) external { |
||||||
|
require(isQuorum(_root, _index, _signatures), "!quorum"); |
||||||
|
require(!_outbox.isCheckpoint(_root, _index), "!improper checkpoint"); |
||||||
|
_outbox.fail(); |
||||||
|
emit ImproperCheckpoint(address(_outbox), _root, _index, _signatures); |
||||||
|
} |
||||||
|
} |
@ -1,11 +0,0 @@ |
|||||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
|
||||||
pragma solidity >=0.6.11; |
|
||||||
|
|
||||||
interface IValidatorManager { |
|
||||||
function isValidatorSignature( |
|
||||||
uint32 _domain, |
|
||||||
bytes32 _root, |
|
||||||
uint256 _index, |
|
||||||
bytes calldata _signature |
|
||||||
) external view returns (bool); |
|
||||||
} |
|
@ -1 +0,0 @@ |
|||||||
export { AbacusDeployment } from './lib/AbacusDeployment'; |
|
@ -1,197 +0,0 @@ |
|||||||
import { assert } from 'chai'; |
|
||||||
import * as ethers from 'ethers'; |
|
||||||
import { types, Validator } from '@abacus-network/utils'; |
|
||||||
|
|
||||||
import { |
|
||||||
TestOutbox, |
|
||||||
TestOutbox__factory, |
|
||||||
ValidatorManager, |
|
||||||
ValidatorManager__factory, |
|
||||||
UpgradeBeaconController, |
|
||||||
UpgradeBeaconController__factory, |
|
||||||
XAppConnectionManager, |
|
||||||
XAppConnectionManager__factory, |
|
||||||
TestInbox, |
|
||||||
TestInbox__factory, |
|
||||||
} from '../../types'; |
|
||||||
|
|
||||||
export interface AbacusInstance { |
|
||||||
domain: types.Domain; |
|
||||||
validator: Validator; |
|
||||||
validatorManager: ValidatorManager; |
|
||||||
outbox: TestOutbox; |
|
||||||
connectionManager: XAppConnectionManager; |
|
||||||
ubc: UpgradeBeaconController; |
|
||||||
inboxs: Record<number, TestInbox>; |
|
||||||
} |
|
||||||
|
|
||||||
export class AbacusDeployment { |
|
||||||
constructor( |
|
||||||
public readonly domains: types.Domain[], |
|
||||||
public readonly instances: Record<number, AbacusInstance>, |
|
||||||
public readonly signer: ethers.Signer, |
|
||||||
) {} |
|
||||||
|
|
||||||
static async fromDomains(domains: types.Domain[], signer: ethers.Signer) { |
|
||||||
const instances: Record<number, AbacusInstance> = {}; |
|
||||||
for (const local of domains) { |
|
||||||
const instance = await AbacusDeployment.deployInstance( |
|
||||||
local, |
|
||||||
domains.filter((d) => d !== local), |
|
||||||
signer, |
|
||||||
); |
|
||||||
instances[local] = instance; |
|
||||||
} |
|
||||||
return new AbacusDeployment(domains, instances, signer); |
|
||||||
} |
|
||||||
|
|
||||||
static async deployInstance( |
|
||||||
local: types.Domain, |
|
||||||
remotes: types.Domain[], |
|
||||||
signer: ethers.Signer, |
|
||||||
): Promise<AbacusInstance> { |
|
||||||
const validatorManagerFactory = new ValidatorManager__factory(signer); |
|
||||||
const validatorManager = await validatorManagerFactory.deploy(); |
|
||||||
await validatorManager.enrollValidator(local, await signer.getAddress()); |
|
||||||
await Promise.all( |
|
||||||
remotes.map(async (remoteDomain) => |
|
||||||
validatorManager.enrollValidator( |
|
||||||
remoteDomain, |
|
||||||
await signer.getAddress(), |
|
||||||
), |
|
||||||
), |
|
||||||
); |
|
||||||
|
|
||||||
const ubcFactory = new UpgradeBeaconController__factory(signer); |
|
||||||
const ubc = await ubcFactory.deploy(); |
|
||||||
|
|
||||||
const outboxFactory = new TestOutbox__factory(signer); |
|
||||||
const outbox = await outboxFactory.deploy(local); |
|
||||||
await outbox.initialize(validatorManager.address); |
|
||||||
|
|
||||||
const connectionManagerFactory = new XAppConnectionManager__factory(signer); |
|
||||||
const connectionManager = await connectionManagerFactory.deploy(); |
|
||||||
await connectionManager.setOutbox(outbox.address); |
|
||||||
|
|
||||||
const inboxFactory = new TestInbox__factory(signer); |
|
||||||
const inboxs: Record<number, TestInbox> = {}; |
|
||||||
const deploys = remotes.map(async (remoteDomain) => { |
|
||||||
const inbox = await inboxFactory.deploy(local); |
|
||||||
await inbox.initialize( |
|
||||||
remoteDomain, |
|
||||||
validatorManager.address, |
|
||||||
ethers.constants.HashZero, |
|
||||||
0, |
|
||||||
); |
|
||||||
await connectionManager.enrollInbox(remoteDomain, inbox.address); |
|
||||||
inboxs[remoteDomain] = inbox; |
|
||||||
}); |
|
||||||
await Promise.all(deploys); |
|
||||||
return { |
|
||||||
domain: local, |
|
||||||
validator: await Validator.fromSigner(signer, local), |
|
||||||
outbox, |
|
||||||
connectionManager, |
|
||||||
validatorManager, |
|
||||||
inboxs, |
|
||||||
ubc, |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
async transferOwnership(domain: types.Domain, address: types.Address) { |
|
||||||
await this.outbox(domain).transferOwnership(address); |
|
||||||
await this.ubc(domain).transferOwnership(address); |
|
||||||
await this.connectionManager(domain).transferOwnership(address); |
|
||||||
await this.validatorManager(domain).transferOwnership(address); |
|
||||||
for (const remote of this.domains) { |
|
||||||
if (remote !== domain) { |
|
||||||
await this.inbox(domain, remote).transferOwnership(address); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
outbox(domain: types.Domain): TestOutbox { |
|
||||||
return this.instances[domain].outbox; |
|
||||||
} |
|
||||||
|
|
||||||
ubc(domain: types.Domain): UpgradeBeaconController { |
|
||||||
return this.instances[domain].ubc; |
|
||||||
} |
|
||||||
|
|
||||||
validator(domain: types.Domain): Validator { |
|
||||||
return this.instances[domain].validator; |
|
||||||
} |
|
||||||
|
|
||||||
inbox(local: types.Domain, remote: types.Domain): TestInbox { |
|
||||||
return this.instances[local].inboxs[remote]; |
|
||||||
} |
|
||||||
|
|
||||||
connectionManager(domain: types.Domain): XAppConnectionManager { |
|
||||||
return this.instances[domain].connectionManager; |
|
||||||
} |
|
||||||
|
|
||||||
validatorManager(domain: types.Domain): ValidatorManager { |
|
||||||
return this.instances[domain].validatorManager; |
|
||||||
} |
|
||||||
|
|
||||||
async processMessages() { |
|
||||||
await Promise.all( |
|
||||||
this.domains.map((d) => this.processMessagesFromDomain(d)), |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
async processMessagesFromDomain(local: types.Domain) { |
|
||||||
const outbox = this.outbox(local); |
|
||||||
const [checkpointedRoot, checkpointedIndex] = |
|
||||||
await outbox.latestCheckpoint(); |
|
||||||
const latestIndex = await outbox.tree(); |
|
||||||
if (latestIndex.eq(checkpointedIndex)) return; |
|
||||||
|
|
||||||
// Find the block number of the last checkpoint submitted on Outbox.
|
|
||||||
const checkpointFilter = outbox.filters.Checkpoint(checkpointedRoot); |
|
||||||
const checkpoints = await outbox.queryFilter(checkpointFilter); |
|
||||||
assert(checkpoints.length === 0 || checkpoints.length === 1); |
|
||||||
const fromBlock = checkpoints.length === 0 ? 0 : checkpoints[0].blockNumber; |
|
||||||
|
|
||||||
await outbox.checkpoint(); |
|
||||||
const [root, index] = await outbox.latestCheckpoint(); |
|
||||||
// If there have been no checkpoints since the last checkpoint, return.
|
|
||||||
if ( |
|
||||||
index.eq(0) || |
|
||||||
(checkpoints.length == 1 && index.eq(checkpoints[0].args.index)) |
|
||||||
) { |
|
||||||
return; |
|
||||||
} |
|
||||||
// Update the Outbox and Inboxs to the latest roots.
|
|
||||||
// This is technically not necessary given that we are not proving against
|
|
||||||
// a root in the TestInbox.
|
|
||||||
const validator = this.validator(local); |
|
||||||
const { signature } = await validator.signCheckpoint( |
|
||||||
root, |
|
||||||
index.toNumber(), |
|
||||||
); |
|
||||||
|
|
||||||
for (const remote of this.domains) { |
|
||||||
if (remote !== local) { |
|
||||||
const inbox = this.inbox(remote, local); |
|
||||||
await inbox.checkpoint(root, index, signature); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Find all messages dispatched on the outbox since the previous checkpoint.
|
|
||||||
const dispatchFilter = outbox.filters.Dispatch(); |
|
||||||
const dispatches = await outbox.queryFilter(dispatchFilter, fromBlock); |
|
||||||
for (const dispatch of dispatches) { |
|
||||||
const destination = dispatch.args.destinationAndNonce.shr(32).toNumber(); |
|
||||||
if (destination !== local) { |
|
||||||
const inbox = this.inbox(destination, local); |
|
||||||
await inbox.setMessageProven(dispatch.args.message); |
|
||||||
await inbox.testProcess(dispatch.args.message); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export const abacus: any = { |
|
||||||
AbacusDeployment, |
|
||||||
}; |
|
@ -0,0 +1,78 @@ |
|||||||
|
import { ethers } from 'hardhat'; |
||||||
|
import { Validator } from '@abacus-network/utils'; |
||||||
|
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; |
||||||
|
|
||||||
|
import { |
||||||
|
Inbox, |
||||||
|
Inbox__factory, |
||||||
|
InboxValidatorManager, |
||||||
|
InboxValidatorManager__factory, |
||||||
|
} from '../../types'; |
||||||
|
import { signCheckpoint } from './utils'; |
||||||
|
import { expect } from 'chai'; |
||||||
|
|
||||||
|
const OUTBOX_DOMAIN = 1234; |
||||||
|
const INBOX_DOMAIN = 4321; |
||||||
|
const QUORUM_THRESHOLD = 2; |
||||||
|
|
||||||
|
describe('InboxValidatorManager', () => { |
||||||
|
let validatorManager: InboxValidatorManager, |
||||||
|
inbox: Inbox, |
||||||
|
signer: SignerWithAddress, |
||||||
|
validator0: Validator, |
||||||
|
validator1: Validator; |
||||||
|
|
||||||
|
before(async () => { |
||||||
|
const signers = await ethers.getSigners(); |
||||||
|
signer = signers[0]; |
||||||
|
validator0 = await Validator.fromSigner(signers[1], OUTBOX_DOMAIN); |
||||||
|
validator1 = await Validator.fromSigner(signers[2], OUTBOX_DOMAIN); |
||||||
|
}); |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const validatorManagerFactory = new InboxValidatorManager__factory(signer); |
||||||
|
validatorManager = await validatorManagerFactory.deploy( |
||||||
|
OUTBOX_DOMAIN, |
||||||
|
[validator0.address, validator1.address], |
||||||
|
QUORUM_THRESHOLD, |
||||||
|
); |
||||||
|
|
||||||
|
const inboxFactory = new Inbox__factory(signer); |
||||||
|
inbox = await inboxFactory.deploy(INBOX_DOMAIN); |
||||||
|
await inbox.initialize( |
||||||
|
OUTBOX_DOMAIN, |
||||||
|
validatorManager.address, |
||||||
|
ethers.constants.HashZero, |
||||||
|
0, |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#checkpoint', () => { |
||||||
|
const root = ethers.utils.formatBytes32String('test root'); |
||||||
|
const index = 1; |
||||||
|
|
||||||
|
it('submits a checkpoint to the Inbox if there is a quorum', async () => { |
||||||
|
const signatures = await signCheckpoint( |
||||||
|
root, |
||||||
|
index, |
||||||
|
[validator0, validator1], // 2/2 signers, making a quorum
|
||||||
|
); |
||||||
|
|
||||||
|
await validatorManager.checkpoint(inbox.address, root, index, signatures); |
||||||
|
|
||||||
|
expect(await inbox.checkpoints(root)).to.equal(index); |
||||||
|
}); |
||||||
|
|
||||||
|
it('reverts if there is not a quorum', async () => { |
||||||
|
const signatures = await signCheckpoint( |
||||||
|
root, |
||||||
|
index, |
||||||
|
[validator0], // 1/2 signers is not a quorum
|
||||||
|
); |
||||||
|
|
||||||
|
await expect( |
||||||
|
validatorManager.checkpoint(inbox.address, root, index, signatures), |
||||||
|
).to.be.revertedWith('!quorum'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,269 @@ |
|||||||
|
import { expect } from 'chai'; |
||||||
|
import { ethers } from 'hardhat'; |
||||||
|
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; |
||||||
|
import { Validator } from '@abacus-network/utils'; |
||||||
|
|
||||||
|
import { |
||||||
|
TestMultisigValidatorManager, |
||||||
|
TestMultisigValidatorManager__factory, |
||||||
|
} from '../../types'; |
||||||
|
import { signCheckpoint } from './utils'; |
||||||
|
|
||||||
|
const OUTBOX_DOMAIN = 1234; |
||||||
|
const QUORUM_THRESHOLD = 1; |
||||||
|
|
||||||
|
const domainHashTestCases = require('../../../../vectors/domainHash.json'); |
||||||
|
|
||||||
|
describe('MultisigValidatorManager', async () => { |
||||||
|
let validatorManager: TestMultisigValidatorManager, |
||||||
|
signer: SignerWithAddress, |
||||||
|
nonOwner: SignerWithAddress, |
||||||
|
validator0: Validator, |
||||||
|
validator1: Validator, |
||||||
|
validator2: Validator, |
||||||
|
validator3: Validator; |
||||||
|
|
||||||
|
before(async () => { |
||||||
|
const signers = await ethers.getSigners(); |
||||||
|
[signer, nonOwner] = signers; |
||||||
|
validator0 = await Validator.fromSigner(signers[2], OUTBOX_DOMAIN); |
||||||
|
validator1 = await Validator.fromSigner(signers[3], OUTBOX_DOMAIN); |
||||||
|
validator2 = await Validator.fromSigner(signers[4], OUTBOX_DOMAIN); |
||||||
|
validator3 = await Validator.fromSigner(signers[5], OUTBOX_DOMAIN); |
||||||
|
}); |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const validatorManagerFactory = new TestMultisigValidatorManager__factory( |
||||||
|
signer, |
||||||
|
); |
||||||
|
validatorManager = await validatorManagerFactory.deploy( |
||||||
|
OUTBOX_DOMAIN, |
||||||
|
[validator0.address], |
||||||
|
QUORUM_THRESHOLD, |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#constructor', () => { |
||||||
|
it('sets the domain', async () => { |
||||||
|
expect(await validatorManager.domain()).to.equal(OUTBOX_DOMAIN); |
||||||
|
}); |
||||||
|
|
||||||
|
it('sets the domainHash', async () => { |
||||||
|
const domainHash = await validatorManager.getDomainHash(OUTBOX_DOMAIN); |
||||||
|
expect(await validatorManager.domainHash()).to.equal(domainHash); |
||||||
|
}); |
||||||
|
|
||||||
|
it('enrolls the validator set', async () => { |
||||||
|
expect(await validatorManager.validators()).to.deep.equal([ |
||||||
|
validator0.address, |
||||||
|
]); |
||||||
|
}); |
||||||
|
|
||||||
|
it('sets the quorum threshold', async () => { |
||||||
|
expect(await validatorManager.threshold()).to.equal([QUORUM_THRESHOLD]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#enrollValidator', () => { |
||||||
|
it('enrolls a validator into the validator set', async () => { |
||||||
|
await validatorManager.enrollValidator(validator1.address); |
||||||
|
|
||||||
|
expect(await validatorManager.validators()).to.deep.equal([ |
||||||
|
validator0.address, |
||||||
|
validator1.address, |
||||||
|
]); |
||||||
|
}); |
||||||
|
|
||||||
|
it('emits the EnrollValidator event', async () => { |
||||||
|
expect(await validatorManager.enrollValidator(validator1.address)) |
||||||
|
.to.emit(validatorManager, 'EnrollValidator') |
||||||
|
.withArgs(validator1.address, 2); |
||||||
|
}); |
||||||
|
|
||||||
|
it('reverts if the validator is already enrolled', async () => { |
||||||
|
await expect( |
||||||
|
validatorManager.enrollValidator(validator0.address), |
||||||
|
).to.be.revertedWith('already enrolled'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('reverts when called by a non-owner', async () => { |
||||||
|
await expect( |
||||||
|
validatorManager.connect(nonOwner).enrollValidator(validator1.address), |
||||||
|
).to.be.revertedWith('Ownable: caller is not the owner'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#unenrollValidator', () => { |
||||||
|
beforeEach(async () => { |
||||||
|
// Enroll a second validator
|
||||||
|
await validatorManager.enrollValidator(validator1.address); |
||||||
|
}); |
||||||
|
|
||||||
|
it('unenrolls a validator from the validator set', async () => { |
||||||
|
await validatorManager.unenrollValidator(validator1.address); |
||||||
|
|
||||||
|
expect(await validatorManager.validators()).to.deep.equal([ |
||||||
|
validator0.address, |
||||||
|
]); |
||||||
|
}); |
||||||
|
|
||||||
|
it('emits the UnenrollValidator event', async () => { |
||||||
|
expect(await validatorManager.unenrollValidator(validator1.address)) |
||||||
|
.to.emit(validatorManager, 'UnenrollValidator') |
||||||
|
.withArgs(validator1.address, 1); |
||||||
|
}); |
||||||
|
|
||||||
|
it('reverts if the resulting validator set size will be less than the quorum threshold', async () => { |
||||||
|
await validatorManager.setThreshold(2); |
||||||
|
|
||||||
|
await expect( |
||||||
|
validatorManager.unenrollValidator(validator1.address), |
||||||
|
).to.be.revertedWith('violates quorum threshold'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('reverts if the validator is not already enrolled', async () => { |
||||||
|
await expect( |
||||||
|
validatorManager.unenrollValidator(validator2.address), |
||||||
|
).to.be.revertedWith('!enrolled'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('reverts when called by a non-owner', async () => { |
||||||
|
await expect( |
||||||
|
validatorManager |
||||||
|
.connect(nonOwner) |
||||||
|
.unenrollValidator(validator1.address), |
||||||
|
).to.be.revertedWith('Ownable: caller is not the owner'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#setThreshold', () => { |
||||||
|
beforeEach(async () => { |
||||||
|
// Have 2 validators to allow us to have more than 1 valid
|
||||||
|
// quorum threshold
|
||||||
|
await validatorManager.enrollValidator(validator1.address); |
||||||
|
}); |
||||||
|
|
||||||
|
it('sets the quorum threshold', async () => { |
||||||
|
await validatorManager.setThreshold(2); |
||||||
|
|
||||||
|
expect(await validatorManager.threshold()).to.equal(2); |
||||||
|
}); |
||||||
|
|
||||||
|
it('emits the SetThreshold event', async () => { |
||||||
|
expect(await validatorManager.setThreshold(2)) |
||||||
|
.to.emit(validatorManager, 'SetThreshold') |
||||||
|
.withArgs(2); |
||||||
|
}); |
||||||
|
|
||||||
|
it('reverts if the new quorum threshold is zero', async () => { |
||||||
|
await expect(validatorManager.setThreshold(0)).to.be.revertedWith( |
||||||
|
'!range', |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('reverts if the new quorum threshold is greater than the validator set size', async () => { |
||||||
|
await expect(validatorManager.setThreshold(3)).to.be.revertedWith( |
||||||
|
'!range', |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('reverts when called by a non-owner', async () => { |
||||||
|
await expect( |
||||||
|
validatorManager.connect(nonOwner).setThreshold(2), |
||||||
|
).to.be.revertedWith('Ownable: caller is not the owner'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#validatorCount', () => { |
||||||
|
it('returns the number of validators enrolled in the validator set', async () => { |
||||||
|
expect(await validatorManager.validatorCount()).to.equal(1); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#isQuorum', () => { |
||||||
|
const root = ethers.utils.formatBytes32String('test root'); |
||||||
|
const index = 1; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
// Have 3 validators and a quorum of 2
|
||||||
|
await validatorManager.enrollValidator(validator1.address); |
||||||
|
await validatorManager.enrollValidator(validator2.address); |
||||||
|
|
||||||
|
await validatorManager.setThreshold(2); |
||||||
|
}); |
||||||
|
|
||||||
|
it('returns true when there is a quorum', async () => { |
||||||
|
const signatures = await signCheckpoint(root, index, [ |
||||||
|
validator0, |
||||||
|
validator1, |
||||||
|
]); |
||||||
|
expect(await validatorManager.isQuorum(root, index, signatures)).to.be |
||||||
|
.true; |
||||||
|
}); |
||||||
|
|
||||||
|
it('returns true when a quorum exists even if provided with non-validator signatures', async () => { |
||||||
|
const signatures = await signCheckpoint( |
||||||
|
root, |
||||||
|
index, |
||||||
|
[validator0, validator1, validator3], // validator 3 is not enrolled
|
||||||
|
); |
||||||
|
expect(await validatorManager.isQuorum(root, index, signatures)).to.be |
||||||
|
.true; |
||||||
|
}); |
||||||
|
|
||||||
|
it('returns false when the signature count is less than the quorum threshold', async () => { |
||||||
|
const signatures = await signCheckpoint(root, index, [validator0]); |
||||||
|
expect(await validatorManager.isQuorum(root, index, signatures)).to.be |
||||||
|
.false; |
||||||
|
}); |
||||||
|
|
||||||
|
it('returns false when some signatures are not from enrolled validators', async () => { |
||||||
|
const signatures = await signCheckpoint( |
||||||
|
root, |
||||||
|
index, |
||||||
|
[validator0, validator3], // validator 3 is not enrolled
|
||||||
|
); |
||||||
|
expect(await validatorManager.isQuorum(root, index, signatures)).to.be |
||||||
|
.false; |
||||||
|
}); |
||||||
|
|
||||||
|
it('reverts when signatures are not ordered by their signer', async () => { |
||||||
|
// Reverse the signature order, purposely messing up the
|
||||||
|
// ascending sort
|
||||||
|
const signatures = ( |
||||||
|
await signCheckpoint(root, index, [validator0, validator1]) |
||||||
|
).reverse(); |
||||||
|
|
||||||
|
await expect( |
||||||
|
validatorManager.isQuorum(root, index, signatures), |
||||||
|
).to.be.revertedWith('!sorted signers'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#isValidator', () => { |
||||||
|
it('returns true if an address is enrolled in the validator set', async () => { |
||||||
|
expect(await validatorManager.isValidator(validator0.address)).to.be.true; |
||||||
|
}); |
||||||
|
|
||||||
|
it('returns false if an address is not enrolled in the validator set', async () => { |
||||||
|
expect(await validatorManager.isValidator(validator1.address)).to.be |
||||||
|
.false; |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#_domainHash', () => { |
||||||
|
it('matches Rust-produced domain hashes', async () => { |
||||||
|
// Compare Rust output in json file to solidity output (json file matches
|
||||||
|
// hash for local domain of 1000)
|
||||||
|
for (let testCase of domainHashTestCases) { |
||||||
|
const { expectedDomainHash } = testCase; |
||||||
|
// This public function on TestMultisigValidatorManager exposes
|
||||||
|
// the internal _domainHash on MultisigValidatorManager.
|
||||||
|
const domainHash = await validatorManager.getDomainHash( |
||||||
|
testCase.outboxDomain, |
||||||
|
); |
||||||
|
expect(domainHash).to.equal(expectedDomainHash); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,112 @@ |
|||||||
|
import { expect } from 'chai'; |
||||||
|
import { ethers } from 'hardhat'; |
||||||
|
import { types, utils, Validator } from '@abacus-network/utils'; |
||||||
|
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; |
||||||
|
|
||||||
|
import { |
||||||
|
Outbox, |
||||||
|
Outbox__factory, |
||||||
|
OutboxValidatorManager, |
||||||
|
OutboxValidatorManager__factory, |
||||||
|
} from '../../types'; |
||||||
|
import { signCheckpoint } from './utils'; |
||||||
|
|
||||||
|
const OUTBOX_DOMAIN = 1234; |
||||||
|
const INBOX_DOMAIN = 4321; |
||||||
|
const QUORUM_THRESHOLD = 2; |
||||||
|
|
||||||
|
describe('OutboxValidatorManager', () => { |
||||||
|
let validatorManager: OutboxValidatorManager, |
||||||
|
outbox: Outbox, |
||||||
|
signer: SignerWithAddress, |
||||||
|
validator0: Validator, |
||||||
|
validator1: Validator; |
||||||
|
|
||||||
|
before(async () => { |
||||||
|
const signers = await ethers.getSigners(); |
||||||
|
signer = signers[0]; |
||||||
|
validator0 = await Validator.fromSigner(signers[1], OUTBOX_DOMAIN); |
||||||
|
validator1 = await Validator.fromSigner(signers[2], OUTBOX_DOMAIN); |
||||||
|
}); |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const validatorManagerFactory = new OutboxValidatorManager__factory(signer); |
||||||
|
validatorManager = await validatorManagerFactory.deploy( |
||||||
|
OUTBOX_DOMAIN, |
||||||
|
[validator0.address, validator1.address], |
||||||
|
QUORUM_THRESHOLD, |
||||||
|
); |
||||||
|
|
||||||
|
const outboxFactory = new Outbox__factory(signer); |
||||||
|
outbox = await outboxFactory.deploy(OUTBOX_DOMAIN); |
||||||
|
await outbox.initialize(validatorManager.address); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('#improperCheckpoint', () => { |
||||||
|
const root = ethers.utils.formatBytes32String('test root'); |
||||||
|
const index = 1; |
||||||
|
|
||||||
|
it('accepts an improper checkpoint if there is a quorum', async () => { |
||||||
|
const signatures = await signCheckpoint( |
||||||
|
root, |
||||||
|
index, |
||||||
|
[validator0, validator1], // 2/2 signers, making a quorum
|
||||||
|
); |
||||||
|
|
||||||
|
await expect( |
||||||
|
validatorManager.improperCheckpoint( |
||||||
|
outbox.address, |
||||||
|
root, |
||||||
|
index, |
||||||
|
signatures, |
||||||
|
), |
||||||
|
) |
||||||
|
.to.emit(validatorManager, 'ImproperCheckpoint') |
||||||
|
.withArgs(outbox.address, root, index, signatures); |
||||||
|
expect(await outbox.state()).to.equal(types.AbacusState.FAILED); |
||||||
|
}); |
||||||
|
|
||||||
|
it('reverts if there is not a quorum', async () => { |
||||||
|
const signatures = await signCheckpoint( |
||||||
|
root, |
||||||
|
index, |
||||||
|
[validator0], // 1/2 signers is not a quorum
|
||||||
|
); |
||||||
|
|
||||||
|
await expect( |
||||||
|
validatorManager.improperCheckpoint( |
||||||
|
outbox.address, |
||||||
|
root, |
||||||
|
index, |
||||||
|
signatures, |
||||||
|
), |
||||||
|
).to.be.revertedWith('!quorum'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('reverts if the checkpoint is not improper', async () => { |
||||||
|
const message = `0x${Buffer.alloc(10).toString('hex')}`; |
||||||
|
await outbox.dispatch( |
||||||
|
INBOX_DOMAIN, |
||||||
|
utils.addressToBytes32(signer.address), |
||||||
|
message, |
||||||
|
); |
||||||
|
await outbox.checkpoint(); |
||||||
|
const [root, index] = await outbox.latestCheckpoint(); |
||||||
|
|
||||||
|
const signatures = await signCheckpoint( |
||||||
|
root, |
||||||
|
index.toNumber(), |
||||||
|
[validator0, validator1], // 2/2 signers, making a quorum
|
||||||
|
); |
||||||
|
|
||||||
|
await expect( |
||||||
|
validatorManager.improperCheckpoint( |
||||||
|
outbox.address, |
||||||
|
root, |
||||||
|
index, |
||||||
|
signatures, |
||||||
|
), |
||||||
|
).to.be.revertedWith('!improper'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,30 @@ |
|||||||
|
import { types, Validator } from '@abacus-network/utils'; |
||||||
|
|
||||||
|
// Signs a checkpoint with the provided validators and returns
|
||||||
|
// the signatures sorted by validator addresses in ascending order
|
||||||
|
export async function signCheckpoint( |
||||||
|
root: types.HexString, |
||||||
|
index: number, |
||||||
|
unsortedValidators: Validator[], |
||||||
|
): Promise<string[]> { |
||||||
|
const validators = unsortedValidators.sort((a, b) => { |
||||||
|
// Remove the checksums for accurate comparison
|
||||||
|
const aAddress = a.address.toLowerCase(); |
||||||
|
const bAddress = b.address.toLowerCase(); |
||||||
|
|
||||||
|
if (aAddress < bAddress) { |
||||||
|
return -1; |
||||||
|
} else if (aAddress > bAddress) { |
||||||
|
return 1; |
||||||
|
} else { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
const signedCheckpoints = await Promise.all( |
||||||
|
validators.map((validator) => validator.signCheckpoint(root, index)), |
||||||
|
); |
||||||
|
return signedCheckpoints.map( |
||||||
|
(signedCheckpoint) => signedCheckpoint.signature, |
||||||
|
); |
||||||
|
} |
@ -1,150 +0,0 @@ |
|||||||
import { ethers } from 'hardhat'; |
|
||||||
import { expect } from 'chai'; |
|
||||||
import { types, utils, Validator } from '@abacus-network/utils'; |
|
||||||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; |
|
||||||
|
|
||||||
import { |
|
||||||
Outbox__factory, |
|
||||||
Outbox, |
|
||||||
ValidatorManager__factory, |
|
||||||
ValidatorManager, |
|
||||||
} from '../types'; |
|
||||||
|
|
||||||
const domainHashCases = require('../../../vectors/domainHash.json'); |
|
||||||
const localDomain = 1000; |
|
||||||
|
|
||||||
describe('ValidatorManager', async () => { |
|
||||||
let signer: SignerWithAddress, |
|
||||||
fakeSigner: SignerWithAddress, |
|
||||||
validatorManager: ValidatorManager, |
|
||||||
validator: Validator, |
|
||||||
fakeValidator: Validator; |
|
||||||
|
|
||||||
before(async () => { |
|
||||||
[signer, fakeSigner] = await ethers.getSigners(); |
|
||||||
validator = await Validator.fromSigner(signer, localDomain); |
|
||||||
fakeValidator = await Validator.fromSigner(fakeSigner, localDomain); |
|
||||||
}); |
|
||||||
|
|
||||||
beforeEach(async () => { |
|
||||||
const validatorManagerFactory = new ValidatorManager__factory(signer); |
|
||||||
validatorManager = await validatorManagerFactory.deploy(); |
|
||||||
await validatorManager.enrollValidator(localDomain, validator.address); |
|
||||||
}); |
|
||||||
|
|
||||||
it('Accepts validator signature', async () => { |
|
||||||
const root = ethers.utils.formatBytes32String('root'); |
|
||||||
const index = 1; |
|
||||||
|
|
||||||
const { signature } = await validator.signCheckpoint(root, index); |
|
||||||
const isValid = await validatorManager.isValidatorSignature( |
|
||||||
localDomain, |
|
||||||
root, |
|
||||||
index, |
|
||||||
signature, |
|
||||||
); |
|
||||||
expect(isValid).to.be.true; |
|
||||||
}); |
|
||||||
|
|
||||||
it('Rejects non-validator signature', async () => { |
|
||||||
const root = ethers.utils.formatBytes32String('root'); |
|
||||||
const index = 1; |
|
||||||
|
|
||||||
const { signature } = await fakeValidator.signCheckpoint(root, index); |
|
||||||
const isValid = await validatorManager.isValidatorSignature( |
|
||||||
localDomain, |
|
||||||
root, |
|
||||||
index, |
|
||||||
signature, |
|
||||||
); |
|
||||||
expect(isValid).to.be.false; |
|
||||||
}); |
|
||||||
|
|
||||||
it('Calculated domain hash matches Rust-produced domain hash', async () => { |
|
||||||
// Compare Rust output in json file to solidity output (json file matches
|
|
||||||
// hash for local domain of 1000)
|
|
||||||
for (let testCase of domainHashCases) { |
|
||||||
const { expectedDomainHash } = testCase; |
|
||||||
const domainHash = await validatorManager.domainHash( |
|
||||||
testCase.outboxDomain, |
|
||||||
); |
|
||||||
expect(domainHash).to.equal(expectedDomainHash); |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
describe('improper checkpoints', async () => { |
|
||||||
let outbox: Outbox; |
|
||||||
beforeEach(async () => { |
|
||||||
const outboxFactory = new Outbox__factory(signer); |
|
||||||
outbox = await outboxFactory.deploy(localDomain); |
|
||||||
await outbox.initialize(validatorManager.address); |
|
||||||
}); |
|
||||||
|
|
||||||
it('Accepts improper checkpoint from validator', async () => { |
|
||||||
const root = ethers.utils.formatBytes32String('root'); |
|
||||||
const index = 1; |
|
||||||
|
|
||||||
const { signature } = await validator.signCheckpoint(root, index); |
|
||||||
// Send message with signer address as msg.sender
|
|
||||||
await expect( |
|
||||||
validatorManager.improperCheckpoint( |
|
||||||
outbox.address, |
|
||||||
root, |
|
||||||
index, |
|
||||||
signature, |
|
||||||
), |
|
||||||
) |
|
||||||
.to.emit(validatorManager, 'ImproperCheckpoint') |
|
||||||
.withArgs( |
|
||||||
outbox.address, |
|
||||||
localDomain, |
|
||||||
validator.address, |
|
||||||
root, |
|
||||||
index, |
|
||||||
signature, |
|
||||||
); |
|
||||||
expect(await outbox.state()).to.equal(types.AbacusState.FAILED); |
|
||||||
}); |
|
||||||
|
|
||||||
it('Rejects improper checkpoint from non-validator', async () => { |
|
||||||
const root = ethers.utils.formatBytes32String('root'); |
|
||||||
const index = 1; |
|
||||||
|
|
||||||
const { signature } = await fakeValidator.signCheckpoint(root, index); |
|
||||||
// Send message with signer address as msg.sender
|
|
||||||
await expect( |
|
||||||
validatorManager.improperCheckpoint( |
|
||||||
outbox.address, |
|
||||||
root, |
|
||||||
index, |
|
||||||
signature, |
|
||||||
), |
|
||||||
).to.be.revertedWith('!validator sig'); |
|
||||||
}); |
|
||||||
|
|
||||||
it('Rejects proper checkpoint from validator', async () => { |
|
||||||
const message = `0x${Buffer.alloc(10).toString('hex')}`; |
|
||||||
await outbox.dispatch( |
|
||||||
localDomain, |
|
||||||
utils.addressToBytes32(signer.address), |
|
||||||
message, |
|
||||||
); |
|
||||||
await outbox.checkpoint(); |
|
||||||
const [root, index] = await outbox.latestCheckpoint(); |
|
||||||
|
|
||||||
const { signature } = await validator.signCheckpoint( |
|
||||||
root, |
|
||||||
index.toNumber(), |
|
||||||
); |
|
||||||
// Send message with signer address as msg.sender
|
|
||||||
await expect( |
|
||||||
validatorManager.improperCheckpoint( |
|
||||||
outbox.address, |
|
||||||
root, |
|
||||||
index, |
|
||||||
signature, |
|
||||||
), |
|
||||||
).to.be.revertedWith('!improper'); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,11 +1,23 @@ |
|||||||
import { CoreConfig } from '../../../src/core'; |
import { CoreConfig } from '../../../src/core'; |
||||||
|
|
||||||
export const core: CoreConfig = { |
export const core: CoreConfig = { |
||||||
validators: { |
validatorManagers: { |
||||||
// Hardhat accounts 1-4
|
// Hardhat accounts 1-4
|
||||||
alfajores: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', |
alfajores: { |
||||||
fuji: '0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc', |
validators: ['0x70997970c51812dc3a010c7d01b50e0d17dc79c8'], |
||||||
kovan: '0x90f79bf6eb2c4f870365e785982e1f101e93b906', |
threshold: 1, |
||||||
mumbai: '0x15d34aaf54267db7d7c367839aaf71a00a2c6a65', |
}, |
||||||
|
fuji: { |
||||||
|
validators: ['0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc'], |
||||||
|
threshold: 1, |
||||||
|
}, |
||||||
|
kovan: { |
||||||
|
validators: ['0x90f79bf6eb2c4f870365e785982e1f101e93b906'], |
||||||
|
threshold: 1, |
||||||
|
}, |
||||||
|
mumbai: { |
||||||
|
validators: ['0x15d34aaf54267db7d7c367839aaf71a00a2c6a65'], |
||||||
|
threshold: 1, |
||||||
|
}, |
||||||
}, |
}, |
||||||
}; |
}; |
||||||
|
@ -1,6 +1,11 @@ |
|||||||
import { types } from '@abacus-network/utils'; |
import { types } from '@abacus-network/utils'; |
||||||
import { ChainName } from '@abacus-network/sdk'; |
import { ChainName } from '@abacus-network/sdk'; |
||||||
|
|
||||||
|
export type ValidatorManagerConfig = { |
||||||
|
validators: Array<types.Address>; |
||||||
|
threshold: number; |
||||||
|
}; |
||||||
|
|
||||||
export type CoreConfig = { |
export type CoreConfig = { |
||||||
validators: Partial<Record<ChainName, types.Address>>; |
validatorManagers: Partial<Record<ChainName, ValidatorManagerConfig>>; |
||||||
}; |
}; |
||||||
|
Loading…
Reference in new issue