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'; |
||||
|
||||
export const core: CoreConfig = { |
||||
validators: { |
||||
validatorManagers: { |
||||
// Hardhat accounts 1-4
|
||||
alfajores: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', |
||||
fuji: '0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc', |
||||
kovan: '0x90f79bf6eb2c4f870365e785982e1f101e93b906', |
||||
mumbai: '0x15d34aaf54267db7d7c367839aaf71a00a2c6a65', |
||||
alfajores: { |
||||
validators: ['0x70997970c51812dc3a010c7d01b50e0d17dc79c8'], |
||||
threshold: 1, |
||||
}, |
||||
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 { ChainName } from '@abacus-network/sdk'; |
||||
|
||||
export type ValidatorManagerConfig = { |
||||
validators: Array<types.Address>; |
||||
threshold: number; |
||||
}; |
||||
|
||||
export type CoreConfig = { |
||||
validators: Partial<Record<ChainName, types.Address>>; |
||||
validatorManagers: Partial<Record<ChainName, ValidatorManagerConfig>>; |
||||
}; |
||||
|
Loading…
Reference in new issue