synapsecns/sanguine

View on GitHub
packages/contracts-core/contracts/libs/memory/Attestation.sol

Summary

Maintainability
Test Coverage
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {MemView, MemViewLib} from "./MemView.sol";
import {ATTESTATION_LENGTH, ATTESTATION_VALID_SALT, ATTESTATION_INVALID_SALT} from "../Constants.sol";
import {UnformattedAttestation} from "../Errors.sol";

/// Attestation is a memory view over a formatted attestation payload.
type Attestation is uint256;

using AttestationLib for Attestation global;

/// # Attestation
/// Attestation structure represents the "Snapshot Merkle Tree" created from
/// every Notary snapshot accepted by the Summit contract. Attestation includes"
/// the root of the "Snapshot Merkle Tree", as well as additional metadata.
///
/// ## Steps for creation of "Snapshot Merkle Tree":
/// 1. The list of hashes is composed for states in the Notary snapshot.
/// 2. The list is padded with zero values until its length is 2**SNAPSHOT_TREE_HEIGHT.
/// 3. Values from the list are used as leafs and the merkle tree is constructed.
///
/// ## Differences between a State and Attestation
/// Similar to Origin, every derived Notary's "Snapshot Merkle Root" is saved in Summit contract.
/// The main difference is that Origin contract itself is keeping track of an incremental merkle tree,
/// by inserting the hash of the sent message and calculating the new "Origin Merkle Root".
/// While Summit relies on Guards and Notaries to provide snapshot data, which is used to calculate the
/// "Snapshot Merkle Root".
///
/// - Origin's State is "state of Origin Merkle Tree after N-th message was sent".
/// - Summit's Attestation is "data for the N-th accepted Notary Snapshot" + "agent merkle root at the
/// time snapshot was submitted" + "attestation metadata".
///
/// ## Attestation validity
/// - Attestation is considered "valid" in Summit contract, if it matches the N-th (nonce)
/// snapshot submitted by Notaries, as well as the historical agent merkle root.
/// - Attestation is considered "valid" in Origin contract, if its underlying Snapshot is "valid".
///
/// - This means that a snapshot could be "valid" in Summit contract and "invalid" in Origin, if the underlying
/// snapshot is invalid (i.e. one of the states in the list is invalid).
/// - The opposite could also be true. If a perfectly valid snapshot was never submitted to Summit, its attestation
/// would be valid in Origin, but invalid in Summit (it was never accepted, so the metadata would be incorrect).
///
/// - Attestation is considered "globally valid", if it is valid in the Summit and all the Origin contracts.
/// # Memory layout of Attestation fields
///
/// | Position   | Field       | Type    | Bytes | Description                                                    |
/// | ---------- | ----------- | ------- | ----- | -------------------------------------------------------------- |
/// | [000..032) | snapRoot    | bytes32 | 32    | Root for "Snapshot Merkle Tree" created from a Notary snapshot |
/// | [032..064) | dataHash    | bytes32 | 32    | Agent Root and SnapGasHash combined into a single hash         |
/// | [064..068) | nonce       | uint32  | 4     | Total amount of all accepted Notary snapshots                  |
/// | [068..073) | blockNumber | uint40  | 5     | Block when this Notary snapshot was accepted in Summit         |
/// | [073..078) | timestamp   | uint40  | 5     | Time when this Notary snapshot was accepted in Summit          |
///
/// @dev Attestation could be signed by a Notary and submitted to `Destination` in order to use if for proving
/// messages coming from origin chains that the initial snapshot refers to.
library AttestationLib {
    using MemViewLib for bytes;

    // TODO: compress three hashes into one?

    /// @dev The variables below are not supposed to be used outside of the library directly.
    uint256 private constant OFFSET_SNAP_ROOT = 0;
    uint256 private constant OFFSET_DATA_HASH = 32;
    uint256 private constant OFFSET_NONCE = 64;
    uint256 private constant OFFSET_BLOCK_NUMBER = 68;
    uint256 private constant OFFSET_TIMESTAMP = 73;

    // ════════════════════════════════════════════════ ATTESTATION ════════════════════════════════════════════════════

    /**
     * @notice Returns a formatted Attestation payload with provided fields.
     * @param snapRoot_     Snapshot merkle tree's root
     * @param dataHash_     Agent Root and SnapGasHash combined into a single hash
     * @param nonce_        Attestation Nonce
     * @param blockNumber_  Block number when attestation was created in Summit
     * @param timestamp_    Block timestamp when attestation was created in Summit
     * @return Formatted attestation
     */
    function formatAttestation(
        bytes32 snapRoot_,
        bytes32 dataHash_,
        uint32 nonce_,
        uint40 blockNumber_,
        uint40 timestamp_
    ) internal pure returns (bytes memory) {
        return abi.encodePacked(snapRoot_, dataHash_, nonce_, blockNumber_, timestamp_);
    }

    /**
     * @notice Returns an Attestation view over the given payload.
     * @dev Will revert if the payload is not an attestation.
     */
    function castToAttestation(bytes memory payload) internal pure returns (Attestation) {
        return castToAttestation(payload.ref());
    }

    /**
     * @notice Casts a memory view to an Attestation view.
     * @dev Will revert if the memory view is not over an attestation.
     */
    function castToAttestation(MemView memView) internal pure returns (Attestation) {
        if (!isAttestation(memView)) revert UnformattedAttestation();
        return Attestation.wrap(MemView.unwrap(memView));
    }

    /// @notice Checks that a payload is a formatted Attestation.
    function isAttestation(MemView memView) internal pure returns (bool) {
        return memView.len() == ATTESTATION_LENGTH;
    }

    /// @notice Returns the hash of an Attestation, that could be later signed by a Notary to signal
    /// that the attestation is valid.
    function hashValid(Attestation att) internal pure returns (bytes32) {
        // The final hash to sign is keccak(attestationSalt, keccak(attestation))
        return att.unwrap().keccakSalted(ATTESTATION_VALID_SALT);
    }

    /// @notice Returns the hash of an Attestation, that could be later signed by a Guard to signal
    /// that the attestation is invalid.
    function hashInvalid(Attestation att) internal pure returns (bytes32) {
        // The final hash to sign is keccak(attestationInvalidSalt, keccak(attestation))
        return att.unwrap().keccakSalted(ATTESTATION_INVALID_SALT);
    }

    /// @notice Convenience shortcut for unwrapping a view.
    function unwrap(Attestation att) internal pure returns (MemView) {
        return MemView.wrap(Attestation.unwrap(att));
    }

    // ════════════════════════════════════════════ ATTESTATION SLICING ════════════════════════════════════════════════

    /// @notice Returns root of the Snapshot merkle tree created in the Summit contract.
    function snapRoot(Attestation att) internal pure returns (bytes32) {
        return att.unwrap().index({index_: OFFSET_SNAP_ROOT, bytes_: 32});
    }

    /// @notice Returns hash of the Agent Root and SnapGasHash combined into a single hash.
    function dataHash(Attestation att) internal pure returns (bytes32) {
        return att.unwrap().index({index_: OFFSET_DATA_HASH, bytes_: 32});
    }

    /// @notice Returns hash of the Agent Root and SnapGasHash combined into a single hash.
    function dataHash(bytes32 agentRoot_, bytes32 snapGasHash_) internal pure returns (bytes32) {
        return keccak256(bytes.concat(agentRoot_, snapGasHash_));
    }

    /// @notice Returns nonce of Summit contract at the time, when attestation was created.
    function nonce(Attestation att) internal pure returns (uint32) {
        // Can be safely casted to uint32, since we index 4 bytes
        return uint32(att.unwrap().indexUint({index_: OFFSET_NONCE, bytes_: 4}));
    }

    /// @notice Returns a block number when attestation was created in Summit.
    function blockNumber(Attestation att) internal pure returns (uint40) {
        // Can be safely casted to uint40, since we index 5 bytes
        return uint40(att.unwrap().indexUint({index_: OFFSET_BLOCK_NUMBER, bytes_: 5}));
    }

    /// @notice Returns a block timestamp when attestation was created in Summit.
    /// @dev This is the timestamp according to the Synapse Chain.
    function timestamp(Attestation att) internal pure returns (uint40) {
        // Can be safely casted to uint40, since we index 5 bytes
        return uint40(att.unwrap().indexUint({index_: OFFSET_TIMESTAMP, bytes_: 5}));
    }
}