The home for Hyperlane core contracts, sdk packages, and other infrastructure
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
hyperlane-monorepo/solidity/optics-core/contracts/Replica.sol

345 lines
12 KiB

// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity >=0.6.11;
// ============ Internal Imports ============
import {Version0} from "./Version0.sol";
import {Common} from "./Common.sol";
import {MerkleLib} from "../libs/Merkle.sol";
import {QueueLib} from "../libs/Queue.sol";
import {Message} from "../libs/Message.sol";
import {IMessageRecipient} from "../interfaces/IMessageRecipient.sol";
// ============ External Imports ============
import {TypedMemView} from "@summa-tx/memview-sol/contracts/TypedMemView.sol";
/**
* @title Replica
* @author Celo Labs Inc.
* @notice Track root updates on Home,
* prove and dispatch messages to end recipients.
*/
contract Replica is Version0, Common {
// ============ Libraries ============
using QueueLib for QueueLib.Queue;
using MerkleLib for MerkleLib.Tree;
using TypedMemView for bytes;
using TypedMemView for bytes29;
using Message for bytes29;
// ============ Enums ============
// Status of Message:
// 0 - None - message has not been proven or processed
// 1 - Proven - message inclusion proof has been validated
// 2 - Processed - message has been dispatched to recipient
enum MessageStatus {
None,
Proven,
Processed
}
// ============ Constants ============
// Minimum gas for message processing
uint256 public constant PROCESS_GAS = 850000;
// Reserved gas (to ensure tx completes in case message processing runs out)
uint256 public constant RESERVE_GAS = 15000;
// ============ Public Storage ============
// Domain of home chain
uint32 public remoteDomain;
// Number of seconds to wait before root becomes confirmable
uint256 public optimisticSeconds;
// re-entrancy guard
uint8 private entered;
// Mapping of roots to allowable confirmation times
mapping(bytes32 => uint256) public confirmAt;
// Mapping of message leaves to MessageStatus
mapping(bytes32 => MessageStatus) public messages;
// ============ Upgrade Gap ============
// gap for upgrade safety
uint256[44] private __GAP;
// ============ Events ============
/**
* @notice Emitted when message is processed without error
* @param messageHash Hash of message processed
*/
event ProcessSuccess(bytes32 indexed messageHash);
/**
* @notice Emitted when processing message reverts
* @param messageHash Hash of message that failed to process
* @param nonce nonce of the message, set for each domain by remote Home
* @param recipient the intended recipient of the failed message
* @param returnData the return data from the failed message
*/
event ProcessError(
bytes32 indexed messageHash,
uint32 indexed nonce,
address indexed recipient,
bytes returnData
);
// ============ Constructor ============
// solhint-disable-next-line no-empty-blocks
constructor(uint32 _localDomain) Common(_localDomain) {}
// ============ Initializer ============
function initialize(
uint32 _remoteDomain,
address _updater,
bytes32 _committedRoot,
uint256 _optimisticSeconds
) public initializer {
__Common_initialize(_updater);
entered = 1;
remoteDomain = _remoteDomain;
committedRoot = _committedRoot;
confirmAt[_committedRoot] = 1;
optimisticSeconds = _optimisticSeconds;
}
// ============ External Functions ============
/**
* @notice Called by external agent. Submits the 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
* committedRoot 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,
bytes memory _signature
) external notFailed {
// ensure that update is building off the last submitted root
if (queue.length() > 0) {
require(_oldRoot == queue.lastItem(), "not end of queue");
} else {
require(_oldRoot == committedRoot, "not current update");
}
// validate updater signature
require(
_isUpdaterSignature(_oldRoot, _newRoot, _signature),
"!updater sig"
);
// Hook for future use
_beforeUpdate();
// set the new root's confirmation timer
confirmAt[_newRoot] = block.timestamp + optimisticSeconds;
// add the new root to the queue of roots
queue.enqueue(_newRoot);
emit Update(remoteDomain, _oldRoot, _newRoot, _signature);
}
/**
* @notice Called by external agent. Confirms as many confirmable roots in
* queue as possible, updating replica's committedRoot 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, "!pending");
// Traverse the queue by peeking each iterm to see if it ought to be
// confirmed. If so, dequeue it
bytes32 _pending;
uint256 _remaining = queue.length();
while (_remaining > 0 && acceptableRoot(queue.peek())) {
_pending = queue.dequeue();
_remaining -= 1;
}
// This condition is hit if the while loop is never executed, because
// the first queue item has not hit its timer yet
require(_pending != bytes32(0), "!time");
// Hook for future use
_beforeConfirm();
// Update committedRoot to last confirmed root
committedRoot = _pending;
}
/**
* @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,
uint256 _index
) external {
require(prove(keccak256(_message), _proof, _index), "!prove");
process(_message);
}
/**
* @notice Called by external agent. Returns next pending root to be
* confirmed and its confirmation time.
* @return _pending Pending (unconfirmed) root
* @return _confirmAt Pending root's confirmation time
*/
function nextPending()
external
view
returns (bytes32 _pending, uint256 _confirmAt)
{
if (queue.length() != 0) {
_pending = queue.peek();
_confirmAt = confirmAt[_pending];
} else {
_pending = committedRoot;
_confirmAt = confirmAt[committedRoot];
}
}
/**
* @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 && acceptableRoot(queue.peek());
}
/**
* @notice Given formatted message, attempts to dispatch
* message payload to end recipient.
* @dev Recipient must implement a `handle` method (refer to IMessageRecipient.sol)
* Reverts if formatted message's destination domain is not the Replica's domain,
* if message has not been proven,
* or if not enough gas is provided for the dispatch transaction.
* @param _message Formatted message
* @return _success TRUE iff dispatch transaction succeeded
*/
function process(bytes memory _message) public returns (bool _success) {
bytes29 _m = _message.ref(0);
// ensure message was meant for this domain
require(_m.destination() == localDomain, "!destination");
// ensure message has been proven
bytes32 _messageHash = _m.keccak();
require(messages[_messageHash] == MessageStatus.Proven, "!proven");
// check re-entrancy guard
require(entered == 1, "!reentrant");
entered = 0;
// update message status as processed
messages[_messageHash] = MessageStatus.Processed;
// A call running out of gas TYPICALLY errors the whole tx. We want to
// a) ensure the call has a sufficient amount of gas to make a
// meaningful state change.
// b) ensure that if the subcall runs out of gas, that the tx as a whole
// does not revert (i.e. we still mark the message processed)
// To do this, we require that we have enough gas to process
// and still return. We then delegate only the minimum processing gas.
require(gasleft() >= PROCESS_GAS + RESERVE_GAS, "!gas");
// get the message recipient
address _recipient = _m.recipientAddress();
// dispatch message to recipient
// by low-level calling "handle" function
bytes memory _returnData;
(_success, _returnData) = _recipient.call{gas: PROCESS_GAS}(
abi.encodeWithSignature(
"handle(uint32,bytes32,bytes)",
_m.origin(),
_m.sender(),
_m.body().clone()
)
);
// emit process results
if (_success) {
emit ProcessSuccess(_messageHash);
} else {
emit ProcessError(
_messageHash,
_m.nonce(),
_recipient,
_returnData
);
}
// reset re-entrancy guard
entered = 1;
}
// ============ Public Functions ============
/**
* @notice Check that the root has been submitted
* and that the optimistic timeout period has expired,
* meaning the root can be processed
* @param _root the Merkle root, submitted in an update, to check
* @return TRUE iff root has been submitted & timeout has expired
*/
function acceptableRoot(bytes32 _root) public view returns (bool) {
uint256 _time = confirmAt[_root];
if (_time == 0) {
return false;
}
return block.timestamp >= _time;
}
/**
* @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)
* @dev For convenience, we allow proving against any previous root.
* This means that witnesses never need to be updated for the new root
* @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,
uint256 _index
) public returns (bool) {
// ensure that message has not been proven or processed
require(messages[_leaf] == MessageStatus.None, "!MessageStatus.None");
// calculate the expected root based on the proof
bytes32 _calculatedRoot = MerkleLib.branchRoot(_leaf, _proof, _index);
// if the root is valid, change status to Proven
if (acceptableRoot(_calculatedRoot)) {
messages[_leaf] = MessageStatus.Proven;
return true;
}
return false;
}
/**
* @notice Hash of Home domain concatenated with "OPTICS"
*/
function homeDomainHash() public view override returns (bytes32) {
return _homeDomainHash(remoteDomain);
}
// ============ Internal Functions ============
/**
* @notice Moves the contract into failed state
* @dev Called when a Double Update is submitted
*/
function _fail() internal override {
_setFailed();
}
/// @notice Hook for potential future use
// solhint-disable-next-line no-empty-blocks
function _beforeConfirm() internal {}
/// @notice Hook for potential future use
// solhint-disable-next-line no-empty-blocks
function _beforeUpdate() internal {}
}