// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "../vendor/openzeppelin/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "../vendor/openzeppelin/ReentrancyGuardUpgradeable.sol";
import "../ccip/IRouterClient.sol";

import {CheckpointStorage} from "./storage/CheckpointStorage.sol";
import {CheckpointLeaf} from "./libraries/CheckpointLeaf.sol";
import {CheckpointFlags} from "./libraries/CheckpointFlags.sol";
import {ICheckpointExtension} from "./interfaces/ICheckpointExtension.sol";
import {IChain138MainnetCheckpoint} from "./interfaces/IChain138MainnetCheckpoint.sol";
import {CheckpointEIP712} from "./libraries/CheckpointEIP712.sol";
import {CheckpointHubConfig} from "./libraries/CheckpointHubConfig.sol";
import {CheckpointErrors} from "./libraries/CheckpointErrors.sol";
import {CheckpointPaymentsLib} from "./libraries/CheckpointPaymentsLib.sol";
import {ExtensionIds} from "./libraries/ExtensionIds.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

/**
 * @title Chain138MainnetCheckpoint
 * @notice Upgradeable hub: state + payment batches from Chain 138 (default 10 txs).
 * @dev UUPS + EIP-7201 storage + extension registry. cW mint remains on CWMultiTokenBridge.
 */
contract Chain138MainnetCheckpoint is
    Initializable,
    AccessControlUpgradeable,
    ReentrancyGuardUpgradeable,
    UUPSUpgradeable,
    IChain138MainnetCheckpoint
{
    using CheckpointStorage for CheckpointStorage.CheckpointStorageStruct;

    uint256 public constant IMPLEMENTATION_VERSION = 4;
    uint64 public constant CHAIN_138 = 138;

    bytes32 public constant SUBMITTER_ROLE = keccak256("SUBMITTER_ROLE");
    bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");
    bytes32 public constant EXTENSION_ADMIN_ROLE = keccak256("EXTENSION_ADMIN_ROLE");
    bytes32 public constant EMERGENCY_ROLE = keccak256("EMERGENCY_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    bytes32 public constant CHECKPOINT_ATTEST_TYPEHASH =
        keccak256("BatchAttestation(uint64 chainId,uint64 batchId,uint256 checkpointBlock,bytes32 blockHash,bytes32 stateRoot,bytes32 paymentsRoot,uint64 previousBatchId)");

    uint32 public constant HOOK_BEFORE_SUBMIT = 1 << 0;
    uint32 public constant HOOK_AFTER_SUBMIT = 1 << 1;
    uint32 public constant HOOK_ON_CCIP = 1 << 2;
    uint32 public constant HOOK_VERIFY_LEAF = 1 << 3;

    event HubConfigApplied(CheckpointHubConfig.HubConfig config);

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize(
        address admin,
        address ccipRouter,
        uint64 sourceChainSelector,
        address batchEmitterOnSource
    ) external initializer {
        __AccessControl_init();
        __ReentrancyGuard_init();
        __UUPSUpgradeable_init();

        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(UPGRADER_ROLE, admin);
        _grantRole(SUBMITTER_ROLE, admin);
        _grantRole(EXTENSION_ADMIN_ROLE, admin);
        _grantRole(PAUSER_ROLE, admin);
        _grantRole(EMERGENCY_ROLE, admin);

        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        $.chainId = CHAIN_138;
        $.batchSize = 10;
        $.maxBatchWaitSeconds = 300;
        $.requireValidatorSigs = true;
        $.allowCalldataOnlySubmit = true;
        $.allowCCIPIngress = true;
        $.enforcePreviousBatchId = true;
        $.ccipRouter = ccipRouter;
        $.expectedSourceChainSelector = sourceChainSelector;
        $.batchEmitterOnSource = batchEmitterOnSource;
    }

    function initializeV2(address legacyMirror, address legacyTether, address attestationSigner) external reinitializer(2) {
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        $.legacyMirrorV1 = legacyMirror;
        $.legacyTetherV1 = legacyTether;
        $.submitterAttestationSigner = attestationSigner;
    }

    // --- views ---

    function getConfig()
        external
        view
        returns (
            uint16 batchSize,
            uint32 maxBatchWaitSeconds,
            uint256 minPaymentValueWei,
            bool requireValidatorSigs,
            bool allowCalldataOnlySubmit,
            bool allowCCIPIngress
        )
    {
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        return (
            $.batchSize,
            $.maxBatchWaitSeconds,
            $.minPaymentValueWei,
            $.requireValidatorSigs,
            $.allowCalldataOnlySubmit,
            $.allowCCIPIngress
        );
    }

    function getFullConfig() external view returns (CheckpointHubConfig.HubConfig memory cfg) {
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        cfg.chainId = $.chainId;
        cfg.batchSize = $.batchSize;
        cfg.maxBatchWaitSeconds = $.maxBatchWaitSeconds;
        cfg.minPaymentValueWei = $.minPaymentValueWei;
        cfg.requireValidatorSigs = $.requireValidatorSigs;
        cfg.allowCalldataOnlySubmit = $.allowCalldataOnlySubmit;
        cfg.allowCCIPIngress = $.allowCCIPIngress;
        cfg.enforcePreviousBatchId = $.enforcePreviousBatchId;
        cfg.ccipRouter = $.ccipRouter;
        cfg.sourceChainSelector = $.expectedSourceChainSelector;
        cfg.batchEmitterOnSource = $.batchEmitterOnSource;
        cfg.legacyMirrorV1 = $.legacyMirrorV1;
        cfg.legacyTetherV1 = $.legacyTetherV1;
        cfg.submitterAttestationSigner = $.submitterAttestationSigner;
    }

    function applyConfig(CheckpointHubConfig.HubConfig calldata cfg) external onlyRole(DEFAULT_ADMIN_ROLE) {
        CheckpointHubConfig.validate(cfg);
        _applyHubConfig(cfg);
        emit HubConfigApplied(cfg);
    }

    function extensionCount() external view returns (uint256) {
        return CheckpointStorage.get().extensionList.length;
    }

    function getExtension(bytes32 extensionId)
        external
        view
        returns (address module, uint32 hooks, bool active)
    {
        CheckpointStorage.ExtensionConfig storage cfg = CheckpointStorage.get().extensions[extensionId];
        return (cfg.module, cfg.hooks, cfg.active);
    }

    function getExtensionList() external view returns (bytes32[] memory) {
        return CheckpointStorage.get().extensionList;
    }

    function getLatestCheckpoint() external view returns (CheckpointStorage.CheckpointHeader memory) {
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        return $.checkpoints[$.latestBatchId];
    }

    function getCheckpoint(uint64 batchId) external view returns (CheckpointStorage.CheckpointHeader memory) {
        return CheckpointStorage.get().checkpoints[batchId];
    }

    function getLatestBatchId() external view returns (uint64) {
        return CheckpointStorage.get().latestBatchId;
    }

    function latestCheckpointBlock() external view returns (uint256) {
        return CheckpointStorage.get().latestCheckpointBlock;
    }

    function verifyPaymentInBatch(
        uint64 batchId,
        CheckpointLeaf.PaymentLeafV1 calldata leaf,
        bytes32[] calldata proof
    ) external view returns (bool) {
        CheckpointStorage.CheckpointHeader storage h = CheckpointStorage.get().checkpoints[batchId];
        if (h.batchId == 0) revert CheckpointErrors.UnknownBatch();
        bytes32 leafHash = CheckpointLeaf.paymentLeafV1(h.chainId, leaf);
        CheckpointStorage.CheckpointHeader memory hm = h;
        if (_extensionVerifyLeaf(hm, leaf, proof)) return true;
        return CheckpointLeaf.verifyMerkle(h.paymentsRoot, leafHash, proof);
    }

    function isTxIncluded(bytes32 txHash) external view returns (bool included, uint64 batchId) {
        batchId = CheckpointStorage.get().txHashToBatchId[txHash];
        included = batchId != 0;
    }

    // --- submit ---

    function paused() external view returns (bool) {
        return CheckpointStorage.get().paused;
    }

    function pause() external onlyRole(PAUSER_ROLE) {
        CheckpointStorage.get().paused = true;
    }

    function unpause() external onlyRole(PAUSER_ROLE) {
        CheckpointStorage.get().paused = false;
    }

    function submitCheckpoint(
        CheckpointStorage.CheckpointHeader calldata header,
        bytes calldata validatorSignatures,
        bytes32[] calldata txHashes,
        bytes calldata extensionData
    ) external onlyRole(SUBMITTER_ROLE) nonReentrant {
        _requireNotPaused();
        _runBeforeSubmitExtensions(header, validatorSignatures, extensionData);
        _submit(header, validatorSignatures, txHashes, bytes32(0), false, false, extensionData);
        _runExtensions(HOOK_AFTER_SUBMIT, header, extensionData);
    }

    function submitCheckpointByRelayer(
        CheckpointStorage.CheckpointHeader calldata header,
        bytes calldata validatorSignatures,
        bytes32[] calldata txHashes,
        bytes calldata extensionData,
        bytes calldata submitterSignature
    ) external onlyRole(RELAYER_ROLE) nonReentrant {
        _requireNotPaused();
        _verifySubmitterAttestation(header, submitterSignature);
        CheckpointStorage.CheckpointHeader memory h = header;
        h.flags |= CheckpointFlags.RELAYER_SUBMIT;
        _runBeforeSubmitExtensions(h, validatorSignatures, extensionData);
        _submit(h, validatorSignatures, txHashes, bytes32(0), false, false, extensionData);
        _runExtensions(HOOK_AFTER_SUBMIT, h, extensionData);
    }

    function submitCheckpointWithLeaves(
        CheckpointStorage.CheckpointHeader calldata header,
        bytes calldata validatorSignatures,
        bytes32[] calldata txHashes,
        CheckpointLeaf.PaymentLeafV1[] calldata leaves
    ) external onlyRole(SUBMITTER_ROLE) nonReentrant {
        _requireNotPaused();
        CheckpointPaymentsLib.assertPaymentsRootV1(header.chainId, header.paymentsRoot, leaves);
        bytes memory leafPayload = abi.encode(leaves);
        _runBeforeSubmitExtensions(header, validatorSignatures, leafPayload);
        _submit(header, validatorSignatures, txHashes, bytes32(0), false, false, leafPayload);
        _runExtensions(HOOK_AFTER_SUBMIT, header, leafPayload);
    }

    function forceCheckpoint(
        CheckpointStorage.CheckpointHeader calldata header,
        bytes calldata validatorSignatures,
        bytes32[] calldata txHashes,
        bytes calldata extensionData
    ) external onlyRole(EMERGENCY_ROLE) nonReentrant {
        CheckpointStorage.CheckpointHeader memory h = header;
        h.flags |= CheckpointFlags.EMERGENCY;
        _runBeforeSubmitExtensions(h, validatorSignatures, extensionData);
        _submit(h, validatorSignatures, txHashes, bytes32(0), false, true, extensionData);
        _runExtensions(HOOK_AFTER_SUBMIT, h, extensionData);
    }

    function submitCheckpointCommitment(
        CheckpointStorage.CheckpointHeader calldata header,
        bytes calldata validatorSignatures,
        bytes32 contentURI,
        bytes calldata extensionData
    ) external onlyRole(SUBMITTER_ROLE) nonReentrant {
        _requireNotPaused();
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        if (!$.allowCalldataOnlySubmit) revert CheckpointErrors.CalldataOnlyDisabled();
        _runBeforeSubmitExtensions(header, validatorSignatures, extensionData);
        _submit(header, validatorSignatures, new bytes32[](0), contentURI, true, false, extensionData);
        _runExtensions(HOOK_AFTER_SUBMIT, header, extensionData);
    }

    function ccipReceive(IRouterClient.Any2EVMMessage calldata message) external nonReentrant {
        _requireNotPaused();
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        if (!$.allowCCIPIngress) revert CheckpointErrors.CcipDisabled();
        if (msg.sender != $.ccipRouter) revert CheckpointErrors.OnlyRouter();
        if (message.sourceChainSelector != $.expectedSourceChainSelector) revert CheckpointErrors.BadSelector();

        address sender = message.sender.length >= 32
            ? address(uint160(uint256(bytes32(message.sender))))
            : address(bytes20(message.sender));
        if (sender != $.batchEmitterOnSource) revert CheckpointErrors.BadEmitter();

        (
            CheckpointStorage.CheckpointHeader memory header,
            bytes memory validatorSignatures,
            bytes32[] memory txHashes,
            bytes32 contentURI,
            bytes memory extensionData
        ) = abi.decode(
            message.data,
            (CheckpointStorage.CheckpointHeader, bytes, bytes32[], bytes32, bytes)
        );

        header.flags |= CheckpointFlags.CCIP_INGRESS;
        _runExtensions(HOOK_ON_CCIP, header, message.data);
        _runBeforeSubmitExtensions(header, validatorSignatures, extensionData);
        _submit(
            header,
            validatorSignatures,
            txHashes,
            contentURI,
            CheckpointFlags.has(header.flags, CheckpointFlags.CALLDATA_ONLY),
            false,
            extensionData
        );
        _runExtensions(HOOK_AFTER_SUBMIT, header, extensionData);
    }

    // --- extensions ---

    function registerExtension(bytes32 extensionId, address module, uint32 hooks) external onlyRole(EXTENSION_ADMIN_ROLE) {
        if (module == address(0)) revert CheckpointErrors.ZeroModule();
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        if ($.extensions[extensionId].active) revert CheckpointErrors.ExtensionExists();
        $.extensions[extensionId] = CheckpointStorage.ExtensionConfig({module: module, hooks: hooks, active: true});
        $.extensionList.push(extensionId);
        emit ExtensionRegistered(extensionId, module, hooks);
    }

    function revokeExtension(bytes32 extensionId) external onlyRole(EXTENSION_ADMIN_ROLE) {
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        if (!$.extensions[extensionId].active) revert CheckpointErrors.ExtensionMissing();
        delete $.extensions[extensionId];
        bytes32[] storage list = $.extensionList;
        for (uint256 i = 0; i < list.length; i++) {
            if (list[i] == extensionId) {
                list[i] = list[list.length - 1];
                list.pop();
                break;
            }
        }
        emit ExtensionRevoked(extensionId);
    }

    function setConfig(
        uint16 batchSize,
        uint32 maxBatchWaitSeconds,
        uint256 minPaymentValueWei,
        bool requireValidatorSigs,
        bool allowCalldataOnlySubmit,
        bool allowCCIPIngress
    ) external onlyRole(DEFAULT_ADMIN_ROLE) {
        if (batchSize == 0 || batchSize > 256) revert CheckpointErrors.BatchSize();
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        $.batchSize = batchSize;
        $.maxBatchWaitSeconds = maxBatchWaitSeconds;
        $.minPaymentValueWei = minPaymentValueWei;
        $.requireValidatorSigs = requireValidatorSigs;
        $.allowCalldataOnlySubmit = allowCalldataOnlySubmit;
        $.allowCCIPIngress = allowCCIPIngress;
    }

    function setExtensionActive(bytes32 extensionId, bool active) external onlyRole(EXTENSION_ADMIN_ROLE) {
        CheckpointStorage.ExtensionConfig storage cfg = CheckpointStorage.get().extensions[extensionId];
        if (cfg.module == address(0)) revert CheckpointErrors.ExtensionMissing();
        cfg.active = active;
    }

    function updateExtensionHooks(bytes32 extensionId, uint32 hooks) external onlyRole(EXTENSION_ADMIN_ROLE) {
        CheckpointStorage.ExtensionConfig storage cfg = CheckpointStorage.get().extensions[extensionId];
        if (cfg.module == address(0)) revert CheckpointErrors.ExtensionMissing();
        cfg.hooks = hooks;
    }

    function _authorizeUpgrade(address) internal override onlyRole(UPGRADER_ROLE) {}

    function _applyHubConfig(CheckpointHubConfig.HubConfig calldata cfg) internal {
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        $.chainId = cfg.chainId;
        $.batchSize = cfg.batchSize;
        $.maxBatchWaitSeconds = cfg.maxBatchWaitSeconds;
        $.minPaymentValueWei = cfg.minPaymentValueWei;
        $.requireValidatorSigs = cfg.requireValidatorSigs;
        $.allowCalldataOnlySubmit = cfg.allowCalldataOnlySubmit;
        $.allowCCIPIngress = cfg.allowCCIPIngress;
        $.enforcePreviousBatchId = cfg.enforcePreviousBatchId;
        if (cfg.ccipRouter != address(0)) $.ccipRouter = cfg.ccipRouter;
        if (cfg.sourceChainSelector != 0) $.expectedSourceChainSelector = cfg.sourceChainSelector;
        if (cfg.batchEmitterOnSource != address(0)) $.batchEmitterOnSource = cfg.batchEmitterOnSource;
        if (cfg.legacyMirrorV1 != address(0)) $.legacyMirrorV1 = cfg.legacyMirrorV1;
        if (cfg.legacyTetherV1 != address(0)) $.legacyTetherV1 = cfg.legacyTetherV1;
        if (cfg.submitterAttestationSigner != address(0)) {
            $.submitterAttestationSigner = cfg.submitterAttestationSigner;
        }
    }

  // --- internal ---

    function _requireNotPaused() internal view {
        if (CheckpointStorage.get().paused) revert CheckpointErrors.Paused();
    }

    function _submit(
        CheckpointStorage.CheckpointHeader memory header,
        bytes memory validatorSignatures,
        bytes32[] memory txHashes,
        bytes32 contentURI,
        bool calldataOnly,
        bool emergency,
        bytes memory extensionData
    ) internal {
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        if ($.processedBatchIds[header.batchId]) revert CheckpointErrors.BatchDone();
        if (header.chainId != $.chainId) revert CheckpointErrors.BadChain();
        if (header.batchId <= $.latestBatchId) revert CheckpointErrors.BatchOrder();
        if ($.enforcePreviousBatchId && $.latestBatchId > 0 && header.previousBatchId != $.latestBatchId) {
            revert CheckpointErrors.PrevBatch();
        }
        if (header.endBlock > 0 && header.endBlock < header.startBlock) revert CheckpointErrors.BadBlocks();
        if (header.txCount == 0 || header.txCount > $.batchSize) revert CheckpointErrors.TxCount();
        if (!emergency && !CheckpointFlags.has(header.flags, CheckpointFlags.PARTIAL_BATCH) && header.txCount != $.batchSize) {
            revert CheckpointErrors.IncompleteBatch();
        }
        if (header.paymentsRoot == bytes32(0)) revert CheckpointErrors.PaymentsRoot();
        if (header.stateRoot == bytes32(0)) revert CheckpointErrors.StateRoot();
        if (header.blockHash == bytes32(0)) revert CheckpointErrors.BlockHash();

        if ($.requireValidatorSigs && validatorSignatures.length == 0) revert CheckpointErrors.Signatures();
        if ($.minPaymentValueWei > 0 && extensionData.length > 0) {
            CheckpointPaymentsLib.enforceMinPaymentValueV1(extensionData, $.minPaymentValueWei);
        }

        bytes32 proofHash = keccak256(
            abi.encodePacked(
                header.batchId,
                header.paymentsRoot,
                header.stateRoot,
                header.checkpointBlock,
                validatorSignatures
            )
        );
        if ($.processedProofHashes[proofHash]) revert CheckpointErrors.Replay();
        $.processedProofHashes[proofHash] = true;

        if (calldataOnly) {
            header.flags |= CheckpointFlags.CALLDATA_ONLY;
        }
        if (contentURI != bytes32(0)) {
            header.flags |= CheckpointFlags.HAS_CONTENT_URI;
            header.contentURI = contentURI;
        }
        header.submittedAt = uint64(block.timestamp);
        header.submitter = msg.sender;

        for (uint256 i = 0; i < txHashes.length; i++) {
            if (txHashes[i] == bytes32(0)) revert CheckpointErrors.ZeroTx();
            if ($.txHashToBatchId[txHashes[i]] != 0) revert CheckpointErrors.TxSeen();
            $.txHashToBatchId[txHashes[i]] = header.batchId;
        }

        $.checkpoints[header.batchId] = header;
        $.processedBatchIds[header.batchId] = true;
        $.latestBatchId = header.batchId;
        $.latestCheckpointBlock = header.checkpointBlock;
        $.latestPaymentsRoot = header.paymentsRoot;

        emit CheckpointSubmitted(
            header.batchId,
            header.checkpointBlock,
            header.paymentsRoot,
            header.txCount,
            header.flags,
            header.contentURI
        );
    }

    /// @dev Validator extension expects ECDSA bytes; other extensions expect leaf/extension payload.
    function _runBeforeSubmitExtensions(
        CheckpointStorage.CheckpointHeader memory header,
        bytes memory validatorSignatures,
        bytes memory extensionData
    ) internal {
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        bytes32[] storage list = $.extensionList;
        bytes32 validatorId = ExtensionIds.VALIDATOR_SIG;
        CheckpointStorage.ExtensionConfig storage validatorCfg = $.extensions[validatorId];
        if (validatorCfg.active && (validatorCfg.hooks & HOOK_BEFORE_SUBMIT) != 0) {
            ICheckpointExtension(validatorCfg.module).beforeSubmit(header, validatorSignatures);
        }
        for (uint256 i = 0; i < list.length; i++) {
            bytes32 id = list[i];
            if (id == validatorId) continue;
            CheckpointStorage.ExtensionConfig storage cfg = $.extensions[id];
            if (!cfg.active || (cfg.hooks & HOOK_BEFORE_SUBMIT) == 0) continue;
            ICheckpointExtension(cfg.module).beforeSubmit(header, extensionData);
        }
    }

    function _runExtensions(
        uint32 hook,
        CheckpointStorage.CheckpointHeader memory header,
        bytes memory data
    ) internal {
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        bytes32[] storage list = $.extensionList;
        for (uint256 i = 0; i < list.length; i++) {
            CheckpointStorage.ExtensionConfig storage cfg = $.extensions[list[i]];
            if (!cfg.active || (cfg.hooks & hook) == 0) continue;
            if (hook == HOOK_BEFORE_SUBMIT) {
                ICheckpointExtension(cfg.module).beforeSubmit(header, data);
            } else if (hook == HOOK_AFTER_SUBMIT) {
                ICheckpointExtension(cfg.module).afterSubmit(header, data);
            } else if (hook == HOOK_ON_CCIP) {
                ICheckpointExtension(cfg.module).onCCIPReceive(data);
            }
        }
    }

    function _verifySubmitterAttestation(
        CheckpointStorage.CheckpointHeader calldata header,
        bytes calldata submitterSignature
    ) internal view {
        if (submitterSignature.length != 65) revert CheckpointErrors.SubmitterSigLen();
        bytes32 digest = CheckpointEIP712.digest(address(this), block.chainid, header);
        address signer = ECDSA.recover(digest, submitterSignature);
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        if ($.submitterAttestationSigner != address(0)) {
            if (signer != $.submitterAttestationSigner) revert CheckpointErrors.AttestSigner();
            return;
        }
        if (!hasRole(SUBMITTER_ROLE, signer)) revert CheckpointErrors.SubmitterRole();
    }

    function _extensionVerifyLeaf(
        CheckpointStorage.CheckpointHeader memory h,
        CheckpointLeaf.PaymentLeafV1 calldata leaf,
        bytes32[] calldata proof
    ) internal view returns (bool) {
        CheckpointStorage.CheckpointStorageStruct storage $ = CheckpointStorage.get();
        bytes32[] storage list = $.extensionList;
        for (uint256 i = 0; i < list.length; i++) {
            CheckpointStorage.ExtensionConfig storage cfg = $.extensions[list[i]];
            if (!cfg.active || (cfg.hooks & HOOK_VERIFY_LEAF) == 0) continue;
            if (ICheckpointExtension(cfg.module).verifyLeaf(h, leaf, proof)) return true;
        }
        return false;
    }
}
