pull/334/head
parent
220b684b29
commit
dc0a64b82b
@ -0,0 +1,204 @@ |
||||
// SPDX-License-Identifier: MIT OR Apache-2.0 |
||||
pragma solidity >=0.6.11; |
||||
pragma abicoder v2; |
||||
|
||||
// ============ Internal Imports ============ |
||||
import {Inbox} from "../Inbox.sol"; |
||||
import {Outbox} from "../Outbox.sol"; |
||||
// ============ 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"; |
||||
|
||||
contract CommonMultisigValidatorManager is Ownable { |
||||
// ============ Libraries ============ |
||||
|
||||
using EnumerableSet for EnumerableSet.AddressSet; |
||||
|
||||
// ============ Immutables ============ |
||||
|
||||
// The domain of the outbox the set of validators this validator manager |
||||
// tracks is for. |
||||
uint32 public immutable outboxDomain; |
||||
|
||||
bytes32 public immutable outboxDomainHash; |
||||
|
||||
// ============ Mutable Storage ============ |
||||
|
||||
// The minimum threshold of validator signatures to constitute a quorum |
||||
uint256 public threshold; |
||||
|
||||
// The set of validators |
||||
EnumerableSet.AddressSet private validators; |
||||
|
||||
// ============ Events ============ |
||||
|
||||
/** |
||||
* @notice Emitted when a validator is enrolled in the validator set. |
||||
* @param validator The address of the validator. |
||||
*/ |
||||
event EnrollValidator(address indexed validator); |
||||
|
||||
/** |
||||
* @notice Emitted when a validator is unenrolled in the validator set. |
||||
* @param validator The address of the validator. |
||||
*/ |
||||
event UnenrollValidator(address indexed validator); |
||||
|
||||
/** |
||||
* @notice Emitted when the quorum threshold is set. |
||||
* @param threshold The quorum threshold. |
||||
*/ |
||||
event SetThreshold(uint256 threshold); |
||||
|
||||
/** |
||||
* @notice Emitted when proof of an improper checkpoint is submitted. |
||||
* @param root Root of the improper checkpoint. |
||||
* @param index Index of the improper checkpoint. |
||||
* @param signatures A quorum of signatures on the improper checkpoint. |
||||
*/ |
||||
event ImproperCheckpoint( |
||||
address indexed outbox, |
||||
bytes32 indexed root, |
||||
uint256 index, |
||||
bytes[] signatures |
||||
); |
||||
|
||||
// ============ Constructor ============ |
||||
|
||||
/** |
||||
* @param _outboxDomain The domain of the outbox this validator manager |
||||
* tracks the validator set for. |
||||
*/ |
||||
constructor(uint32 _outboxDomain) Ownable() { |
||||
outboxDomain = _outboxDomain; |
||||
outboxDomainHash = keccak256(abi.encodePacked(_outboxDomain, "ABACUS")); |
||||
} |
||||
|
||||
// ============ External Functions ============ |
||||
|
||||
// Adds _validator to validators |
||||
function enrollValidator(address _validator) external onlyOwner { |
||||
// Revert if _validator is already an enrolled validator. |
||||
require(validators.add(_validator), "!unenrolled"); |
||||
emit EnrollValidator(_validator); |
||||
} |
||||
|
||||
// Removes _validator from validators |
||||
function unenrollValidator(address _validator) external onlyOwner { |
||||
// Revert if _validator is not an already enrolled validator. |
||||
require(validators.remove(_validator), "!enrolled"); |
||||
emit UnenrollValidator(_validator); |
||||
} |
||||
|
||||
function setThreshold(uint256 _threshold) external onlyOwner { |
||||
threshold = _threshold; |
||||
emit SetThreshold(_threshold); |
||||
} |
||||
|
||||
// Gets the domain from IInbox(_inbox).localDomain(), then |
||||
// requires isQuorum(domain, _root, _index, _signatures), |
||||
// and then calls IInbox(_inbox).checkpoint(_root, _index); |
||||
function checkpoint( |
||||
Inbox _inbox, |
||||
bytes32 _root, |
||||
uint256 _index, |
||||
bytes[] calldata _signatures |
||||
) external { |
||||
require(isQuorum(_root, _index, _signatures), "!quorum"); |
||||
_inbox.checkpoint(_root, _index); |
||||
} |
||||
|
||||
// Determines if a quorum of signers have signed an improper checkpoint, |
||||
// and fails the Outbox if so. |
||||
// If staking / slashing existed, we'd want to check this for individual validator |
||||
// signatures. Because we don't care about that and we don't want a single byzantine |
||||
// validator to be able to fail the outbox, we require a quorum. |
||||
// |
||||
// Gets the domain from IOutbox(_outbox).localDomain(), then |
||||
// requires isQuorum(domain, _root, _index, _signatures), |
||||
// requires that the checkpoint is an improper checkpoint, |
||||
// and calls IOutbox(_outbox).fail(). (Similar behavior as existing improperCheckpoint) |
||||
function improperCheckpoint( |
||||
Outbox _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); |
||||
} |
||||
|
||||
// Just returns the addresses in the private enumerable set `validators`. |
||||
function validatorSet() external view returns (address[] memory) { |
||||
uint256 _length = validators.length(); |
||||
address[] memory _validatorSet = new address[](_length); |
||||
for (uint256 i = 0; i < _length; i++) { |
||||
_validatorSet[i] = validators.at(i); |
||||
} |
||||
return _validatorSet; |
||||
} |
||||
|
||||
// ============ Public Functions ============ |
||||
|
||||
// Returns whether the provided signatures over the checkpoint for the domain |
||||
// constitute a quorum of validator signatures. |
||||
// Requires each signature to be over the given domain, root, and index. |
||||
// Requires _signatures to be sorted by their recovered signer's address for duplicate detection. |
||||
// Requires each recovered signer to be in the `validators` set. |
||||
// Requires _signatures.length to be >= threshold. |
||||
function isQuorum( |
||||
bytes32 _root, |
||||
uint256 _index, |
||||
bytes[] calldata _signatures |
||||
) public view returns (bool) { |
||||
uint256 _signaturesLength = _signatures.length; |
||||
// If there are less signatures provided than the required threshold, |
||||
// this is not a quorum. |
||||
if (_signaturesLength < threshold) { |
||||
return false; |
||||
} |
||||
// To identify duplicates, the signers recovered from _signatures |
||||
// must be sorted in ascending order. previousSigner is used to |
||||
// enforce sort order. |
||||
address _previousSigner = address(0); |
||||
uint256 _validatorSignatureCount = 0; |
||||
for (uint256 i = 0; i < _signaturesLength; 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 (validators.contains(_signer)) { |
||||
_validatorSignatureCount++; |
||||
} |
||||
} |
||||
return _validatorSignatureCount >= threshold; |
||||
} |
||||
|
||||
// ============ 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(outboxDomainHash, _root, _index) |
||||
); |
||||
_digest = ECDSA.toEthSignedMessageHash(_digest); |
||||
return ECDSA.recover(_digest, _signature); |
||||
} |
||||
} |
Loading…
Reference in new issue