packages/contracts-core/contracts/libs/memory/Attestation.sol
// 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}));
}
}