docs: adds natspec documentation to core contracts (#137)

* docs: adds documentation to Home contract

* docs: specifies what destinationAndSequence calculation gives

* docs: adds documentation for Replica contract

* docs: adds documentation to Common

* docs: adds documentation to Queue

* docs: adds documentation to Merkle
buddies-main-deployment
Luke Tchang 4 years ago committed by GitHub
parent 9cae2f469a
commit 398f928cbc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 84
      solidity/optics-core/contracts/Common.sol
  2. 62
      solidity/optics-core/contracts/Home.sol
  3. 41
      solidity/optics-core/contracts/Merkle.sol
  4. 60
      solidity/optics-core/contracts/Queue.sol
  5. 92
      solidity/optics-core/contracts/Replica.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,

@ -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

@ -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;
}

@ -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();
}

@ -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,

Loading…
Cancel
Save