Update multisig ISMs for merkle hooks (#2722)

Fixes https://github.com/hyperlane-xyz/issues/issues/591
pull/2736/head
Yorke Rhodes 1 year ago
parent f4b12a438b
commit f7dbc71335
No known key found for this signature in database
GPG Key ID: 9EEACF1DA75C5627
  1. 29
      solidity/contracts/isms/multisig/AbstractMerkleRootMultisigIsm.sol
  2. 17
      solidity/contracts/isms/multisig/AbstractMessageIdMultisigIsm.sol
  3. 2
      solidity/contracts/isms/multisig/AbstractMultisigIsm.sol
  4. 359
      solidity/contracts/isms/multisig/LegacyMultisigIsm.sol
  5. 25
      solidity/contracts/libs/CheckpointLib.sol
  6. 50
      solidity/contracts/libs/LegacyCheckpointLib.sol
  7. 170
      solidity/contracts/libs/isms/LegacyMultisigIsmMetadata.sol
  8. 60
      solidity/contracts/libs/isms/MerkleRootMultisigIsmMetadata.sol
  9. 31
      solidity/contracts/libs/isms/MessageIdMultisigIsmMetadata.sol
  10. 16
      solidity/contracts/test/TestLegacyMultisigIsm.sol
  11. 40
      solidity/test/isms/MultisigIsm.t.sol
  12. 595
      solidity/test/isms/legacyMultisigIsm.test.ts

@ -23,6 +23,9 @@ import {CheckpointLib} from "../../libs/CheckpointLib.sol";
* @dev May be adapted in future to support batch message verification against a single root.
*/
abstract contract AbstractMerkleRootMultisigIsm is AbstractMultisigIsm {
using MerkleRootMultisigIsmMetadata for bytes;
using Message for bytes;
// ============ Constants ============
// solhint-disable-next-line const-name-snakecase
@ -38,20 +41,24 @@ abstract contract AbstractMerkleRootMultisigIsm is AbstractMultisigIsm {
override
returns (bytes32)
{
require(
_metadata.messageIndex() <= _metadata.signedIndex(),
"Invalid merkle index metadata"
);
// We verify a merkle proof of (messageId, index) I to compute root J
bytes32 _root = MerkleLib.branchRoot(
Message.id(_message),
MerkleRootMultisigIsmMetadata.proof(_metadata),
Message.nonce(_message)
bytes32 _signedRoot = MerkleLib.branchRoot(
_message.id(),
_metadata.proof(),
_metadata.messageIndex()
);
// We provide (messageId, index) J in metadata for digest derivation
return
CheckpointLib.digest(
Message.origin(_message),
MerkleRootMultisigIsmMetadata.originMailbox(_metadata),
_root,
MerkleRootMultisigIsmMetadata.index(_metadata),
MerkleRootMultisigIsmMetadata.messageId(_metadata)
_message.origin(),
_metadata.originMerkleTreeHook(),
_signedRoot,
_metadata.signedIndex(),
_metadata.signedMessageId()
);
}
@ -63,8 +70,8 @@ abstract contract AbstractMerkleRootMultisigIsm is AbstractMultisigIsm {
pure
virtual
override
returns (bytes memory signature)
returns (bytes calldata)
{
return MerkleRootMultisigIsmMetadata.signatureAt(_metadata, _index);
return _metadata.signatureAt(_index);
}
}

@ -19,6 +19,9 @@ import {CheckpointLib} from "../../libs/CheckpointLib.sol";
* This abstract contract can be customized to change the `validatorsAndThreshold()` (static or dynamic).
*/
abstract contract AbstractMessageIdMultisigIsm is AbstractMultisigIsm {
using Message for bytes;
using MessageIdMultisigIsmMetadata for bytes;
// ============ Constants ============
// solhint-disable-next-line const-name-snakecase
@ -36,11 +39,11 @@ abstract contract AbstractMessageIdMultisigIsm is AbstractMultisigIsm {
{
return
CheckpointLib.digest(
Message.origin(_message),
MessageIdMultisigIsmMetadata.originMailbox(_metadata),
MessageIdMultisigIsmMetadata.root(_metadata),
Message.nonce(_message),
Message.id(_message)
_message.origin(),
_metadata.originMerkleTreeHook(),
_metadata.root(),
_metadata.index(),
_message.id()
);
}
@ -52,8 +55,8 @@ abstract contract AbstractMessageIdMultisigIsm is AbstractMultisigIsm {
pure
virtual
override
returns (bytes memory)
returns (bytes calldata)
{
return MessageIdMultisigIsmMetadata.signatureAt(_metadata, _index);
return _metadata.signatureAt(_index);
}
}

@ -58,7 +58,7 @@ abstract contract AbstractMultisigIsm is IMultisigIsm {
internal
pure
virtual
returns (bytes memory);
returns (bytes calldata);
// ============ Public Functions ============

@ -1,359 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
// ============ External Imports ============
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
// ============ Internal Imports ============
import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol";
import {Message} from "../../libs/Message.sol";
import {IMultisigIsm} from "../../interfaces/isms/IMultisigIsm.sol";
import {LegacyMultisigIsmMetadata} from "../../libs/isms/LegacyMultisigIsmMetadata.sol";
import {MerkleLib} from "../../libs/Merkle.sol";
import {LegacyCheckpointLib} from "../../libs/LegacyCheckpointLib.sol";
/**
* @title MultisigIsm
* @notice Manages an ownable set of validators that ECDSA sign checkpoints to
* reach a quorum.
*/
contract LegacyMultisigIsm is IMultisigIsm, Ownable {
// ============ Libraries ============
using EnumerableSet for EnumerableSet.AddressSet;
using Message for bytes;
using LegacyMultisigIsmMetadata for bytes;
using MerkleLib for MerkleLib.Tree;
// ============ Constants ============
// solhint-disable-next-line const-name-snakecase
uint8 public constant moduleType =
uint8(IInterchainSecurityModule.Types.LEGACY_MULTISIG);
// ============ Mutable Storage ============
/// @notice The validator threshold for each remote domain.
mapping(uint32 => uint8) public threshold;
/// @notice The validator set for each remote domain.
mapping(uint32 => EnumerableSet.AddressSet) private validatorSet;
/// @notice A succinct commitment to the validator set and threshold for each remote
/// domain.
mapping(uint32 => bytes32) public commitment;
// ============ Events ============
/**
* @notice Emitted when a validator is enrolled in a validator set.
* @param domain The remote domain of the validator set.
* @param validator The address of the validator.
* @param validatorCount The number of enrolled validators in the validator set.
*/
event ValidatorEnrolled(
uint32 indexed domain,
address indexed validator,
uint256 validatorCount
);
/**
* @notice Emitted when a validator is unenrolled from a validator set.
* @param domain The remote domain of the validator set.
* @param validator The address of the validator.
* @param validatorCount The number of enrolled validators in the validator set.
*/
event ValidatorUnenrolled(
uint32 indexed domain,
address indexed validator,
uint256 validatorCount
);
/**
* @notice Emitted when the quorum threshold is set.
* @param domain The remote domain of the validator set.
* @param threshold The new quorum threshold.
*/
event ThresholdSet(uint32 indexed domain, uint8 threshold);
/**
* @notice Emitted when the validator set or threshold changes.
* @param domain The remote domain of the validator set.
* @param commitment A commitment to the validator set and threshold.
*/
event CommitmentUpdated(uint32 domain, bytes32 commitment);
// ============ Constructor ============
// solhint-disable-next-line no-empty-blocks
constructor() Ownable() {}
// ============ External Functions ============
/**
* @notice Enrolls multiple validators into a validator set.
* @dev Reverts if `_validator` is already in the validator set.
* @param _domains The remote domains of the validator sets.
* @param _validators The validators to add to the validator sets.
* @dev _validators[i] are the validators to enroll for _domains[i].
*/
function enrollValidators(
uint32[] calldata _domains,
address[][] calldata _validators
) external onlyOwner {
uint256 domainsLength = _domains.length;
require(domainsLength == _validators.length, "!length");
for (uint256 i = 0; i < domainsLength; i += 1) {
address[] calldata _domainValidators = _validators[i];
uint256 validatorsLength = _domainValidators.length;
for (uint256 j = 0; j < validatorsLength; j += 1) {
_enrollValidator(_domains[i], _domainValidators[j]);
}
_updateCommitment(_domains[i]);
}
}
/**
* @notice Enrolls a validator into a validator set.
* @dev Reverts if `_validator` is already in the validator set.
* @param _domain The remote domain of the validator set.
* @param _validator The validator to add to the validator set.
*/
function enrollValidator(uint32 _domain, address _validator)
external
onlyOwner
{
_enrollValidator(_domain, _validator);
_updateCommitment(_domain);
}
/**
* @notice Unenrolls a validator from a validator set.
* @dev Reverts if `_validator` is not in the validator set.
* @param _domain The remote domain of the validator set.
* @param _validator The validator to remove from the validator set.
*/
function unenrollValidator(uint32 _domain, address _validator)
external
onlyOwner
{
require(validatorSet[_domain].remove(_validator), "!enrolled");
uint256 _validatorCount = validatorCount(_domain);
require(
_validatorCount >= threshold[_domain],
"violates quorum threshold"
);
_updateCommitment(_domain);
emit ValidatorUnenrolled(_domain, _validator, _validatorCount);
}
/**
* @notice Sets the quorum threshold for multiple domains.
* @param _domains The remote domains of the validator sets.
* @param _thresholds The new quorum thresholds.
*/
function setThresholds(
uint32[] calldata _domains,
uint8[] calldata _thresholds
) external onlyOwner {
uint256 length = _domains.length;
require(length == _thresholds.length, "!length");
for (uint256 i = 0; i < length; i += 1) {
setThreshold(_domains[i], _thresholds[i]);
}
}
/**
* @notice Returns whether an address is enrolled in a validator set.
* @param _domain The remote domain of the validator set.
* @param _address The address to test for set membership.
* @return True if the address is enrolled, false otherwise.
*/
function isEnrolled(uint32 _domain, address _address)
external
view
returns (bool)
{
EnumerableSet.AddressSet storage _validatorSet = validatorSet[_domain];
return _validatorSet.contains(_address);
}
// ============ Public Functions ============
/**
* @notice Sets the quorum threshold.
* @param _domain The remote domain of the validator set.
* @param _threshold The new quorum threshold.
*/
function setThreshold(uint32 _domain, uint8 _threshold) public onlyOwner {
require(
_threshold > 0 && _threshold <= validatorCount(_domain),
"!range"
);
threshold[_domain] = _threshold;
emit ThresholdSet(_domain, _threshold);
_updateCommitment(_domain);
}
/**
* @notice Verifies that a quorum of the origin domain's validators signed
* a checkpoint, and verifies the merkle proof of `_message` against that
* checkpoint.
* @param _metadata ABI encoded module metadata (see LegacyMultisigIsmMetadata.sol)
* @param _message Formatted Hyperlane message (see Message.sol).
*/
function verify(bytes calldata _metadata, bytes calldata _message)
external
view
returns (bool)
{
require(_verifyMerkleProof(_metadata, _message), "!merkle");
require(_verifyValidatorSignatures(_metadata, _message), "!sigs");
return true;
}
/**
* @notice Gets the current validator set
* @param _domain The remote domain of the validator set.
* @return The addresses of the validator set.
*/
function validators(uint32 _domain) public view returns (address[] memory) {
EnumerableSet.AddressSet storage _validatorSet = validatorSet[_domain];
uint256 _validatorCount = _validatorSet.length();
address[] memory _validators = new address[](_validatorCount);
for (uint256 i = 0; i < _validatorCount; i++) {
_validators[i] = _validatorSet.at(i);
}
return _validators;
}
/**
* @notice Returns the set of validators responsible for verifying _message
* and the number of signatures required
* @dev Can change based on the content of _message
* @param _message Hyperlane formatted interchain message
* @return validators The array of validator addresses
* @return threshold The number of validator signatures needed
*/
function validatorsAndThreshold(bytes calldata _message)
external
view
returns (address[] memory, uint8)
{
uint32 _origin = _message.origin();
address[] memory _validators = validators(_origin);
uint8 _threshold = threshold[_origin];
return (_validators, _threshold);
}
/**
* @notice Returns the number of validators enrolled in the validator set.
* @param _domain The remote domain of the validator set.
* @return The number of validators enrolled in the validator set.
*/
function validatorCount(uint32 _domain) public view returns (uint256) {
return validatorSet[_domain].length();
}
// ============ Internal Functions ============
/**
* @notice Enrolls a validator into a validator set.
* @dev Reverts if `_validator` is already in the validator set.
* @param _domain The remote domain of the validator set.
* @param _validator The validator to add to the validator set.
*/
function _enrollValidator(uint32 _domain, address _validator) internal {
require(_validator != address(0), "zero address");
require(validatorSet[_domain].add(_validator), "already enrolled");
emit ValidatorEnrolled(_domain, _validator, validatorCount(_domain));
}
/**
* @notice Updates the commitment to the validator set for `_domain`.
* @param _domain The remote domain of the validator set.
* @return The commitment to the validator set for `_domain`.
*/
function _updateCommitment(uint32 _domain) internal returns (bytes32) {
address[] memory _validators = validators(_domain);
uint8 _threshold = threshold[_domain];
bytes32 _commitment = keccak256(
abi.encodePacked(_threshold, _validators)
);
commitment[_domain] = _commitment;
emit CommitmentUpdated(_domain, _commitment);
return _commitment;
}
/**
* @notice Verifies the merkle proof of `_message` against the provided
* checkpoint.
* @param _metadata ABI encoded module metadata (see LegacyMultisigIsmMetadata.sol)
* @param _message Formatted Hyperlane message (see Message.sol).
*/
function _verifyMerkleProof(
bytes calldata _metadata,
bytes calldata _message
) internal pure returns (bool) {
// calculate the expected root based on the proof
bytes32 _calculatedRoot = MerkleLib.branchRoot(
_message.id(),
_metadata.proof(),
_message.nonce()
);
return _calculatedRoot == _metadata.root();
}
/**
* @notice Verifies that a quorum of the origin domain's validators signed
* the provided checkpoint.
* @param _metadata ABI encoded module metadata (see LegacyMultisigIsmMetadata.sol)
* @param _message Formatted Hyperlane message (see Message.sol).
*/
function _verifyValidatorSignatures(
bytes calldata _metadata,
bytes calldata _message
) internal view returns (bool) {
uint8 _threshold = _metadata.threshold();
bytes32 _digest;
{
uint32 _origin = _message.origin();
bytes32 _commitment = keccak256(
abi.encodePacked(_threshold, _metadata.validators())
);
// Ensures the validator set encoded in the metadata matches
// what we've stored on chain.
// NB: An empty validator set in `_metadata` will result in a
// non-zero computed commitment, and this check will fail
// as the commitment in storage will be zero.
require(_commitment == commitment[_origin], "!commitment");
_digest = LegacyCheckpointLib.digest(
_origin,
LegacyMultisigIsmMetadata.originMailbox(_metadata),
LegacyMultisigIsmMetadata.root(_metadata),
LegacyMultisigIsmMetadata.index(_metadata)
);
}
uint256 _validatorCount = _metadata.validatorCount();
uint256 _validatorIndex = 0;
// Assumes that signatures are ordered by validator
for (uint256 i = 0; i < _threshold; ++i) {
address _signer = ECDSA.recover(_digest, _metadata.signatureAt(i));
// Loop through remaining validators until we find a match
while (
_validatorIndex < _validatorCount &&
_signer != _metadata.validatorAt(_validatorIndex)
) {
++_validatorIndex;
}
// Fail if we never found a match
require(_validatorIndex < _validatorCount, "!threshold");
++_validatorIndex;
}
return true;
}
}

@ -4,13 +4,11 @@ pragma solidity >=0.8.0;
// ============ External Imports ============
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {LegacyCheckpointLib} from "./LegacyCheckpointLib.sol";
library CheckpointLib {
/**
* @notice Returns the digest validators are expected to sign when signing checkpoints.
* @param _origin The origin domain of the checkpoint.
* @param _originMailbox The address of the origin mailbox as bytes32.
* @param _originMerkleTree The address of the origin merkle tree hook as bytes32.
* @param _checkpointRoot The root of the checkpoint.
* @param _checkpointIndex The index of the checkpoint.
* @param _messageId The message ID of the checkpoint.
@ -19,12 +17,12 @@ library CheckpointLib {
*/
function digest(
uint32 _origin,
bytes32 _originMailbox,
bytes32 _originMerkleTree,
bytes32 _checkpointRoot,
uint32 _checkpointIndex,
bytes32 _messageId
) internal pure returns (bytes32) {
bytes32 _domainHash = domainHash(_origin, _originMailbox);
bytes32 _domainHash = domainHash(_origin, _originMerkleTree);
return
ECDSA.toEthSignedMessageHash(
keccak256(
@ -42,21 +40,22 @@ library CheckpointLib {
* @notice Returns the domain hash that validators are expected to use
* when signing checkpoints.
* @param _origin The origin domain of the checkpoint.
* @param _originMailbox The address of the origin mailbox as bytes32.
* @param _originMerkleTree The address of the origin merkle tree as bytes32.
* @return The domain hash.
*/
function domainHash(uint32 _origin, bytes32 _originMailbox)
function domainHash(uint32 _origin, bytes32 _originMerkleTree)
internal
pure
returns (bytes32)
{
// Including the origin mailbox address in the signature allows the slashing
// protocol to enroll multiple mailboxes. Otherwise, a valid signature for
// mailbox A would be indistinguishable from a fraudulent signature for mailbox
// B.
// Including the origin merkle tree address in the signature allows the slashing
// protocol to enroll multiple trees. Otherwise, a valid signature for
// tree A would be indistinguishable from a fraudulent signature for tree B.
// The slashing protocol should slash if validators sign attestations for
// anything other than a whitelisted mailbox.
// anything other than a whitelisted tree.
return
keccak256(abi.encodePacked(_origin, _originMailbox, "HYPERLANE"));
keccak256(
abi.encodePacked(_origin, _originMerkleTree, "HYPERLANE")
);
}
}

@ -1,50 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
// ============ External Imports ============
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
// ============ Internal Imports ============
import {CheckpointLib} from "./CheckpointLib.sol";
library LegacyCheckpointLib {
/**
* @notice Returns the digest validators are expected to sign when signing legacy checkpoints.
* @param _origin The origin domain of the checkpoint.
* @param _originMailbox The address of the origin mailbox as bytes32.
* @return The digest of the legacy checkpoint.
*/
function digest(
uint32 _origin,
bytes32 _originMailbox,
bytes32 _checkpointRoot,
uint32 _checkpointIndex
) internal pure returns (bytes32) {
bytes32 _domainHash = domainHash(_origin, _originMailbox);
return
ECDSA.toEthSignedMessageHash(
keccak256(
abi.encodePacked(
_domainHash,
_checkpointRoot,
_checkpointIndex
)
)
);
}
/**
* @notice Returns the domain hash that validators are expected to use
* when signing checkpoints.
* @param _origin The origin domain of the checkpoint.
* @param _originMailbox The address of the origin mailbox as bytes32.
* @return The domain hash.
*/
function domainHash(uint32 _origin, bytes32 _originMailbox)
internal
pure
returns (bytes32)
{
return CheckpointLib.domainHash(_origin, _originMailbox);
}
}

@ -1,170 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
/**
* Format of metadata:
* [ 0: 32] Merkle root
* [ 32: 36] Root index
* [ 36: 68] Origin mailbox address
* [ 68:1092] Merkle proof
* [1092:1093] Threshold
* [1093:????] Validator signatures, 65 bytes each, length == Threshold
* [????:????] Addresses of the entire validator set, left padded to bytes32
*/
library LegacyMultisigIsmMetadata {
uint256 private constant MERKLE_ROOT_OFFSET = 0;
uint256 private constant MERKLE_INDEX_OFFSET = 32;
uint256 private constant ORIGIN_MAILBOX_OFFSET = 36;
uint256 private constant MERKLE_PROOF_OFFSET = 68;
uint256 private constant THRESHOLD_OFFSET = 1092;
uint256 private constant SIGNATURES_OFFSET = 1093;
uint256 private constant SIGNATURE_LENGTH = 65;
/**
* @notice Returns the merkle root of the signed checkpoint.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Merkle root of the signed checkpoint
*/
function root(bytes calldata _metadata) internal pure returns (bytes32) {
return bytes32(_metadata[MERKLE_ROOT_OFFSET:MERKLE_INDEX_OFFSET]);
}
/**
* @notice Returns the index of the signed checkpoint.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Index of the signed checkpoint
*/
function index(bytes calldata _metadata) internal pure returns (uint32) {
return
uint32(
bytes4(_metadata[MERKLE_INDEX_OFFSET:ORIGIN_MAILBOX_OFFSET])
);
}
/**
* @notice Returns the origin mailbox of the signed checkpoint as bytes32.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Origin mailbox of the signed checkpoint as bytes32
*/
function originMailbox(bytes calldata _metadata)
internal
pure
returns (bytes32)
{
return bytes32(_metadata[ORIGIN_MAILBOX_OFFSET:MERKLE_PROOF_OFFSET]);
}
/**
* @notice Returns the merkle proof branch of the message.
* @dev This appears to be more gas efficient than returning a calldata
* slice and using that.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Merkle proof branch of the message.
*/
function proof(bytes calldata _metadata)
internal
pure
returns (bytes32[32] memory)
{
return
abi.decode(
_metadata[MERKLE_PROOF_OFFSET:THRESHOLD_OFFSET],
(bytes32[32])
);
}
/**
* @notice Returns the number of required signatures. Verified against
* the commitment stored in the module.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return The number of required signatures.
*/
function threshold(bytes calldata _metadata) internal pure returns (uint8) {
return uint8(bytes1(_metadata[THRESHOLD_OFFSET:SIGNATURES_OFFSET]));
}
/**
* @notice Returns the validator ECDSA signature at `_index`.
* @dev Assumes signatures are sorted by validator
* @dev Assumes `_metadata` encodes `threshold` signatures.
* @dev Assumes `_index` is less than `threshold`
* @param _metadata ABI encoded Multisig ISM metadata.
* @param _index The index of the signature to return.
* @return The validator ECDSA signature at `_index`.
*/
function signatureAt(bytes calldata _metadata, uint256 _index)
internal
pure
returns (bytes calldata)
{
uint256 _start = SIGNATURES_OFFSET + (_index * SIGNATURE_LENGTH);
uint256 _end = _start + SIGNATURE_LENGTH;
return _metadata[_start:_end];
}
/**
* @notice Returns the validator address at `_index`.
* @dev Assumes `_index` is less than the number of validators
* @param _metadata ABI encoded Multisig ISM metadata.
* @param _index The index of the validator to return.
* @return The validator address at `_index`.
*/
function validatorAt(bytes calldata _metadata, uint256 _index)
internal
pure
returns (address)
{
// Validator addresses are left padded to bytes32 in order to match
// abi.encodePacked(address[]).
uint256 _start = _validatorsOffset(_metadata) + (_index * 32) + 12;
uint256 _end = _start + 20;
return address(bytes20(_metadata[_start:_end]));
}
/**
* @notice Returns the validator set encoded as bytes. Verified against the
* commitment stored in the module.
* @dev Validator addresses are encoded as tightly packed array of bytes32,
* sorted to match the enumerable set stored by the module.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return The validator set encoded as bytes.
*/
function validators(bytes calldata _metadata)
internal
pure
returns (bytes calldata)
{
return _metadata[_validatorsOffset(_metadata):];
}
/**
* @notice Returns the size of the validator set encoded in the metadata
* @dev Validator addresses are encoded as tightly packed array of bytes32,
* sorted to match the enumerable set stored by the module.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return The size of the validator set encoded in the metadata
*/
function validatorCount(bytes calldata _metadata)
internal
pure
returns (uint256)
{
return (_metadata.length - _validatorsOffset(_metadata)) / 32;
}
/**
* @notice Returns the offset in bytes of the list of validators within
* `_metadata`.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return The index at which the list of validators starts
*/
function _validatorsOffset(bytes calldata _metadata)
private
pure
returns (uint256)
{
return
SIGNATURES_OFFSET +
(uint256(threshold(_metadata)) * SIGNATURE_LENGTH);
}
}

@ -3,49 +3,59 @@ pragma solidity >=0.8.0;
/**
* Format of metadata:
* [ 0: 32] Origin mailbox address
* [ 32: 36] Signed checkpoint index
* [ 0: 32] Origin merkle tree address
* [ 32: 36] Index of message ID in merkle tree
* [ 36: 68] Signed checkpoint message ID
* [ 68:1092] Merkle proof
* [1092:????] Validator signatures (length := threshold * 65)
* [1092:1096] Signed checkpoint index (computed from proof and index)
* [1096:????] Validator signatures (length := threshold * 65)
*/
library MerkleRootMultisigIsmMetadata {
uint8 private constant ORIGIN_MAILBOX_OFFSET = 0;
uint8 private constant CHECKPOINT_INDEX_OFFSET = 32;
uint8 private constant CHECKPOINT_MESSAGE_ID_OFFSET = 36;
uint8 private constant ORIGIN_MERKLE_TREE_OFFSET = 0;
uint8 private constant MESSAGE_INDEX_OFFSET = 32;
uint8 private constant MESSAGE_ID_OFFSET = 36;
uint8 private constant MERKLE_PROOF_OFFSET = 68;
uint16 private constant MERKLE_PROOF_LENGTH = 32 * 32;
uint16 private constant SIGNATURES_OFFSET = 1092;
uint16 private constant SIGNED_INDEX_OFFSET = 1092;
uint16 private constant SIGNATURES_OFFSET = 1096;
uint8 private constant SIGNATURE_LENGTH = 65;
/**
* @notice Returns the origin mailbox of the signed checkpoint as bytes32.
* @notice Returns the origin merkle tree hook of the signed checkpoint as bytes32.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Origin mailbox of the signed checkpoint as bytes32
* @return Origin merkle tree hook of the signed checkpoint as bytes32
*/
function originMailbox(bytes calldata _metadata)
function originMerkleTreeHook(bytes calldata _metadata)
internal
pure
returns (bytes32)
{
return
bytes32(
_metadata[ORIGIN_MAILBOX_OFFSET:ORIGIN_MAILBOX_OFFSET + 32]
_metadata[ORIGIN_MERKLE_TREE_OFFSET:ORIGIN_MERKLE_TREE_OFFSET +
32]
);
}
/**
* @notice Returns the index of the signed checkpoint.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Index of the signed checkpoint
*/
function index(bytes calldata _metadata) internal pure returns (uint32) {
function messageIndex(bytes calldata _metadata)
internal
pure
returns (uint32)
{
return
uint32(
bytes4(
_metadata[CHECKPOINT_INDEX_OFFSET:CHECKPOINT_INDEX_OFFSET +
4]
)
bytes4(_metadata[MESSAGE_INDEX_OFFSET:MESSAGE_INDEX_OFFSET + 4])
);
}
function signedIndex(bytes calldata _metadata)
internal
pure
returns (uint32)
{
return
uint32(
bytes4(_metadata[SIGNED_INDEX_OFFSET:SIGNED_INDEX_OFFSET + 4])
);
}
@ -54,16 +64,12 @@ library MerkleRootMultisigIsmMetadata {
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Message ID of the signed checkpoint
*/
function messageId(bytes calldata _metadata)
function signedMessageId(bytes calldata _metadata)
internal
pure
returns (bytes32)
{
return
bytes32(
_metadata[CHECKPOINT_MESSAGE_ID_OFFSET:CHECKPOINT_MESSAGE_ID_OFFSET +
32]
);
return bytes32(_metadata[MESSAGE_ID_OFFSET:MESSAGE_ID_OFFSET + 32]);
}
/**

@ -3,29 +3,32 @@ pragma solidity >=0.8.0;
/**
* Format of metadata:
* [ 0: 32] Origin mailbox address
* [ 0: 32] Origin merkle tree address
* [ 32: 64] Signed checkpoint root
* [ 64:????] Validator signatures (length := threshold * 65)
* [ 64: 68] Signed checkpoint index
* [ 68:????] Validator signatures (length := threshold * 65)
*/
library MessageIdMultisigIsmMetadata {
uint8 private constant ORIGIN_MAILBOX_OFFSET = 0;
uint8 private constant ORIGIN_MERKLE_TREE_OFFSET = 0;
uint8 private constant MERKLE_ROOT_OFFSET = 32;
uint8 private constant SIGNATURES_OFFSET = 64;
uint8 private constant MERKLE_INDEX_OFFSET = 64;
uint8 private constant SIGNATURES_OFFSET = 68;
uint8 private constant SIGNATURE_LENGTH = 65;
/**
* @notice Returns the origin mailbox of the signed checkpoint as bytes32.
* @notice Returns the origin merkle tree hook of the signed checkpoint as bytes32.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Origin mailbox of the signed checkpoint as bytes32
* @return Origin merkle tree hook of the signed checkpoint as bytes32
*/
function originMailbox(bytes calldata _metadata)
function originMerkleTreeHook(bytes calldata _metadata)
internal
pure
returns (bytes32)
{
return
bytes32(
_metadata[ORIGIN_MAILBOX_OFFSET:ORIGIN_MAILBOX_OFFSET + 32]
_metadata[ORIGIN_MERKLE_TREE_OFFSET:ORIGIN_MERKLE_TREE_OFFSET +
32]
);
}
@ -38,6 +41,18 @@ library MessageIdMultisigIsmMetadata {
return bytes32(_metadata[MERKLE_ROOT_OFFSET:MERKLE_ROOT_OFFSET + 32]);
}
/**
* @notice Returns the merkle index of the signed checkpoint.
* @param _metadata ABI encoded Multisig ISM metadata.
* @return Merkle index of the signed checkpoint
*/
function index(bytes calldata _metadata) internal pure returns (uint32) {
return
uint32(
bytes4(_metadata[MERKLE_INDEX_OFFSET:MERKLE_INDEX_OFFSET + 4])
);
}
/**
* @notice Returns the validator ECDSA signature at `_index`.
* @dev Assumes signatures are sorted by validator

@ -1,16 +0,0 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.8.0;
// ============ Internal Imports ============
import {LegacyMultisigIsm} from "../isms/multisig/LegacyMultisigIsm.sol";
import {LegacyCheckpointLib} from "../libs/LegacyCheckpointLib.sol";
contract TestLegacyMultisigIsm is LegacyMultisigIsm {
function getDomainHash(uint32 _origin, bytes32 _originMailbox)
external
pure
returns (bytes32)
{
return LegacyCheckpointLib.domainHash(_origin, _originMailbox);
}
}

@ -19,6 +19,7 @@ import {MOfNTestUtils} from "./IsmTestUtils.sol";
/// @notice since we removed merkle tree from the mailbox, we need to include the MerkleTreeHook in the test
abstract contract AbstractMultisigIsmTest is Test {
using Message for bytes;
using TypeCasts for address;
uint32 constant ORIGIN = 11;
StaticMOfNAddressSetFactory factory;
@ -42,16 +43,14 @@ abstract contract AbstractMultisigIsmTest is Test {
uint32 domain = mailbox.localDomain();
uint256[] memory keys = addValidators(m, n, seed);
uint256[] memory signers = MOfNTestUtils.choose(m, keys, seed);
bytes32 mailboxAsBytes32 = TypeCasts.addressToBytes32(address(mailbox));
// bytes
bytes32 checkpointRoot = merkleTreeHook.root();
uint32 checkpointIndex = uint32(merkleTreeHook.count() - 1);
(bytes32 root, uint32 index) = merkleTreeHook.latestCheckpoint();
bytes32 messageId = message.id();
bytes32 digest = CheckpointLib.digest(
domain,
mailboxAsBytes32,
checkpointRoot,
checkpointIndex,
address(merkleTreeHook).addressToBytes32(),
root,
index,
messageId
);
bytes memory metadata = metadataPrefix(message);
@ -83,15 +82,7 @@ abstract contract AbstractMultisigIsmTest is Test {
bytes32 recipient,
bytes calldata body
) internal returns (bytes memory) {
uint8 version = mailbox.VERSION();
uint32 origin = mailbox.localDomain();
bytes32 sender = TypeCasts.addressToBytes32(address(this));
uint32 nonce = merkleTreeHook.count();
bytes memory message = Message.formatMessage(
version,
nonce,
origin,
sender,
bytes memory message = mailbox.buildOutboundMessage(
destination,
recipient,
body
@ -134,6 +125,7 @@ abstract contract AbstractMultisigIsmTest is Test {
}
contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest {
using TypeCasts for address;
using Message for bytes;
function setUp() public {
@ -145,6 +137,7 @@ contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest {
mailbox.setRequiredHook(address(noopHook));
}
// TODO: test merkleIndex != signedIndex
function metadataPrefix(bytes memory message)
internal
view
@ -152,19 +145,19 @@ contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest {
returns (bytes memory)
{
uint32 checkpointIndex = uint32(merkleTreeHook.count() - 1);
bytes32 mailboxAsBytes32 = TypeCasts.addressToBytes32(address(mailbox));
return
abi.encodePacked(
mailboxAsBytes32,
address(merkleTreeHook).addressToBytes32(),
checkpointIndex,
message.id(),
merkleTreeHook.proof()
merkleTreeHook.proof(),
checkpointIndex
);
}
}
contract MessageIdMultisigIsmTest is AbstractMultisigIsmTest {
using Message for bytes;
using TypeCasts for address;
function setUp() public {
mailbox = new TestMailbox(ORIGIN);
@ -182,7 +175,12 @@ contract MessageIdMultisigIsmTest is AbstractMultisigIsmTest {
override
returns (bytes memory)
{
bytes32 mailboxAsBytes32 = TypeCasts.addressToBytes32(address(mailbox));
return abi.encodePacked(mailboxAsBytes32, merkleTreeHook.root());
(bytes32 root, uint32 index) = merkleTreeHook.latestCheckpoint();
return
abi.encodePacked(
address(merkleTreeHook).addressToBytes32(),
root,
index
);
}
}

@ -1,595 +0,0 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { expect } from 'chai';
import { ethers } from 'hardhat';
import {
InterchainSecurityModuleType,
Validator,
addressToBytes32,
formatLegacyMultisigIsmMetadata,
parseLegacyMultisigIsmMetadata,
} from '@hyperlane-xyz/utils';
import domainHashTestCases from '../../../vectors/domainHash.json';
import {
LightTestRecipient__factory,
TestIsm__factory,
TestLegacyMultisigIsm,
TestLegacyMultisigIsm__factory,
TestMailbox,
TestMailbox__factory,
TestMerkleTreeHook,
TestMerkleTreeHook__factory,
TestPostDispatchHook,
TestPostDispatchHook__factory,
TestRecipient__factory,
} from '../../types';
import {
dispatchMessage,
dispatchMessageAndReturnMetadata,
getCommitment,
signCheckpoint,
} from '../lib/mailboxes';
const ORIGIN_DOMAIN = 1234;
const DESTINATION_DOMAIN = 4321;
describe('LegacyMultisigIsm', async () => {
let multisigIsm: TestLegacyMultisigIsm,
mailbox: TestMailbox,
defaultHook: TestMerkleTreeHook,
requiredHook: TestPostDispatchHook,
signer: SignerWithAddress,
nonOwner: SignerWithAddress,
validators: Validator[];
before(async () => {
const signers = await ethers.getSigners();
[signer, nonOwner] = signers;
const mailboxFactory = new TestMailbox__factory(signer);
mailbox = await mailboxFactory.deploy(ORIGIN_DOMAIN);
const defaultHookFactory = new TestMerkleTreeHook__factory(signer);
defaultHook = await defaultHookFactory.deploy(mailbox.address);
requiredHook = await new TestPostDispatchHook__factory(signer).deploy();
await requiredHook.setFee(0);
const testIsm = await new TestIsm__factory(signer).deploy();
mailbox.initialize(
signer.address,
testIsm.address,
defaultHook.address,
requiredHook.address,
);
validators = await Promise.all(
signers
.filter((_, i) => i > 1)
.map((s) => Validator.fromSigner(s, ORIGIN_DOMAIN, mailbox.address)),
);
});
beforeEach(async () => {
const multisigIsmFactory = new TestLegacyMultisigIsm__factory(signer);
multisigIsm = await multisigIsmFactory.deploy();
await mailbox.setDefaultIsm(multisigIsm.address);
});
describe('#constructor', () => {
it('sets the owner', async () => {
expect(await multisigIsm.owner()).to.equal(signer.address);
});
});
describe('#moduleType', () => {
it('returns the correct type', async () => {
expect(await multisigIsm.moduleType()).to.equal(
InterchainSecurityModuleType.MULTISIG,
);
});
});
describe('#enrollValidators', () => {
let validatorAddresses: string[];
const domains = [ORIGIN_DOMAIN, DESTINATION_DOMAIN];
before(async () => {
validatorAddresses = validators.map((v) => v.address);
});
it('enrolls validators into multiple validator sets', async () => {
await multisigIsm.enrollValidators(
domains,
domains.map(() => validatorAddresses),
);
await Promise.all(
domains.map(async (domain) => {
expect(await multisigIsm.validators(domain)).to.deep.equal(
validatorAddresses,
);
}),
);
});
it('emits the ValidatorEnrolled event', async () => {
expect(
await multisigIsm.enrollValidators(
domains,
domains.map(() => validatorAddresses),
),
)
.to.emit(multisigIsm, 'ValidatorEnrolled')
.withArgs(ORIGIN_DOMAIN, validatorAddresses[0], 1);
});
it('emits the CommitmentUpdated event', async () => {
const expectedCommitment = getCommitment(0, validatorAddresses);
expect(
await multisigIsm.enrollValidators(
domains,
domains.map(() => validatorAddresses),
),
)
.to.emit(multisigIsm, 'CommitmentUpdated')
.withArgs(ORIGIN_DOMAIN, expectedCommitment);
});
it('reverts when called by a non-owner', async () => {
await expect(
multisigIsm.connect(nonOwner).enrollValidators(
domains,
domains.map(() => validatorAddresses),
),
).to.be.revertedWith('Ownable: caller is not the owner');
});
});
describe('#enrollValidator', () => {
it('enrolls a validator into the validator set', async () => {
await multisigIsm.enrollValidator(ORIGIN_DOMAIN, validators[0].address);
expect(await multisigIsm.validators(ORIGIN_DOMAIN)).to.deep.equal([
validators[0].address,
]);
});
it('emits the ValidatorEnrolled event', async () => {
expect(
await multisigIsm.enrollValidator(ORIGIN_DOMAIN, validators[0].address),
)
.to.emit(multisigIsm, 'ValidatorEnrolled')
.withArgs(ORIGIN_DOMAIN, validators[0].address, 1);
});
it('emits the CommitmentUpdated event', async () => {
const expectedCommitment = getCommitment(0, [validators[0].address]);
expect(
await multisigIsm.enrollValidator(ORIGIN_DOMAIN, validators[0].address),
)
.to.emit(multisigIsm, 'CommitmentUpdated')
.withArgs(ORIGIN_DOMAIN, expectedCommitment);
});
it('reverts if the validator is already enrolled', async () => {
await multisigIsm.enrollValidator(ORIGIN_DOMAIN, validators[0].address);
await expect(
multisigIsm.enrollValidator(ORIGIN_DOMAIN, validators[0].address),
).to.be.revertedWith('already enrolled');
});
it('reverts when called by a non-owner', async () => {
await expect(
multisigIsm
.connect(nonOwner)
.enrollValidator(ORIGIN_DOMAIN, validators[0].address),
).to.be.revertedWith('Ownable: caller is not the owner');
});
});
describe('#unenrollValidator', () => {
beforeEach(async () => {
await multisigIsm.enrollValidator(ORIGIN_DOMAIN, validators[0].address);
});
it('unenrolls a validator from the validator set', async () => {
await multisigIsm.unenrollValidator(ORIGIN_DOMAIN, validators[0].address);
expect(await multisigIsm.validators(ORIGIN_DOMAIN)).to.deep.equal([]);
});
it('emits the ValidatorUnenrolled event', async () => {
expect(
await multisigIsm.unenrollValidator(
ORIGIN_DOMAIN,
validators[0].address,
),
)
.to.emit(multisigIsm, 'ValidatorUnenrolled')
.withArgs(ORIGIN_DOMAIN, validators[0].address, 0);
});
it('emits the CommitmentUpdated event', async () => {
const expectedCommitment = getCommitment(0, []);
expect(
await multisigIsm.unenrollValidator(
ORIGIN_DOMAIN,
validators[0].address,
),
)
.to.emit(multisigIsm, 'CommitmentUpdated')
.withArgs(ORIGIN_DOMAIN, expectedCommitment);
});
it('reverts if the resulting validator set size will be less than the quorum threshold', async () => {
await multisigIsm.setThreshold(ORIGIN_DOMAIN, 1);
await expect(
multisigIsm.unenrollValidator(ORIGIN_DOMAIN, validators[0].address),
).to.be.revertedWith('violates quorum threshold');
});
it('reverts if the validator is not already enrolled', async () => {
await expect(
multisigIsm.unenrollValidator(ORIGIN_DOMAIN, validators[1].address),
).to.be.revertedWith('!enrolled');
});
it('reverts when called by a non-owner', async () => {
await expect(
multisigIsm
.connect(nonOwner)
.unenrollValidator(ORIGIN_DOMAIN, validators[0].address),
).to.be.revertedWith('Ownable: caller is not the owner');
});
});
describe('#setThresholds', () => {
let validatorAddresses: string[];
const domains = [ORIGIN_DOMAIN, DESTINATION_DOMAIN];
const thresholds = [2, 4];
before(async () => {
validatorAddresses = validators.map((v) => v.address);
});
beforeEach(async () => {
await multisigIsm.enrollValidators(
domains,
domains.map(() => validatorAddresses),
);
});
it('sets the quorum thresholds', async () => {
await multisigIsm.setThresholds(domains, thresholds);
await Promise.all(
domains.map(async (domain, i) => {
expect(await multisigIsm.threshold(domain)).to.equal(thresholds[i]);
}),
);
});
it('emits the SetThreshold event', async () => {
expect(await multisigIsm.setThresholds(domains, thresholds))
.to.emit(multisigIsm, 'ThresholdSet')
.withArgs(ORIGIN_DOMAIN, 2);
});
it('emits the CommitmentUpdated event', async () => {
const expectedCommitment = getCommitment(2, validatorAddresses);
expect(await multisigIsm.setThresholds(domains, thresholds))
.to.emit(multisigIsm, 'CommitmentUpdated')
.withArgs(ORIGIN_DOMAIN, expectedCommitment);
});
it('reverts when called by a non-owner', async () => {
await expect(
multisigIsm.connect(nonOwner).setThresholds(domains, thresholds),
).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 multisigIsm.enrollValidator(ORIGIN_DOMAIN, validators[0].address);
await multisigIsm.enrollValidator(ORIGIN_DOMAIN, validators[1].address);
});
it('sets the quorum threshold', async () => {
await multisigIsm.setThreshold(ORIGIN_DOMAIN, 2);
expect(await multisigIsm.threshold(ORIGIN_DOMAIN)).to.equal(2);
});
it('emits the SetThreshold event', async () => {
expect(await multisigIsm.setThreshold(ORIGIN_DOMAIN, 2))
.to.emit(multisigIsm, 'ThresholdSet')
.withArgs(ORIGIN_DOMAIN, 2);
});
it('emits the CommitmentUpdated event', async () => {
const expectedCommitment = getCommitment(2, [
validators[0].address,
validators[1].address,
]);
expect(await multisigIsm.setThreshold(ORIGIN_DOMAIN, 2))
.to.emit(multisigIsm, 'CommitmentUpdated')
.withArgs(ORIGIN_DOMAIN, expectedCommitment);
});
it('reverts if the new quorum threshold is zero', async () => {
await expect(
multisigIsm.setThreshold(ORIGIN_DOMAIN, 0),
).to.be.revertedWith('!range');
});
it('reverts if the new quorum threshold is greater than the validator set size', async () => {
await expect(
multisigIsm.setThreshold(ORIGIN_DOMAIN, 3),
).to.be.revertedWith('!range');
});
it('reverts when called by a non-owner', async () => {
await expect(
multisigIsm.connect(nonOwner).setThreshold(ORIGIN_DOMAIN, 2),
).to.be.revertedWith('Ownable: caller is not the owner');
});
});
describe('#validators', () => {
beforeEach(async () => {
await multisigIsm.enrollValidators(
[ORIGIN_DOMAIN],
[validators.map((v) => v.address)],
);
});
it('returns the validators', async () => {
expect(await multisigIsm.validators(ORIGIN_DOMAIN)).to.deep.equal(
validators.map((v) => v.address),
);
});
});
describe('#validatorsAndThreshold', () => {
const threshold = 7;
let message: string;
beforeEach(async () => {
await multisigIsm.enrollValidators(
[ORIGIN_DOMAIN],
[validators.map((v) => v.address)],
);
await multisigIsm.setThreshold(ORIGIN_DOMAIN, threshold);
const dispatch = await dispatchMessage(
mailbox,
DESTINATION_DOMAIN,
addressToBytes32(multisigIsm.address),
'hello',
);
message = dispatch.message;
});
it('returns the validators and threshold', async () => {
expect(await multisigIsm.validatorsAndThreshold(message)).to.deep.equal([
validators.map((v) => v.address),
threshold,
]);
});
});
describe('#validatorCount', () => {
beforeEach(async () => {
// Must be done sequentially so gas estimation is correct.
for (const v of validators) {
await multisigIsm.enrollValidator(ORIGIN_DOMAIN, v.address);
}
});
it('returns the number of validators enrolled in the validator set', async () => {
expect(await multisigIsm.validatorCount(ORIGIN_DOMAIN)).to.equal(
validators.length,
);
});
});
describe('#verify', () => {
let metadata: string, message: string, recipient: string;
before(async () => {
const recipientF = new TestRecipient__factory(signer);
recipient = (await recipientF.deploy()).address;
});
beforeEach(async () => {
// Must be done sequentially so gas estimation is correct
// and so that signatures are produced in the same order.
for (const v of validators) {
await multisigIsm.enrollValidator(ORIGIN_DOMAIN, v.address);
}
await multisigIsm.setThreshold(ORIGIN_DOMAIN, validators.length - 1);
({ message, metadata } = await dispatchMessageAndReturnMetadata(
mailbox,
defaultHook,
multisigIsm,
DESTINATION_DOMAIN,
recipient,
'hello world',
validators.slice(1),
));
});
it('returns true when valid metadata is provided', async () => {
expect(await multisigIsm.verify(metadata, message)).to.be.true;
});
it('allows for message processing when valid metadata is provided', async () => {
const mailboxFactory = new TestMailbox__factory(signer);
const destinationMailbox = await mailboxFactory.deploy(
DESTINATION_DOMAIN,
);
await destinationMailbox.initialize(
signer.address,
multisigIsm.address,
defaultHook.address,
requiredHook.address,
);
await destinationMailbox.process(metadata, message);
});
it('reverts when non-validator signatures are provided', async () => {
const nonValidator = await Validator.fromSigner(
signer,
ORIGIN_DOMAIN,
mailbox.address,
);
const parsedMetadata = parseLegacyMultisigIsmMetadata(metadata);
const nonValidatorSignature = (
await signCheckpoint(
parsedMetadata.checkpointRoot,
parsedMetadata.checkpointIndex,
mailbox.address,
[nonValidator],
)
)[0];
parsedMetadata.signatures.push(nonValidatorSignature);
const modifiedMetadata = formatLegacyMultisigIsmMetadata({
...parsedMetadata,
signatures: parsedMetadata.signatures.slice(1),
});
await expect(
multisigIsm.verify(modifiedMetadata, message),
).to.be.revertedWith('!threshold');
});
it('reverts when the provided validator set does not match the stored commitment', async () => {
const parsedMetadata = parseLegacyMultisigIsmMetadata(metadata);
const modifiedMetadata = formatLegacyMultisigIsmMetadata({
...parsedMetadata,
validators: parsedMetadata.validators.slice(1),
});
await expect(
multisigIsm.verify(modifiedMetadata, message),
).to.be.revertedWith('!commitment');
});
it('reverts when an invalid merkle proof is provided', async () => {
const parsedMetadata = parseLegacyMultisigIsmMetadata(metadata);
const modifiedMetadata = formatLegacyMultisigIsmMetadata({
...parsedMetadata,
proof: parsedMetadata.proof.reverse(),
});
await expect(
multisigIsm.verify(modifiedMetadata, message),
).to.be.revertedWith('!merkle');
});
});
describe('#isEnrolled', () => {
beforeEach(async () => {
await multisigIsm.enrollValidator(ORIGIN_DOMAIN, validators[0].address);
});
it('returns true if an address is enrolled in the validator set', async () => {
expect(await multisigIsm.isEnrolled(ORIGIN_DOMAIN, validators[0].address))
.to.be.true;
});
it('returns false if an address is not enrolled in the validator set', async () => {
expect(await multisigIsm.isEnrolled(ORIGIN_DOMAIN, validators[1].address))
.to.be.false;
});
});
describe('#_getDomainHash', () => {
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 (const testCase of domainHashTestCases) {
const { expectedDomainHash } = testCase;
// This public function on TestLegacyMultisigIsm exposes
// the internal _domainHash on MultisigIsm.
const domainHash = await multisigIsm.getDomainHash(
testCase.domain,
testCase.mailbox,
);
expect(domainHash).to.equal(expectedDomainHash);
}
});
});
// Manually unskip to run gas instrumentation.
// The JSON that's logged can then be copied to `typescript/sdk/src/consts/multisigIsmVerifyCosts.json`,
// which is ultimately used for configuring the default ISM overhead IGP.
describe.skip('#verify gas instrumentation for the OverheadISM', () => {
const MAX_VALIDATOR_COUNT = 18;
let metadata: string, message: string, recipient: string;
const gasOverhead: Record<number, Record<number, number>> = {};
before(async () => {
const recipientF = new LightTestRecipient__factory(signer);
recipient = (await recipientF.deploy()).address;
});
after(() => {
// eslint-disable-next-line no-console
console.log('Instrumented gas overheads:');
// eslint-disable-next-line no-console
console.log(JSON.stringify(gasOverhead));
});
for (
let numValidators = 1;
numValidators <= MAX_VALIDATOR_COUNT;
numValidators++
) {
for (let threshold = 1; threshold <= numValidators; threshold++) {
it(`instrument mailbox.process gas costs with ${threshold} of ${numValidators} multisig`, async () => {
const adjustedValidators = validators.slice(0, numValidators);
// Must be done sequentially so gas estimation is correct
// and so that signatures are produced in the same order.
for (const v of adjustedValidators) {
await multisigIsm.enrollValidator(ORIGIN_DOMAIN, v.address);
}
await multisigIsm.setThreshold(ORIGIN_DOMAIN, threshold);
const maxBodySize = 2 ** 16 - 1;
// The max body is used to estimate an upper bound on gas usage.
const maxBody = '0x' + 'AA'.repeat(maxBodySize);
({ message, metadata } = await dispatchMessageAndReturnMetadata(
mailbox,
defaultHook,
multisigIsm,
DESTINATION_DOMAIN,
recipient,
maxBody,
adjustedValidators,
threshold,
false,
));
const mailboxFactory = new TestMailbox__factory(signer);
const destinationMailbox = await mailboxFactory.deploy(
DESTINATION_DOMAIN,
);
await destinationMailbox.initialize(
signer.address,
multisigIsm.address,
defaultHook.address,
requiredHook.address,
);
const gas = await destinationMailbox.estimateGas.process(
metadata,
message,
);
if (gasOverhead[numValidators] === undefined) {
gasOverhead[numValidators] = {};
}
gasOverhead[numValidators][threshold] = gas.toNumber();
});
}
}
});
});
Loading…
Cancel
Save