diff --git a/solidity/optics-core/contracts/Common.sol b/solidity/optics-core/contracts/Common.sol index 9e37d24f7..a09daaa8f 100644 --- a/solidity/optics-core/contracts/Common.sol +++ b/solidity/optics-core/contracts/Common.sol @@ -4,12 +4,28 @@ pragma solidity >=0.6.11; import "@openzeppelin/contracts/cryptography/ECDSA.sol"; import "@summa-tx/memview-sol/contracts/TypedMemView.sol"; +/** + * @title Message Library + * @author Celo Labs Inc. + * @notice Library for formatted messages used by Home and Replica. + **/ library Message { using TypedMemView for bytes; using TypedMemView for bytes29; + // Number of bytes in formatted message before `body` field uint256 internal constant PREFIX_LENGTH = 76; + /** + * @notice Returns formatted (packed) message with provided fields + * @param _origin Domain of home chain + * @param _sender Address of sender as bytes32 + * @param _sequence Destination-specific sequence number + * @param _destination Domain of destination chain + * @param _recipient Address of recipient on destination chain as bytes32 + * @param _body Raw bytes of message body + * @return Formatted message + **/ function formatMessage( uint32 _origin, bytes32 _sender, @@ -29,6 +45,16 @@ library Message { ); } + /** + * @notice Returns leaf of formatted message with provided fields. + * @param _origin Domain of home chain + * @param _sender Address of sender as bytes32 + * @param _sequence Destination-specific sequence number + * @param _destination Domain of destination chain + * @param _recipient Address of recipient on destination chain as bytes32 + * @param _body Raw bytes of message body + * @return Leaf (hash) of formatted message + **/ function messageHash( uint32 _origin, bytes32 _sender, @@ -50,26 +76,32 @@ library Message { ); } + /// @notice Returns message's origin field function origin(bytes29 _message) internal pure returns (uint32) { return uint32(_message.indexUint(0, 4)); } + /// @notice Returns message's sender field function sender(bytes29 _message) internal pure returns (bytes32) { return _message.index(4, 32); } + /// @notice Returns message's sequence field function sequence(bytes29 _message) internal pure returns (uint32) { return uint32(_message.indexUint(36, 4)); } + /// @notice Returns message's destination field function destination(bytes29 _message) internal pure returns (uint32) { return uint32(_message.indexUint(40, 4)); } + /// @notice Returns message's recipient field as bytes32 function recipient(bytes29 _message) internal pure returns (bytes32) { return _message.index(44, 32); } + /// @notice Returns message's recipient field as an address function recipientAddress(bytes29 _message) internal pure @@ -78,27 +110,55 @@ library Message { return address(uint160(uint256(recipient(_message)))); } + /// @notice Returns message's body field as bytes29 (refer to TypedMemView library for details on bytes29 type) function body(bytes29 _message) internal pure returns (bytes29) { return _message.slice(PREFIX_LENGTH, _message.len() - PREFIX_LENGTH, 0); } } +/** + * @title Common + * @author Celo Labs Inc. + * @notice Shared utilities between Home and Replica. + **/ abstract contract Common { enum States {ACTIVE, FAILED} + /// @notice Domain of owning contract uint32 public immutable originDomain; + /// @notice Hash of `originDomain` concatenated with "OPTICS" bytes32 public immutable domainHash; + /// @notice Address of bonded updater address public updater; + /// @notice Current state of contract States public state; + /// @notice Current root bytes32 public current; + /** + * @notice Event emitted when update is made on Home or unconfirmed update + * root is enqueued on Replica + * @param _originDomain Domain of contract's chain + * @param _oldRoot Old merkle root + * @param _newRoot New merkle root + * @param signature Updater's signature on `_oldRoot` and `_newRoot` + **/ event Update( uint32 indexed _originDomain, bytes32 indexed _oldRoot, bytes32 indexed _newRoot, bytes signature ); + + /** + * @notice Event emitted when valid double update proof is provided to + * contract + * @param _oldRoot Old root shared between two conflicting updates + * @param _newRoot Array containing two conflicting new roots + * @param _signature Signature on `_oldRoot` and `_newRoot`[0] + * @param _signature2 Signature on `_oldRoot` and `_newRoot`[1] + **/ event DoubleUpdate( bytes32 _oldRoot, bytes32[2] _newRoot, @@ -118,17 +178,28 @@ abstract contract Common { state = States.ACTIVE; } + /// @notice Called when a double update or fraudulent update is detected function fail() internal virtual; + /// @notice Sets contract state to FAILED function _setFailed() internal { state = States.FAILED; } + /// @notice Ensures that contract state != FAILED modifier notFailed() { require(state != States.FAILED, "failed state"); _; } + /** + * @notice Called internally. Checks that signature is valid (belongs to + * updater). + * @param _oldRoot Old merkle root + * @param _newRoot New merkle root + * @param _signature Signature on `_oldRoot` and `_newRoot` + * @return Returns true if signature is valid and false if otherwise + **/ function checkSig( bytes32 _oldRoot, bytes32 _newRoot, @@ -140,8 +211,17 @@ abstract contract Common { return ECDSA.recover(_digest, _signature) == updater; } - // Checks that updater signed both updates and that - // the two updates are not equal (i.e. conflicting) + /** + * @notice Called by external agent. Checks that signatures on two sets of + * roots are valid and that the new roots conflict with each other. If both + * cases hold true, the contract is failed and a `DoubleUpdate` event is + * emitted. + * @dev When `fail()` is called on Home, updater is slashed. + * @param _oldRoot Old root shared between two conflicting updates + * @param _newRoot Array containing two conflicting new roots + * @param _signature Signature on `_oldRoot` and `_newRoot`[0] + * @param _signature2 Signature on `_oldRoot` and `_newRoot`[1] + **/ function doubleUpdate( bytes32 _oldRoot, bytes32[2] calldata _newRoot, diff --git a/solidity/optics-core/contracts/Home.sol b/solidity/optics-core/contracts/Home.sol index e78d1153c..fe75a9687 100644 --- a/solidity/optics-core/contracts/Home.sol +++ b/solidity/optics-core/contracts/Home.sol @@ -6,22 +6,41 @@ import "./Merkle.sol"; import "./Queue.sol"; import "./Sortition.sol"; +/** + * @title Home + * @author Celo Labs Inc. + * @notice Contract responsible for managing production of the message tree and + * holding custody of the updater bond. + **/ contract Home is MerkleTreeManager, QueueManager, Common { using QueueLib for QueueLib.Queue; using MerkleLib for MerkleLib.Tree; + /// @notice Mapping of sequence numbers for each destination mapping(uint32 => uint32) public sequences; + // TODO: removing sortition? ISortition internal sortition; + /** + * @notice Event emitted when new message is enqueued + * @param leafIndex Index of message's leaf in merkle tree + * @param destinationAndSequence Destination and destination-specific + * sequence combined in single field ((destination << 32) & sequence) + * @param leaf Hash of formatted message + * @param message Raw bytes of enqueued message + **/ event Dispatch( uint256 indexed leafIndex, uint64 indexed destinationAndSequence, bytes32 indexed leaf, bytes message ); + + /// @notice Event emitted when improper update detected event ImproperUpdate(); + // TODO: removing sortition? constructor(uint32 _originDomain, address _sortition) payable MerkleTreeManager() @@ -32,11 +51,20 @@ contract Home is MerkleTreeManager, QueueManager, Common { updater = ISortition(_sortition).current(); } + /// @notice Sets contract state to FAILED and slashes updater function fail() internal override { _setFailed(); sortition.slash(msg.sender); } + /** + * @notice Internal utility function that combines provided `_destination` + * and `_sequence`. + * @dev Both destination and sequence should be < 2^32 - 1 + * @param _destination Domain of destination chain + * @param _sequence Current sequence for given destination chain + * @return Returns (`_destination` << 32) & `_sequence` + **/ function destinationAndSequence(uint32 _destination, uint32 _sequence) internal pure @@ -45,6 +73,13 @@ contract Home is MerkleTreeManager, QueueManager, Common { return (uint64(_destination) << 32) | _sequence; } + /** + * @notice Formats message, adds its leaf into merkle tree, enqueues new + * merkle root, and emits `Dispatch` event with data regarding message. + * @param destination Domain of destination chain + * @param recipient Address or recipient on destination chain + * @param body Raw bytes of message + **/ function enqueue( uint32 destination, bytes32 recipient, @@ -76,6 +111,15 @@ contract Home is MerkleTreeManager, QueueManager, Common { ); } + /** + * @notice Called by updater. Updates home's `current` root from `_oldRoot` + * to `_newRoot` and emits `Update` event. If fraudulent update + * detected in `improperUpdate`, updater is slashed and home is + * failed. + * @param _oldRoot Old merkle root (should equal home's current root) + * @param _newRoot New merkle root + * @param _signature Updater's signature on `_oldRoot` and `_newRoot` + **/ function update( bytes32 _oldRoot, bytes32 _newRoot, @@ -91,6 +135,17 @@ contract Home is MerkleTreeManager, QueueManager, Common { emit Update(originDomain, _oldRoot, _newRoot, _signature); } + /** + * @notice Checks that `_newRoot` in update currently exists in queue. If + * `_newRoot` doesn't exist in queue, update is fraudulent, causing + * updater to be slashed and home to be failed. + * @dev Reverts (and doesn't slash updater) if signature is invalid or + * update not current + * @param _oldRoot Old merkle tree root (should equal home's current root) + * @param _newRoot New merkle tree root + * @param _signature Updater's signature on `_oldRoot` and `_newRoot` + * @return Returns true if update was fraudulent + **/ function improperUpdate( bytes32 _oldRoot, bytes32 _newRoot, @@ -106,6 +161,13 @@ contract Home is MerkleTreeManager, QueueManager, Common { return false; } + /** + * @notice Suggests an update to caller. If queue is non-empty, returns the + * home's current root as `_current` and the queue's latest root as + * `_new`. Null bytes returned if queue is empty. + * @return _current Current root + * @return _new New root + **/ function suggestUpdate() external view diff --git a/solidity/optics-core/contracts/Merkle.sol b/solidity/optics-core/contracts/Merkle.sol index 0f0db5de1..5307d0b9d 100644 --- a/solidity/optics-core/contracts/Merkle.sol +++ b/solidity/optics-core/contracts/Merkle.sol @@ -6,17 +6,26 @@ pragma solidity >=0.6.11; import "hardhat/console.sol"; -// There is a bit of cruft in this design. The library needs the 0-hashes, -// but can't produce them on construction. Consider: hardcode constants? +/** + * @title MerkleLib + * @author Celo Labs Inc. + * @notice An incremental merkle tree modeled on the eth2 deposit contract. + **/ library MerkleLib { uint256 internal constant TREE_DEPTH = 32; uint256 internal constant MAX_LEAVES = 2**TREE_DEPTH - 1; + /** + * @notice Struct representing incremental merkle tree. Contains current + * branch and the number of inserted leaves in the tree. + **/ struct Tree { bytes32[TREE_DEPTH] branch; uint256 count; } + /// @notice Returns array of TREE_DEPTH zero hashes + /// @return _zeroes Array of TREE_DEPTH zero hashes function zeroHashes() internal pure @@ -56,6 +65,14 @@ library MerkleLib { _zeroes[31] = Z_31; } + /** + * @notice Calculates and returns the merkle root for the given leaf + * `_item`, a merkle branch, and the index of `_item` in the tree. + * @param _item Merkle leaf + * @param _branch Merkle proof + * @param _index Index of `_item` in tree + * @return _current Calculated merkle root + **/ function branchRoot( bytes32 _item, bytes32[TREE_DEPTH] memory _branch, @@ -74,6 +91,12 @@ library MerkleLib { } } + /** + * @notice Calculates and returns`_tree`'s current root given array of zero + * hashes + * @param _zeroes Array of zero hashes + * @return _current Calculated root of `_tree` + **/ function rootWithCtx(Tree storage _tree, bytes32[TREE_DEPTH] memory _zeroes) internal view @@ -92,10 +115,16 @@ library MerkleLib { } } + /// @notice Calculates and returns`_tree`'s current root function root(Tree storage _tree) internal view returns (bytes32) { return rootWithCtx(_tree, zeroHashes()); } + /** + * @notice Inserts `_node` into merkle tree + * @dev Reverts if tree is full + * @param _node Element to insert into tree + **/ function insert(Tree storage _tree, bytes32 _node) internal { require(_tree.count < MAX_LEAVES, "merkle tree full"); @@ -181,6 +210,12 @@ library MerkleLib { hex"8448818bb4ae4562849e949e17ac16e0be16688e156b5cf15e098c627c0056a9"; } +/** + * @title MerkleTreeManager + * @author Celo Labs Inc. + * @notice Contract containing a merkle tree instance and view operations on + * the tree. + **/ contract MerkleTreeManager { using MerkleLib for MerkleLib.Tree; @@ -189,10 +224,12 @@ contract MerkleTreeManager { // solhint-disable-next-line no-empty-blocks constructor() {} + /// @notice Calculates and returns`tree`'s current root function root() public view returns (bytes32) { return tree.root(); } + /// @notice Returns the number of inserted leaves in the tree (current index) function count() public view returns (uint256) { return tree.count; } diff --git a/solidity/optics-core/contracts/Queue.sol b/solidity/optics-core/contracts/Queue.sol index 0e75e638f..361a5bed3 100644 --- a/solidity/optics-core/contracts/Queue.sol +++ b/solidity/optics-core/contracts/Queue.sol @@ -1,19 +1,40 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.6.11; +/** + * @title QueueLib + * @author Celo Labs Inc. + * @notice Library containing queue struct and operations for queue used by + * Home and Replica. + **/ library QueueLib { + /** + * @notice Queue struct + * @dev Internally keeps track of the `first` and `last` elements through + * indices and a mapping of indices to enqueued elements. + **/ struct Queue { uint128 first; uint128 last; mapping(uint256 => bytes32) queue; } + /** + * @notice Initializes the queue + * @dev Empty state denoted by _q.first > q._last. Queue initialized + * with _q.first = 1 and _q.last = 0. + **/ function init(Queue storage _q) internal { if (_q.first == 0) { _q.first = 1; } } + /** + * @notice Enqueues a single new element + * @param _item New element to be enqueued + * @return _last Index of newly enqueued element + **/ function enqueue(Queue storage _q, bytes32 _item) internal returns (uint128 _last) @@ -26,6 +47,11 @@ library QueueLib { } } + /** + * @notice Dequeues element at front of queue + * @dev Removes dequeued element from storage + * @return _item Dequeued element + **/ function dequeue(Queue storage _q) internal returns (bytes32 _item) { uint128 _last = _q.last; uint128 _first = _q.first; @@ -38,6 +64,11 @@ library QueueLib { _q.first = _first + 1; } + /** + * @notice Batch enqueues several elements + * @param _items Array of elements to be enqueued + * @return _last Index of last enqueued element + **/ function enqueue(Queue storage _q, bytes32[] memory _items) internal returns (uint128 _last) @@ -53,6 +84,12 @@ library QueueLib { _q.last = _last; } + /** + * @notice Batch dequeues `_number` elements + * @dev Reverts if `_number` > queue length + * @param _number Number of elements to dequeue + * @return Array of dequeued elements + **/ function dequeue(Queue storage _q, uint256 _number) internal returns (bytes32[] memory) @@ -73,7 +110,12 @@ library QueueLib { return _items; } - // NB: this is unfortunately expensive + /** + * @notice Returns true if `_item` is in the queue and false if otherwise + * @dev Linearly scans from _q.first to _q.last looking for `_item` + * @param _item Item being searched for in queue + * @return True if `_item` currently exists in queue, false if otherwise + **/ function contains(Queue storage _q, bytes32 _item) internal view @@ -87,19 +129,25 @@ library QueueLib { return false; } + /// @notice Returns last item in queue + /// @dev Returns bytes32(0) if queue empty function lastItem(Queue storage _q) internal view returns (bytes32) { return _q.queue[_q.last]; } + /// @notice Returns element at front of queue without removing element + /// @dev Reverts if queue is empty function peek(Queue storage _q) internal view returns (bytes32 _item) { require(!isEmpty(_q), "Empty"); _item = _q.queue[_q.first]; } + /// @notice Returns true if queue is empty and false if otherwise function isEmpty(Queue storage _q) internal view returns (bool) { return _q.last < _q.first; } + /// @notice Returns number of elements between `_last` and `_first` (used internally) function _length(uint128 _last, uint128 _first) internal pure @@ -108,6 +156,7 @@ library QueueLib { return uint256(_last + 1 - _first); } + /// @notice Returns number of elements in queue function length(Queue storage _q) internal view returns (uint256) { uint128 _last = _q.last; uint128 _first = _q.first; @@ -116,6 +165,12 @@ library QueueLib { } } +/** + * @title QueueManager + * @author Celo Labs Inc. + * @notice Contract containing a queue instance and view operations on the + * queue. + **/ contract QueueManager { using QueueLib for QueueLib.Queue; QueueLib.Queue internal queue; @@ -124,14 +179,17 @@ contract QueueManager { queue.init(); } + /// @notice Returns number of elements in queue function queueLength() external view returns (uint256) { return queue.length(); } + /// @notice Returns true if `_item` is in the queue and false if otherwise function queueContains(bytes32 _item) external view returns (bool) { return queue.contains(_item); } + /// @notice Returns last item in queue function queueEnd() external view returns (bytes32) { return queue.lastItem(); } diff --git a/solidity/optics-core/contracts/Replica.sol b/solidity/optics-core/contracts/Replica.sol index 379503491..215e35e62 100644 --- a/solidity/optics-core/contracts/Replica.sol +++ b/solidity/optics-core/contracts/Replica.sol @@ -7,12 +7,21 @@ import "./Merkle.sol"; import "./Queue.sol"; import {OpticsHandlerI} from "./UsingOptics.sol"; +/** + * @title Replica + * @author Celo Labs Inc. + * @notice Contract responsible tracking root updates on home. + **/ abstract contract Replica is Common, QueueManager { using QueueLib for QueueLib.Queue; + /// @notice Domain of replica's native chain uint32 public immutable ownDomain; + + /// @notice Number of seconds to wait before enqueued root becomes confirmable uint256 public optimisticSeconds; + /// @notice Mapping of enqueued roots to allowable confirmation times mapping(bytes32 => uint256) public confirmAt; constructor( @@ -27,16 +36,24 @@ abstract contract Replica is Common, QueueManager { current = _current; } + /// @notice Sets contract state to FAILED function fail() internal override { _setFailed(); } - /// Hook for tasks + /// @notice Hook called before confirming root function _beforeConfirm() internal virtual; - /// Hook for tasks + /// @notice Hook called before enqueuing update's root function _beforeUpdate() internal virtual; + /** + * @notice Called by external agent. Returns next pending root to be + * confirmed and its confirmation time. If queue is empty, returns null + * values. + * @return _pending Pending (unconfirmed) root + * @return _confirmAt Pending root's confirmation time + **/ function nextPending() external view @@ -48,6 +65,15 @@ abstract contract Replica is Common, QueueManager { } } + /** + * @notice Called by external agent. Enqueues signed update's new root, + * marks root's allowable confirmation time, and emits an `Update` event. + * @dev Reverts if update doesn't build off queue's last root or replica's + * current root if queue is empty. Also reverts if signature is invalid. + * @param _oldRoot Old merkle root + * @param _newRoot New merkle root + * @param _signature Updater's signature on `_oldRoot` and `_newRoot` + **/ function update( bytes32 _oldRoot, bytes32 _newRoot, @@ -68,11 +94,21 @@ abstract contract Replica is Common, QueueManager { emit Update(originDomain, _oldRoot, _newRoot, _signature); } + /** + * @notice Called by external agent. Returns true if there is a confirmable + * root in the queue and false if otherwise. + **/ function canConfirm() external view returns (bool) { return queue.length() != 0 && block.timestamp >= confirmAt[queue.peek()]; } + /** + * @notice Called by external agent. Confirms as many confirmable roots in + * queue as possible, updating replica's current root to be the last + * confirmed root. + * @dev Reverts if queue started as empty (i.e. no roots to confirm) + **/ function confirm() external notFailed { require(queue.length() != 0, "no pending"); @@ -96,21 +132,32 @@ abstract contract Replica is Common, QueueManager { } } +/** + * @title ProcessingReplica + * @author Celo Labs Inc. + * @notice Contract responsible for dispatching messages on home to end + * recipients. Inherits home root tracking capabilities from `Replica`. + **/ contract ProcessingReplica is Replica { using MerkleLib for MerkleLib.Tree; using TypedMemView for bytes; using TypedMemView for bytes29; using Message for bytes29; - // minimum gas for message processing + /// @notice Minimum gas for message processing uint256 public constant PROCESS_GAS = 500000; - // reserved gas (to ensure tx completes in case message processing runs out) + /// @notice Reserved gas (to ensure tx completes in case message processing runs out) uint256 public constant RESERVE_GAS = 10000; bytes32 public previous; // to smooth over witness invalidation + + /// @notice Index of last processed message's leaf in home's merkle tree uint256 public lastProcessed; - mapping(bytes32 => MessageStatus) public messages; + + /// @notice Status of message enum MessageStatus {None, Pending, Processed} + /// @notice Mapping of message leaves to MessageStatus + mapping(bytes32 => MessageStatus) public messages; constructor( uint32 _originDomain, @@ -123,6 +170,7 @@ contract ProcessingReplica is Replica { lastProcessed = _lastProcessed; } + /// @notice Sets `previous` to `current` root before updating `current` function _beforeConfirm() internal override { previous = current; } @@ -130,6 +178,21 @@ contract ProcessingReplica is Replica { // solhint-disable-next-line no-empty-blocks function _beforeUpdate() internal override {} + /** + * @notice Given formatted message, attempts to dispatch message payload to + * end recipient. + * @dev Requires recipient to have implemented `handle` method (refer to + * UsingOptics.sol). Reverts if formatted message's destination domain + * doesn't match replica's own domain, if message is out of order (skips + * one or more sequence numbers), if message has not been proven (doesn't + * have MessageStatus.Pending), or if not enough gas is provided for + * dispatch transaction. + * @param _message Formatted message (refer to Common.sol Message library) + * @return _success True if dispatch transaction succeeded (false if + * otherwise) + * @return _result Response returned by recipient's `handle` method on + * success. Error if dispatch transaction failed. + **/ function process(bytes memory _message) public returns (bool _success, bytes memory _result) @@ -180,6 +243,16 @@ contract ProcessingReplica is Replica { lastProcessed = _sequence; } + /** + * @notice Attempts to prove the validity of message given its leaf, the + * merkle proof of inclusion for the leaf, and the index of the leaf. + * @dev Reverts if message's MessageStatus != None (i.e. if message was + * already proven or processed) + * @param leaf Leaf of message to prove + * @param proof Merkle proof of inclusion for leaf + * @param index Index of leaf in home's merkle tree + * @return Returns true if proof was valid and `prove` call succeeded + **/ function prove( bytes32 leaf, bytes32[32] calldata proof, @@ -198,6 +271,15 @@ contract ProcessingReplica is Replica { return false; } + /** + * @notice First attempts to prove the validity of provided formatted + * `message`. If the message is successfully proven, then tries to process + * message. + * @dev Reverts if `prove` call returns false + * @param message Formatted message (refer to Common.sol Message library) + * @param proof Merkle proof of inclusion for message's leaf + * @param index Index of leaf in home's merkle tree + **/ function proveAndProcess( bytes memory message, bytes32[32] calldata proof,