// SPDX-License-Identifier: MIT pragma solidity 0.8.17; // ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════ import {Attestation, AttestationLib} from "../libs/memory/Attestation.sol"; import { IncorrectAttestation, IncorrectState, IndexOutOfRange, NonceOutOfRange, OutdatedNonce } from "../libs/Errors.sol"; import {ChainGas, GasData, GasDataLib} from "../libs/stack/GasData.sol"; import {MerkleMath} from "../libs/merkle/MerkleMath.sol"; import {Snapshot, SnapshotLib} from "../libs/memory/Snapshot.sol"; import {State, StateLib} from "../libs/memory/State.sol"; import {ChainContext} from "../libs/ChainContext.sol"; // ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════ import {AgentSecured} from "../base/AgentSecured.sol"; import {SnapshotHubEvents} from "../events/SnapshotHubEvents.sol"; import {ISnapshotHub} from "../interfaces/ISnapshotHub.sol"; import {IStatementInbox} from "../interfaces/IStatementInbox.sol"; // ═════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; /// @notice `SnapshotHub` is a parent contract for `Summit`. It is responsible for the following: /// - Accepting and storing Guard and Notary snapshots to keep track of all the remote `Origin` states. /// - Generating and storing Attestations derived from Notary snapshots, as well as verifying their validity. abstract contract SnapshotHub is AgentSecured, SnapshotHubEvents, ISnapshotHub { using AttestationLib for bytes; using SafeCast for uint256; using StateLib for bytes; /// @notice Struct that represents stored State of Origin contract /// @param guardIndex Index of Guard who submitted this State to Summit /// @param notaryIndex Index of Notary who submitted this State to Summit struct SummitState { bytes32 root; uint32 origin; uint32 nonce; uint40 blockNumber; uint40 timestamp; GasData gasData; uint32 guardIndex; uint32 notaryIndex; } // TODO: revisit packing struct SummitSnapshot { // TODO: compress this - indexes might as well be uint32/uint64 uint256[] statePtrs; uint256 sigIndex; } struct SummitAttestation { bytes32 snapRoot; bytes32 agentRoot; bytes32 snapGasHash; uint40 blockNumber; uint40 timestamp; } // ══════════════════════════════════════════════════ STORAGE ══════════════════════════════════════════════════════ /// @dev All States submitted by any of the Guards SummitState[] private _states; /// @dev All Snapshots submitted by any of the Guards SummitSnapshot[] private _guardSnapshots; /// @dev All Snapshots submitted by any of the Notaries SummitSnapshot[] private _notarySnapshots; /// @dev All Attestations created from Notary-submitted Snapshots /// Invariant: _attestations.length == _notarySnapshots.length SummitAttestation[] private _attestations; /// @dev Pointer for the given State Leaf of the origin /// with ZERO as a sentinel value for "state not submitted yet". // (origin => (stateLeaf => {state index in _states PLUS 1})) mapping(uint32 => mapping(bytes32 => uint256)) private _leafPtr; /// @dev Pointer for the latest Agent State of a given origin /// with ZERO as a sentinel value for "no states submitted yet". // (origin => (agent index => {latest state index in _states PLUS 1})) mapping(uint32 => mapping(uint32 => uint256)) private _latestStatePtr; /// @dev Latest nonce that a Notary created // (notary index => latest nonce) mapping(uint32 => uint32) private _latestAttNonce; /// @dev gap for upgrade safety uint256[43] private __GAP; // solhint-disable-line var-name-mixedcase // ═══════════════════════════════════════════════════ VIEWS ═══════════════════════════════════════════════════════ /// @inheritdoc ISnapshotHub function isValidAttestation(bytes memory attPayload) external view returns (bool isValid) { // This will revert if payload is not a formatted attestation Attestation attestation = attPayload.castToAttestation(); return _isValidAttestation(attestation); } /// @inheritdoc ISnapshotHub function getAttestation(uint32 attNonce) external view returns (bytes memory attPayload, bytes32 agentRoot, uint256[] memory snapGas) { if (attNonce >= _attestations.length) revert NonceOutOfRange(); SummitAttestation memory summitAtt = _attestations[attNonce]; attPayload = _formatSummitAttestation(summitAtt, attNonce); agentRoot = summitAtt.agentRoot; snapGas = _restoreSnapGas(_notarySnapshots[attNonce]); } /// @inheritdoc ISnapshotHub function getLatestAgentState(uint32 origin, address agent) external view returns (bytes memory stateData) { SummitState memory latestState = _latestState(origin, _agentStatus(agent).index); if (latestState.nonce == 0) return bytes(""); return _formatSummitState(latestState); } /// @inheritdoc ISnapshotHub function getLatestNotaryAttestation(address notary) external view returns (bytes memory attPayload, bytes32 agentRoot, uint256[] memory snapGas) { uint32 latestAttNonce = _latestAttNonce[_agentStatus(notary).index]; if (latestAttNonce != 0) { SummitAttestation memory summitAtt = _attestations[latestAttNonce]; attPayload = _formatSummitAttestation(summitAtt, latestAttNonce); agentRoot = summitAtt.agentRoot; snapGas = _restoreSnapGas(_notarySnapshots[latestAttNonce]); } } /// @inheritdoc ISnapshotHub function getGuardSnapshot(uint256 index) external view returns (bytes memory snapPayload, bytes memory snapSignature) { if (index >= _guardSnapshots.length) revert IndexOutOfRange(); return _restoreSnapshot(_guardSnapshots[index]); } /// @inheritdoc ISnapshotHub function getNotarySnapshot(uint256 index) public view returns (bytes memory snapPayload, bytes memory snapSignature) { uint256 nonce = index + 1; if (nonce >= _notarySnapshots.length) revert IndexOutOfRange(); return _restoreSnapshot(_notarySnapshots[nonce]); } /// @inheritdoc ISnapshotHub // solhint-disable-next-line ordering function getNotarySnapshot(bytes memory attPayload) external view returns (bytes memory snapPayload, bytes memory snapSignature) { // This will revert if payload is not a formatted attestation Attestation attestation = attPayload.castToAttestation(); if (!_isValidAttestation(attestation)) revert IncorrectAttestation(); // Attestation is valid => _attestations[nonce] exists // _notarySnapshots.length == _attestations.length => _notarySnapshots[nonce] exists return _restoreSnapshot(_notarySnapshots[attestation.nonce()]); } /// @inheritdoc ISnapshotHub function getSnapshotProof(uint32 attNonce, uint8 stateIndex) external view returns (bytes32[] memory snapProof) { if (attNonce == 0 || attNonce >= _notarySnapshots.length) revert NonceOutOfRange(); SummitSnapshot memory snap = _notarySnapshots[attNonce]; uint256 statesAmount = snap.statePtrs.length; if (stateIndex >= statesAmount) revert IndexOutOfRange(); // Reconstruct the leafs of Snapshot Merkle Tree: two for each state bytes32[] memory hashes = new bytes32[](2 * statesAmount); for (uint256 i = 0; i < statesAmount; ++i) { // Get value for "index in _states PLUS 1" uint256 statePtr = snap.statePtrs[i]; // We are never saving zero values when accepting Guard/Notary snapshots, so this holds assert(statePtr != 0); State state = _formatSummitState(_states[statePtr - 1]).castToState(); (hashes[2 * i], hashes[2 * i + 1]) = state.subLeafs(); } // Index of State's left leaf is twice the state index return MerkleMath.calculateProof(hashes, 2 * stateIndex); } // ════════════════════════════════════════ INTERNAL LOGIC: ACCEPT DATA ════════════════════════════════════════════ /// @dev Accepts a Snapshot signed by a Guard. /// It is assumed that the Guard signature has been checked outside of this contract. function _acceptGuardSnapshot(Snapshot snapshot, uint32 guardIndex, uint256 sigIndex) internal { // Snapshot Signer is a Guard: save the states for later use. uint256 statesAmount = snapshot.statesAmount(); uint256[] memory statePtrs = new uint256[](statesAmount); for (uint256 i = 0; i < statesAmount; ++i) { statePtrs[i] = _saveState(snapshot.state(i), guardIndex); // Guard either submitted a fresh state, or reused state submitted by another Guard // In any case, the "state pointer" would never be zero assert(statePtrs[i] != 0); } // Save Guard snapshot for later retrieval _saveGuardSnapshot(statePtrs, sigIndex); } /// @dev Accepts a Snapshot signed by a Notary. /// It is assumed that the Notary signature has been checked outside of this contract. /// Returns the attestation created from the Notary snapshot. function _acceptNotarySnapshot(Snapshot snapshot, bytes32 agentRoot, uint32 notaryIndex, uint256 sigIndex) internal returns (bytes memory attPayload) { // Snapshot Signer is a Notary: construct a Snapshot Merkle Tree, // while checking that the states were previously saved. uint256 statesAmount = snapshot.statesAmount(); uint256[] memory statePtrs = new uint256[](statesAmount); for (uint256 i = 0; i < statesAmount; ++i) { State state = snapshot.state(i); uint256 statePtr = _statePtr(state); // Notary can only used states previously submitted by any fo the Guards if (statePtr == 0) revert IncorrectState(); statePtrs[i] = statePtr; // Check that Notary hasn't used a fresher state for this origin before uint32 origin = state.origin(); if (state.nonce() <= _latestState(origin, notaryIndex).nonce) revert OutdatedNonce(); // Save Notary if they are the first to use this state if (_states[statePtr - 1].notaryIndex == 0) _states[statePtr - 1].notaryIndex = notaryIndex; // Update Notary latest state for origin _latestStatePtr[origin][notaryIndex] = statePtrs[i]; } // Derive the snapshot merkle root and save it for a Notary attestation. // Save Notary snapshot for later retrieval return _saveNotarySnapshot(snapshot, statePtrs, agentRoot, notaryIndex, sigIndex); } // ════════════════════════════════════ INTERNAL LOGIC: SAVE STATEMENT DATA ════════════════════════════════════════ /// @dev Initializes the saved _attestations list by inserting empty values. function _initializeAttestations() internal { // This should only be called once, when the contract is initialized assert(_attestations.length == 0); // Insert empty non-meaningful values, that can't be used to prove anything _attestations.push(_toSummitAttestation(0, 0, 0)); _notarySnapshots.push(SummitSnapshot(new uint256[](0), 0)); } /// @dev Saves the Guard snapshot. function _saveGuardSnapshot(uint256[] memory statePtrs, uint256 sigIndex) internal { _guardSnapshots.push(SummitSnapshot(statePtrs, sigIndex)); } /// @dev Saves the Notary snapshot and the attestation created from it. /// Returns the created attestation. function _saveNotarySnapshot( Snapshot snapshot, uint256[] memory statePtrs, bytes32 agentRoot, uint32 notaryIndex, uint256 sigIndex ) internal returns (bytes memory attPayload) { // Attestation nonce is its index in `_attestations` array // TODO: consider using more than 32 bits for attestation nonces uint32 attNonce = _attestations.length.toUint32(); bytes32 snapGasHash = GasDataLib.snapGasHash(snapshot.snapGas()); SummitAttestation memory summitAtt = _toSummitAttestation(snapshot.calculateRoot(), agentRoot, snapGasHash); attPayload = _formatSummitAttestation(summitAtt, attNonce); _latestAttNonce[notaryIndex] = attNonce; /// @dev Add a single element to both `_attestations` and `_notarySnapshots`, /// enforcing the (_attestations.length == _notarySnapshots.length) invariant. _attestations.push(summitAtt); _notarySnapshots.push(SummitSnapshot(statePtrs, sigIndex)); // Emit event with raw attestation data emit AttestationSaved(attPayload); } /// @dev Saves the state signed by a Guard. function _saveState(State state, uint32 guardIndex) internal returns (uint256 statePtr) { uint32 origin = state.origin(); // Check that Guard hasn't submitted a fresher State before if (state.nonce() <= _latestState(origin, guardIndex).nonce) revert OutdatedNonce(); bytes32 stateHash = state.leaf(); statePtr = _leafPtr[origin][stateHash]; // Save state only if it wasn't previously submitted if (statePtr == 0) { // Extract data that needs to be saved SummitState memory summitState = _toSummitState(state, guardIndex); _states.push(summitState); // State is stored at (length - 1), but we are tracking "index PLUS 1" as "pointer" statePtr = _states.length; _leafPtr[origin][stateHash] = statePtr; // Emit event with raw state data emit StateSaved(state.unwrap().clone()); } // Update latest guard state for origin _latestStatePtr[origin][guardIndex] = statePtr; } // ══════════════════════════════════════════════ INTERNAL VIEWS ═══════════════════════════════════════════════════ /// @dev Checks if attestation was previously submitted by a Notary (as a signed snapshot). function _isValidAttestation(Attestation att) internal view returns (bool) { // Check if nonce exists uint32 nonce = att.nonce(); if (nonce >= _attestations.length) return false; // Check if Attestation matches the historical one return _areEqual(att, _attestations[nonce]); } /// @dev Restores Snapshot payload from a list of state pointers used for the snapshot. function _restoreSnapshot(SummitSnapshot memory snapshot) internal view returns (bytes memory snapPayload, bytes memory snapSignature) { uint256 statesAmount = snapshot.statePtrs.length; State[] memory states = new State[](statesAmount); for (uint256 i = 0; i < statesAmount; ++i) { // Get value for "index in _states PLUS 1" uint256 statePtr = snapshot.statePtrs[i]; // We are never saving zero values when accepting Guard/Notary snapshots, so this holds assert(statePtr != 0); // Get the state that Agent used for the snapshot states[i] = _formatSummitState(_states[statePtr - 1]).castToState(); } snapPayload = SnapshotLib.formatSnapshot(states); snapSignature = IStatementInbox(inbox).getStoredSignature(snapshot.sigIndex); } /// @dev Restores the gas data from the snapshot. function _restoreSnapGas(SummitSnapshot memory snapshot) internal view returns (uint256[] memory snapGas) { uint256 statesAmount = snapshot.statePtrs.length; snapGas = new uint256[](statesAmount); for (uint256 i = 0; i < statesAmount; ++i) { // Get value for "index in _states PLUS 1" uint256 statePtr = snapshot.statePtrs[i]; // We are never saving zero values when accepting Guard/Notary snapshots, so this holds assert(statePtr != 0); // Get the state that Agent used for the snapshot snapGas[i] = ChainGas.unwrap( GasDataLib.encodeChainGas({ gasData_: _states[statePtr - 1].gasData, domain_: _states[statePtr - 1].origin }) ); } } /// @dev Returns indexes of agents who provided state data for the Notary snapshot with the given nonce. function _stateAgents(uint32 nonce, uint8 stateIndex) internal view returns (uint32 guardIndex, uint32 notaryIndex) { uint256 statePtr = _notarySnapshots[nonce].statePtrs[stateIndex]; return (_states[statePtr - 1].guardIndex, _states[statePtr - 1].notaryIndex); } /// @dev Returns the pointer for a matching Guard State, if it exists. function _statePtr(State state) internal view returns (uint256) { return _leafPtr[state.origin()][state.leaf()]; } /// @dev Returns the latest state submitted by the Agent for the origin. /// Will return an empty struct, if the Agent hasn't submitted a single origin State yet. function _latestState(uint32 origin, uint32 agentIndex) internal view returns (SummitState memory state) { // Get value for "index in _states PLUS 1" uint256 latestPtr = _latestStatePtr[origin][agentIndex]; // Check if the Agent has submitted at least one State for origin if (latestPtr != 0) { state = _states[latestPtr - 1]; } // An empty struct is returned if the Agent hasn't submitted a single State for origin yet. } // ═════════════════════════════════════════════ STRUCT FORMATTING ═════════════════════════════════════════════════ /// @dev Returns a formatted payload for a stored SummitState. function _formatSummitState(SummitState memory summitState) internal pure returns (bytes memory) { return StateLib.formatState({ root_: summitState.root, origin_: summitState.origin, nonce_: summitState.nonce, blockNumber_: summitState.blockNumber, timestamp_: summitState.timestamp, gasData_: summitState.gasData }); } /// @dev Returns a SummitState struct to save in the contract. function _toSummitState(State state, uint32 guardIndex) internal pure returns (SummitState memory summitState) { summitState.root = state.root(); summitState.origin = state.origin(); summitState.nonce = state.nonce(); summitState.blockNumber = state.blockNumber(); summitState.timestamp = state.timestamp(); summitState.gasData = state.gasData(); summitState.guardIndex = guardIndex; // summitState.notaryIndex is left as ZERO } /// @dev Returns a formatted payload for a stored SummitAttestation. function _formatSummitAttestation(SummitAttestation memory summitAtt, uint32 nonce) internal pure returns (bytes memory) { return AttestationLib.formatAttestation({ snapRoot_: summitAtt.snapRoot, dataHash_: AttestationLib.dataHash(summitAtt.agentRoot, summitAtt.snapGasHash), nonce_: nonce, blockNumber_: summitAtt.blockNumber, timestamp_: summitAtt.timestamp }); } /// @dev Returns an Attestation struct to save in the Summit contract. /// Current block number and timestamp are used. // solhint-disable-next-line ordering function _toSummitAttestation(bytes32 snapRoot, bytes32 agentRoot, bytes32 snapGasHash) internal view returns (SummitAttestation memory summitAtt) { summitAtt.snapRoot = snapRoot; summitAtt.agentRoot = agentRoot; summitAtt.snapGasHash = snapGasHash; summitAtt.blockNumber = ChainContext.blockNumber(); summitAtt.timestamp = ChainContext.blockTimestamp(); } /// @dev Checks that an Attestation and its Summit representation are equal. function _areEqual(Attestation att, SummitAttestation memory summitAtt) internal pure returns (bool) { // forgefmt: disable-next-item return att.snapRoot() == summitAtt.snapRoot && att.dataHash() == AttestationLib.dataHash(summitAtt.agentRoot, summitAtt.snapGasHash) && att.blockNumber() == summitAtt.blockNumber && att.timestamp() == summitAtt.timestamp; } }