Skip to content

Commit

Permalink
[STABLE-929] Implement multisig message signing (circlefin#19)
Browse files Browse the repository at this point in the history
* implement multisig message signing

* comments

* cr
  • Loading branch information
walkerq authored Sep 12, 2022
1 parent 3b38b37 commit 216f79d
Show file tree
Hide file tree
Showing 4 changed files with 850 additions and 41 deletions.
236 changes: 215 additions & 21 deletions src/MessageTransmitter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,24 @@ pragma solidity ^0.7.6;

import "@memview-sol/contracts/TypedMemView.sol";
import "@openzeppelin/contracts/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/EnumerableSet.sol";
import "./interfaces/IMessageTransmitter.sol";
import "./interfaces/IMessageDestinationHandler.sol";
import "./messages/Message.sol";
import "./roles/Pausable.sol";
import "./roles/Rescuable.sol";
import "./roles/Attestable.sol";

/**
* @title MessageTransmitter
* @notice Contract responsible for sending and receiving messages across chains.
*/
contract MessageTransmitter is IMessageTransmitter, Pausable, Rescuable {
contract MessageTransmitter is
IMessageTransmitter,
Pausable,
Rescuable,
Attestable
{
// ============ Events ============
/**
* @notice Emitted when a new message is dispatched
Expand Down Expand Up @@ -54,16 +61,39 @@ contract MessageTransmitter is IMessageTransmitter, Pausable, Rescuable {
*/
event MaxMessageBodySizeUpdated(uint256 newMaxMessageBodySize);

/**
* @notice Emitted when an attester is enabled
* @param attester newly enabled attester
*/
event AttesterEnabled(address attester);

/**
* @notice Emitted when an attester is disabled
* @param attester newly disabled attester
*/
event AttesterDisabled(address attester);

/**
* @notice Emitted when threshold number of attestations (m in m/n multisig) is updated
* @param oldSignatureThreshold old signature threshold
* @param newSignatureThreshold new signature threshold
*/
event SignatureThresholdUpdated(
uint256 oldSignatureThreshold,
uint256 newSignatureThreshold
);

// ============ Libraries ============
using TypedMemView for bytes;
using TypedMemView for bytes29;
using Message for bytes29;
using EnumerableSet for EnumerableSet.AddressSet;

// ============ State Variables ============
// Maximum size of message body, in bytes.
// This value is set by owner.
uint256 public maxMessageBodySize;

// ============ Public Variables ============
// Domain of chain on which the contract is deployed
uint32 public immutable localDomain;

Expand All @@ -73,11 +103,20 @@ contract MessageTransmitter is IMessageTransmitter, Pausable, Rescuable {
// Maps a hash of (sourceDomain, nonce) -> boolean
// boolean value is true if nonce is used
mapping(bytes32 => bool) public usedNonces;
// message signer
address public attester;

// number of signatures from distinct attesters required for a message to be received (m in m/n multisig)
uint256 public signatureThreshold;

// message format version
uint32 public version;

// 65-byte ECDSA signature: v (32) + r (32) + s (1)
uint256 internal immutable signatureLength = 65;

// enabled attesters (message signers)
// (length of enabledAttesters is n in m/n multisig of message signers)
EnumerableSet.AddressSet private enabledAttesters;

// ============ Constructor ============
constructor(
uint32 _localDomain,
Expand All @@ -86,13 +125,14 @@ contract MessageTransmitter is IMessageTransmitter, Pausable, Rescuable {
uint32 _version
) {
localDomain = _localDomain;
// [BRAAV-11992] TODO refactor once role reassignment is supported
attester = _attester;
maxMessageBodySize = _maxMessageBodySize;
version = _version;
// Initially 1 signature is required. Threshold can be increased by attesterManager.
signatureThreshold = 1;
enableAttester(_attester);
}

// ============ Public Functions ============
// ============ External Functions ============
/**
* @notice Send the message to the destination domain and recipient
* @dev Increment nonce, format the message, and emit `MessageSent` event with message information.
Expand Down Expand Up @@ -139,7 +179,16 @@ contract MessageTransmitter is IMessageTransmitter, Pausable, Rescuable {
* can only be broadcast once for a (sourceDomain, destinationDomain)
* pair. The message body of a valid message is passed to the
* specified recipient for further processing.
* @dev Message format:
*
* @dev Attestation format:
* A valid attestation is the concatenated 65-byte signature(s) of exactly
* `thresholdSignature` signatures, in increasing order of attester address.
* ***If the attester addresses recovered from signatures are not in
* increasing order, signature verification will fail.***
* If incorrect number of signatures or duplicate signatures are supplied,
* signature verification will fail.
*
* Message format:
* Field Bytes Type Index
* version 4 uint32 0
* sourceDomain 4 uint32 4
Expand All @@ -149,15 +198,19 @@ contract MessageTransmitter is IMessageTransmitter, Pausable, Rescuable {
* recipient 32 bytes32 52
* messageBody dynamic bytes 84
* @param _message Message bytes
* @param _signature Signature of message
* @param _attestation Concatenated 65-byte signature(s) of `_message`, in increasing order
* of the attester address recovered from signatures.
* @return success bool, true if successful
*/
function receiveMessage(bytes memory _message, bytes memory _signature)
function receiveMessage(bytes memory _message, bytes calldata _attestation)
external
override
whenNotPaused
returns (bool success)
{
// Validate each signature in the attestation
_verifyAttestationSignatures(_message, _attestation);

bytes29 _m = _message.ref(0);

// Validate domain
Expand All @@ -166,12 +219,6 @@ contract MessageTransmitter is IMessageTransmitter, Pausable, Rescuable {
"Invalid destination domain"
);

// Validate attester signature
require(
_isAttesterSignature(_message, _signature),
"Invalid attester signature"
);

// Validate nonce is available
uint32 _sourceDomain = _m.sourceDomain();
uint64 _nonce = _m.nonce();
Expand Down Expand Up @@ -208,13 +255,114 @@ contract MessageTransmitter is IMessageTransmitter, Pausable, Rescuable {
emit MaxMessageBodySizeUpdated(maxMessageBodySize);
}

/**
* @notice Enables an attester
* @dev Only callable by attesterManager. New attester must be nonzero, and currently disabled.
* @param _newAttester attester to enable
*/
function enableAttester(address _newAttester) public onlyAttesterManager {
require(_newAttester != address(0), "New attester must be nonzero");
require(enabledAttesters.add(_newAttester), "Attester already enabled");
emit AttesterEnabled(_newAttester);
}

/**
* @notice Disables an attester
* @dev Only callable by attesterManager. Disabling the attester is not allowed if there is only one attester
* enabled, or if it would cause the number of enabled attesters to become less than signatureThreshold.
* (Attester must be currently enabled.)
* @param _attester attester to disable
*/
function disableAttester(address _attester) external onlyAttesterManager {
// Disallow disabling attester if there is only 1 active attester
require(
enabledAttesters.length() > 1,
"Unable to disable attester because 1 or less attesters are enabled"
);

// Disallow disabling an attester if it would cause the n in m/n multisig to fall below m (threshold # of signers).
require(
enabledAttesters.length() > signatureThreshold,
"Unable to disable attester because signature threshold is too low"
);

require(
enabledAttesters.remove(_attester),
"Attester already disabled"
);
emit AttesterDisabled(_attester);
}

/**
* @notice Sets the threshold of signatures required to attest to a message.
* (This is the m in m/n multisig.)
* @dev new signature threshold must be nonzero, and must not exceed number
* of enabled attesters.
* @param _newSignatureThreshold new signature threshold
*/
function setSignatureThreshold(uint256 _newSignatureThreshold)
external
onlyAttesterManager
{
require(
_newSignatureThreshold != 0,
"New signature threshold must be nonzero"
);

require(
_newSignatureThreshold <= enabledAttesters.length(),
"New signature threshold cannot exceed the number of enabled attesters"
);

require(
_newSignatureThreshold != signatureThreshold,
"New signature threshold must not equal current signature threshold"
);

uint256 _oldSignatureThreshold = signatureThreshold;
signatureThreshold = _newSignatureThreshold;
emit SignatureThresholdUpdated(
_oldSignatureThreshold,
signatureThreshold
);
}

/**
* @notice returns true if given `_attester` is enabled, else false
* @return true if given `_attester` is enabled, else false
*/
function isEnabledAttester(address _attester) external view returns (bool) {
return enabledAttesters.contains(_attester);
}

/**
* @notice returns the number of enabled attesters
* @return number of enabled attesters
*/
function getNumEnabledAttesters() external view returns (uint256) {
return enabledAttesters.length();
}

/**
* @notice gets enabled attester at given `_index`
* @param _index index of attester to check
* @return enabled attester at given `_index`
*/
function getEnabledAttester(uint256 _index)
external
view
returns (address)
{
return enabledAttesters.at(_index);
}

// ============ Internal Utils ============
/**
* @notice hashes `_source` and `_nonce`.
* @param _source Domain of chain where the transfer originated
* @param _nonce The unique identifier for the message from source to
destination
* @return Returns hash of source and nonce
* @return hash of source and nonce
*/
function _hashSourceAndNonce(uint32 _source, uint256 _nonce)
internal
Expand All @@ -224,17 +372,63 @@ contract MessageTransmitter is IMessageTransmitter, Pausable, Rescuable {
return keccak256(abi.encodePacked(_source, _nonce));
}

/**
* @notice reverts if the attestation, which is comprised of one or more concatenated 65-byte signatures, is invalid.
*
* @dev Rules for valid attestation:
* 1. length of `_attestation` == 65 (signature length) * signatureThreshold
* 2. addresses recovered from attestation must be in increasing order.
* For example, if signature A is signed by address 0x1..., and signature B
* is signed by address 0x2..., attestation must be passed as AB.
* 3. no duplicate signers
* 4. all signers must be enabled attesters
*
* Based on Christian Lundkvist's Simple Multisig
* (https://github.com/christianlundkvist/simple-multisig/tree/560c463c8651e0a4da331bd8f245ccd2a48ab63d)
*/
function _verifyAttestationSignatures(
bytes memory _message,
bytes calldata _attestation
) internal view {
require(
_attestation.length == signatureLength * signatureThreshold,
"Invalid attestation length"
);

// (Attesters cannot be address(0))
address latestAttesterAddress = address(0);
// Address recovered from signatures must be in increasing order, to prevent duplicates
for (uint256 i = 0; i < signatureThreshold; i++) {
bytes memory _signature = _attestation[i * signatureLength:i *
signatureLength +
signatureLength];
address recoveredAttester = _recoverAttesterSignature(
_message,
_signature
);
require(
recoveredAttester > latestAttesterAddress,
"Signature verification failed: signer is out of order or duplicate"
);
require(
enabledAttesters.contains(recoveredAttester),
"Signature verification failed: signer is not enabled attester"
);
latestAttesterAddress = recoveredAttester;
}
}

/**
* @notice Checks that signature was signed by attester
* @param _message unsigned message bytes
* @param _signature message signature
* @return true iff signature is signed by the attester
* @return address of recovered signer
**/
function _isAttesterSignature(
function _recoverAttesterSignature(
bytes memory _message,
bytes memory _signature
) internal view returns (bool) {
) internal view returns (address) {
bytes32 _digest = keccak256(_message);
return (ECDSA.recover(_digest, _signature) == attester);
return (ECDSA.recover(_digest, _signature));
}
}
Loading

0 comments on commit 216f79d

Please sign in to comment.