packages/contracts-core/contracts/libs/memory/State.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import {MemView, MemViewLib} from "./MemView.sol";
import {GAS_DATA_LENGTH, STATE_LENGTH, STATE_INVALID_SALT} from "../Constants.sol";
import {UnformattedState} from "../Errors.sol";
import {GasData, GasDataLib} from "../stack/GasData.sol";
/// State is a memory view over a formatted state payload.
type State is uint256;
using StateLib for State global;
/// # State
/// State structure represents the state of Origin contract at some point of time.
/// - State is structured in a way to track the updates of the Origin Merkle Tree.
/// - State includes root of the Origin Merkle Tree, origin domain and some additional metadata.
/// ## Origin Merkle Tree
/// Hash of every sent message is inserted in the Origin Merkle Tree, which changes
/// the value of Origin Merkle Root (which is the root for the mentioned tree).
/// - Origin has a single Merkle Tree for all messages, regardless of their destination domain.
/// - This leads to Origin state being updated if and only if a message was sent in a block.
/// - Origin contract is a "source of truth" for states: a state is considered "valid" in its Origin,
/// if it matches the state of the Origin contract after the N-th (nonce) message was sent.
///
/// # Memory layout of State fields
///
/// | Position | Field | Type | Bytes | Description |
/// | ---------- | ----------- | ------- | ----- | ------------------------------ |
/// | [000..032) | root | bytes32 | 32 | Root of the Origin Merkle Tree |
/// | [032..036) | origin | uint32 | 4 | Domain where Origin is located |
/// | [036..040) | nonce | uint32 | 4 | Amount of sent messages |
/// | [040..045) | blockNumber | uint40 | 5 | Block of last sent message |
/// | [045..050) | timestamp | uint40 | 5 | Time of last sent message |
/// | [050..062) | gasData | uint96 | 12 | Gas data for the chain |
///
/// @dev State could be used to form a Snapshot to be signed by a Guard or a Notary.
library StateLib {
using MemViewLib for bytes;
/// @dev The variables below are not supposed to be used outside of the library directly.
uint256 private constant OFFSET_ROOT = 0;
uint256 private constant OFFSET_ORIGIN = 32;
uint256 private constant OFFSET_NONCE = 36;
uint256 private constant OFFSET_BLOCK_NUMBER = 40;
uint256 private constant OFFSET_TIMESTAMP = 45;
uint256 private constant OFFSET_GAS_DATA = 50;
// ═══════════════════════════════════════════════════ STATE ═══════════════════════════════════════════════════════
/**
* @notice Returns a formatted State payload with provided fields
* @param root_ New merkle root
* @param origin_ Domain of Origin's chain
* @param nonce_ Nonce of the merkle root
* @param blockNumber_ Block number when root was saved in Origin
* @param timestamp_ Block timestamp when root was saved in Origin
* @param gasData_ Gas data for the chain
* @return Formatted state
*/
function formatState(
bytes32 root_,
uint32 origin_,
uint32 nonce_,
uint40 blockNumber_,
uint40 timestamp_,
GasData gasData_
) internal pure returns (bytes memory) {
return abi.encodePacked(root_, origin_, nonce_, blockNumber_, timestamp_, gasData_);
}
/**
* @notice Returns a State view over the given payload.
* @dev Will revert if the payload is not a state.
*/
function castToState(bytes memory payload) internal pure returns (State) {
return castToState(payload.ref());
}
/**
* @notice Casts a memory view to a State view.
* @dev Will revert if the memory view is not over a state.
*/
function castToState(MemView memView) internal pure returns (State) {
if (!isState(memView)) revert UnformattedState();
return State.wrap(MemView.unwrap(memView));
}
/// @notice Checks that a payload is a formatted State.
function isState(MemView memView) internal pure returns (bool) {
return memView.len() == STATE_LENGTH;
}
/// @notice Returns the hash of a State, that could be later signed by a Guard to signal
/// that the state is invalid.
function hashInvalid(State state) internal pure returns (bytes32) {
// The final hash to sign is keccak(stateInvalidSalt, keccak(state))
return state.unwrap().keccakSalted(STATE_INVALID_SALT);
}
/// @notice Convenience shortcut for unwrapping a view.
function unwrap(State state) internal pure returns (MemView) {
return MemView.wrap(State.unwrap(state));
}
/// @notice Compares two State structures.
function equals(State a, State b) internal pure returns (bool) {
// Length of a State payload is fixed, so we just need to compare the hashes
return a.unwrap().keccak() == b.unwrap().keccak();
}
// ═══════════════════════════════════════════════ STATE HASHING ═══════════════════════════════════════════════════
/// @notice Returns the hash of the State.
/// @dev We are using the Merkle Root of a tree with two leafs (see below) as state hash.
function leaf(State state) internal pure returns (bytes32) {
(bytes32 leftLeaf_, bytes32 rightLeaf_) = state.subLeafs();
// Final hash is the parent of these leafs
return keccak256(bytes.concat(leftLeaf_, rightLeaf_));
}
/// @notice Returns "sub-leafs" of the State. Hash of these "sub leafs" is going to be used
/// as a "state leaf" in the "Snapshot Merkle Tree".
/// This enables proving that leftLeaf = (root, origin) was a part of the "Snapshot Merkle Tree",
/// by combining `rightLeaf` with the remainder of the "Snapshot Merkle Proof".
function subLeafs(State state) internal pure returns (bytes32 leftLeaf_, bytes32 rightLeaf_) {
MemView memView = state.unwrap();
// Left leaf is (root, origin)
leftLeaf_ = memView.prefix({len_: OFFSET_NONCE}).keccak();
// Right leaf is (metadata), or (nonce, blockNumber, timestamp)
rightLeaf_ = memView.sliceFrom({index_: OFFSET_NONCE}).keccak();
}
/// @notice Returns the left "sub-leaf" of the State.
function leftLeaf(bytes32 root_, uint32 origin_) internal pure returns (bytes32) {
// We use encodePacked here to simulate the State memory layout
return keccak256(abi.encodePacked(root_, origin_));
}
/// @notice Returns the right "sub-leaf" of the State.
function rightLeaf(uint32 nonce_, uint40 blockNumber_, uint40 timestamp_, GasData gasData_)
internal
pure
returns (bytes32)
{
// We use encodePacked here to simulate the State memory layout
return keccak256(abi.encodePacked(nonce_, blockNumber_, timestamp_, gasData_));
}
// ═══════════════════════════════════════════════ STATE SLICING ═══════════════════════════════════════════════════
/// @notice Returns a historical Merkle root from the Origin contract.
function root(State state) internal pure returns (bytes32) {
return state.unwrap().index({index_: OFFSET_ROOT, bytes_: 32});
}
/// @notice Returns domain of chain where the Origin contract is deployed.
function origin(State state) internal pure returns (uint32) {
// Can be safely casted to uint32, since we index 4 bytes
return uint32(state.unwrap().indexUint({index_: OFFSET_ORIGIN, bytes_: 4}));
}
/// @notice Returns nonce of Origin contract at the time, when `root` was the Merkle root.
function nonce(State state) internal pure returns (uint32) {
// Can be safely casted to uint32, since we index 4 bytes
return uint32(state.unwrap().indexUint({index_: OFFSET_NONCE, bytes_: 4}));
}
/// @notice Returns a block number when `root` was saved in Origin.
function blockNumber(State state) internal pure returns (uint40) {
// Can be safely casted to uint40, since we index 5 bytes
return uint40(state.unwrap().indexUint({index_: OFFSET_BLOCK_NUMBER, bytes_: 5}));
}
/// @notice Returns a block timestamp when `root` was saved in Origin.
/// @dev This is the timestamp according to the origin chain.
function timestamp(State state) internal pure returns (uint40) {
// Can be safely casted to uint40, since we index 5 bytes
return uint40(state.unwrap().indexUint({index_: OFFSET_TIMESTAMP, bytes_: 5}));
}
/// @notice Returns gas data for the chain.
function gasData(State state) internal pure returns (GasData) {
return GasDataLib.wrapGasData(state.unwrap().indexUint({index_: OFFSET_GAS_DATA, bytes_: GAS_DATA_LENGTH}));
}
}