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

import { IBondManager } from "../iface/IBondManager.sol";
import { ICodec } from "../iface/ICodec.sol";
import { IForcedInclusionStore } from "../iface/IForcedInclusionStore.sol";
import { IInbox } from "../iface/IInbox.sol";
import { IProposerChecker } from "../iface/IProposerChecker.sol";
import { IProverWhitelist } from "../iface/IProverWhitelist.sol";
import { LibBlobs } from "../libs/LibBlobs.sol";
import { LibBonds } from "../libs/LibBonds.sol";
import { LibCodec } from "../libs/LibCodec.sol";
import { LibForcedInclusion } from "../libs/LibForcedInclusion.sol";
import { LibHashOptimized } from "../libs/LibHashOptimized.sol";
import { LibInboxSetup } from "../libs/LibInboxSetup.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IProofVerifier } from "src/layer1/verifiers/IProofVerifier.sol";
import { EssentialContract } from "src/shared/common/EssentialContract.sol";
import { LibAddress } from "src/shared/libs/LibAddress.sol";
import { LibMath } from "src/shared/libs/LibMath.sol";
import { ICheckpointStore } from "src/shared/signal/ICheckpointStore.sol";
import { ISignalService } from "src/shared/signal/ISignalService.sol";

/// @title Inbox
/// @notice Core contract for managing L2 proposals, proof verification, and forced inclusion in
/// Taiko's based rollup architecture.
/// @dev The Pacaya inbox contract is not being upgraded to the Shasta implementation;
///      instead, Shasta uses a separate inbox address.
/// @dev This contract implements the fundamental inbox logic including:
///      - Proposal submission with forced inclusion support
///      - Sequential proof verification
///      - Ring buffer storage for efficient state management
///      - Bond accounting and liveness bond processing
///      - Finalization of proven proposals with checkpoint syncing
/// @custom:security-contact security@taiko.xyz
contract Inbox is IInbox, ICodec, IForcedInclusionStore, IBondManager, EssentialContract {
    using LibAddress for address;
    using LibBonds for LibBonds.Storage;
    using LibForcedInclusion for LibForcedInclusion.Storage;
    using LibMath for uint48;
    using LibMath for uint256;

    // ---------------------------------------------------------------
    // Structs
    // ---------------------------------------------------------------

    /// @notice Result from consuming forced inclusions
    struct ConsumptionResult {
        DerivationSource[] sources;
    }

    // ---------------------------------------------------------------
    // Events
    // ---------------------------------------------------------------

    event InboxActivated(bytes32 lastPacayaBlockHash);

    // ---------------------------------------------------------------
    // Constants
    // ---------------------------------------------------------------

    /// @notice Maximum number of forced inclusions processed per proposal.
    /// @dev Must be < 12 to avoid derived block timestamps drifting into the future when proposals
    /// happen every L1 slot (Derivation enforces 1s block times).
    uint256 internal constant MAX_FORCED_INCLUSIONS_PER_PROPOSAL = 10;

    // ---------------------------------------------------------------
    // Immutable Variables
    // ---------------------------------------------------------------

    /// @notice The proof verifier contract.
    IProofVerifier internal immutable _proofVerifier;

    /// @notice The proposer checker contract.
    IProposerChecker internal immutable _proposerChecker;

    /// @notice The prover whitelist contract (address(0) means no whitelist)
    IProverWhitelist internal immutable _proverWhitelist;

    /// @notice Signal service responsible for checkpoints.
    ISignalService internal immutable _signalService;

    /// @notice ERC20 token used as bond.
    IERC20 internal immutable _bondToken;

    /// @notice Minimum bond the proposer is required to have in gwei.
    uint64 internal immutable _minBond;

    /// @notice Liveness bond amount in gwei.
    uint64 internal immutable _livenessBond;

    /// @notice Time delay required before withdrawal after request.
    uint48 internal immutable _withdrawalDelay;

    /// @notice The proving window in seconds.
    uint48 internal immutable _provingWindow;

    /// @notice The delay after which proving becomes permissionless when whitelist is enabled.
    uint48 internal immutable _permissionlessProvingDelay;

    /// @notice Maximum delay allowed between sequential proofs to remain on time.
    uint48 internal immutable _maxProofSubmissionDelay;

    /// @notice The ring buffer size for storing proposal hashes.
    uint48 internal immutable _ringBufferSize;

    /// @notice The percentage of basefee paid to coinbase.
    uint8 internal immutable _basefeeSharingPctg;

    /// @notice The delay for forced inclusions measured in seconds.
    uint16 internal immutable _forcedInclusionDelay;

    /// @notice The base fee for forced inclusions in Gwei.
    uint64 internal immutable _forcedInclusionFeeInGwei;

    /// @notice Queue size at which the fee doubles. See Config for formula details.
    uint64 internal immutable _forcedInclusionFeeDoubleThreshold;

    /// @notice The multiplier to determine when a forced inclusion is too old so that proposing
    /// becomes permissionless
    uint8 internal immutable _permissionlessInclusionMultiplier;

    // ---------------------------------------------------------------
    // State Variables
    // ---------------------------------------------------------------

    /// @notice The timestamp when the first activation occurred.
    uint48 public activationTimestamp;

    /// @notice Persisted core state.
    CoreState internal _coreState;

    /// @dev Ring buffer for storing proposal hashes indexed by buffer slot
    /// - bufferSlot: The ring buffer slot calculated as proposalId % ringBufferSize
    /// - proposalHash: The keccak256 hash of the Proposal struct
    mapping(uint256 bufferSlot => bytes32 proposalHash) internal _proposalHashes;

    /// @dev Storage for forced inclusion requests
    /// @dev 2 slots used
    LibForcedInclusion.Storage private _forcedInclusionStorage;

    /// @dev Storage for bond balances.
    LibBonds.Storage private _bondStorage;

    uint256[43] private __gap;

    // ---------------------------------------------------------------
    // Constructor
    // ---------------------------------------------------------------

    /// @notice Initializes the Inbox contract
    /// @param _config Configuration struct containing all constructor parameters
    constructor(Config memory _config) {
        LibInboxSetup.validateConfig(_config);

        _proofVerifier = IProofVerifier(_config.proofVerifier);
        _proposerChecker = IProposerChecker(_config.proposerChecker);
        _proverWhitelist = IProverWhitelist(_config.proverWhitelist);
        _signalService = ISignalService(_config.signalService);
        _bondToken = IERC20(_config.bondToken);
        _minBond = _config.minBond;
        _livenessBond = _config.livenessBond;
        _withdrawalDelay = _config.withdrawalDelay;
        _provingWindow = _config.provingWindow;
        _permissionlessProvingDelay = _config.permissionlessProvingDelay;
        _maxProofSubmissionDelay = _config.maxProofSubmissionDelay;
        _ringBufferSize = _config.ringBufferSize;
        _basefeeSharingPctg = _config.basefeeSharingPctg;
        _forcedInclusionDelay = _config.forcedInclusionDelay;
        _forcedInclusionFeeInGwei = _config.forcedInclusionFeeInGwei;
        _forcedInclusionFeeDoubleThreshold = _config.forcedInclusionFeeDoubleThreshold;
        _permissionlessInclusionMultiplier = _config.permissionlessInclusionMultiplier;
    }

    // ---------------------------------------------------------------
    // External Functions
    // ---------------------------------------------------------------

    /// @notice Initializes the owner of the inbox.
    /// @param _owner The owner of this contract
    function init(address _owner) external initializer {
        __Essential_init(_owner);
    }

    /// @notice Activates the inbox so that it can start accepting proposals.
    /// @dev Can be called multiple times within the activation window to handle reorgs.
    /// @param _lastPacayaBlockHash The block hash of the last Pacaya block
    function activate(bytes32 _lastPacayaBlockHash) external onlyOwner {
        (
            uint48 newActivationTimestamp,
            CoreState memory state,
            Proposal memory proposal,
            bytes32 genesisProposalHash
        ) = LibInboxSetup.activate(_lastPacayaBlockHash, activationTimestamp);

        activationTimestamp = newActivationTimestamp;
        _coreState = state;
        _setProposalHash(0, genesisProposalHash);
        _emitProposedEvent(proposal);
        emit InboxActivated(_lastPacayaBlockHash);
    }

    /// @notice One-time owner recovery that resets the inbox core state after an incident.
    /// @dev Invoke via `upgradeToAndCall` on the proxy. The caller must supply a consistent
    ///      `(_lastFinalizedProposalId, _lastFinalizedBlockHash)` pair: `_lastFinalizedBlockHash`
    ///      must be the true L2 end-block hash of `_lastFinalizedProposalId`, otherwise `prove`
    ///      cannot resume.
    /// @dev `nextProposalId` and `lastProposalBlockId` are intentionally preserved from the current
    ///      core state instead of being reset. Proposals submitted after the incident keep their
    ///      unique IDs, so resuming proposing cannot collide with existing on-chain proposal IDs.
    /// @param _lastFinalizedProposalId The ID to record as last finalized. Must be less than the
    ///        current `nextProposalId`, and the unfinalized range
    ///        `nextProposalId - _lastFinalizedProposalId` must be smaller than the ring buffer size
    ///        so proposing can resume. The chosen finalized proposal must still be anchored in the
    ///        current ring buffer.
    /// @param _lastFinalizedBlockHash The true L2 block hash at the end of `_lastFinalizedProposalId`.
    function init2(
        uint48 _lastFinalizedProposalId,
        bytes32 _lastFinalizedBlockHash
    )
        external
        onlyOwner
        reinitializer(2)
    {
        CoreState memory currentState = _coreState;

        require(currentState.nextProposalId > 0, InvalidRecoveryState());
        require(_lastFinalizedProposalId < currentState.nextProposalId, InvalidRecoveryState());
        require(
            currentState.nextProposalId - _lastFinalizedProposalId < _ringBufferSize,
            InvalidRecoveryState()
        );
        require(_lastFinalizedBlockHash != 0, InvalidRecoveryState());

        currentState.lastFinalizedProposalId = _lastFinalizedProposalId;
        currentState.lastFinalizedTimestamp = uint48(block.timestamp);
        currentState.lastCheckpointTimestamp = uint48(block.timestamp);
        currentState.lastFinalizedBlockHash = _lastFinalizedBlockHash;
        _coreState = currentState;

        emit StateRecovered(
            currentState.nextProposalId, _lastFinalizedProposalId, _lastFinalizedBlockHash
        );
    }

    /// @notice One-time owner function that voids all queued forced inclusions by moving the
    ///         queue head to the tail.
    /// @dev Invoke via `upgradeToAndCall` on the proxy. The skipped entries were queued while
    ///      forced inclusions were disabled after the June 2026 incident; their blobs have
    ///      expired from the blob retention window and can no longer be derived, so they must
    ///      be evicted before forced inclusion processing is re-enabled. Their fees remain in
    ///      the contract.
    function init3() external onlyOwner reinitializer(3) {
        LibForcedInclusion.Storage storage $ = _forcedInclusionStorage;
        (uint48 head, uint48 tail) = ($.head, $.tail);
        $.head = tail;
        emit ForcedInclusionsVoided(head, tail);
    }

    /// @inheritdoc IInbox
    /// @notice Proposes new L2 blocks and forced inclusions to the rollup using blobs for DA.
    /// @dev Key behaviors:
    ///      1. Validates proposer authorization via `IProposerChecker`
    ///      2. Processes up to `min(input.numForcedInclusions, MAX_FORCED_INCLUSIONS_PER_PROPOSAL)`
    ///         forced inclusions. If forced inclusions are due, the proposer must request at least
    ///         `min(numDue, MAX_FORCED_INCLUSIONS_PER_PROPOSAL)` forced inclusions.
    ///      3. Updates core state and emits `Proposed` event
    /// NOTE: This function can only be called once per block to prevent spams that can fill the
    /// ring buffer.
    function propose(bytes calldata _lookahead, bytes calldata _data) external nonReentrant {
        unchecked {
            ProposeInput memory input = LibCodec.decodeProposeInput(_data);
            _validateProposeInput(input);

            uint48 nextProposalId = _coreState.nextProposalId;
            uint48 lastProposalBlockId = _coreState.lastProposalBlockId;
            uint48 lastFinalizedProposalId = _coreState.lastFinalizedProposalId;
            require(nextProposalId > 0, ActivationRequired());

            Proposal memory proposal = _buildProposal(
                input, _lookahead, nextProposalId, lastProposalBlockId, lastFinalizedProposalId
            );

            _coreState.nextProposalId = nextProposalId + 1;
            _coreState.lastProposalBlockId = uint48(block.number);
            _setProposalHash(proposal.id, LibHashOptimized.hashProposal(proposal));

            _emitProposedEvent(proposal);
        }
    }

    /// @inheritdoc IInbox
    /// @dev When the prover whitelist is enabled and contains at least one prover, only
    ///      whitelisted provers may prove.
    /// @dev The proof covers a contiguous range of proposals. The input contains an array of
    ///      Transition structs, each with the proposal metadata and end block hash. The proof
    ///      range can start at or before the last finalized proposal to handle race conditions
    ///      where proposals get finalized between proof generation and submission.
    ///
    /// Example: Proving proposals 3-7 when lastFinalizedProposalId=4
    ///
    ///       lastFinalizedProposalId                nextProposalId
    ///                             ┆                             ┆
    ///                             ▼                             ▼
    ///     0     1     2     3     4     5     6     7     8     9
    ///     ■─────■─────■─────■─────■─────□─────□─────□─────□─────
    ///                       ▲           ▲                 ▲
    ///                       ┆<-offset-> ┆                 ┆
    ///                       ┆                             ┆
    ///                       ┆<-    input.transitions[]   ->┆
    ///         firstProposalId                             lastProposalId
    ///
    /// Key validation rules:
    /// 1. firstProposalId <= lastFinalizedProposalId + 1 (can overlap with finalized range)
    /// 2. lastProposalId < nextProposalId (cannot prove unproposed blocks)
    /// 3. lastProposalId >= lastFinalizedProposalId + 1 (must advance at least one proposal)
    /// 4. The block hash must link to the lastFinalizedBlockHash
    ///
    /// @param _data Encoded ProveInput struct
    /// @param _proof Validity proof for the batch of proposals
    function prove(bytes calldata _data, bytes calldata _proof) external nonReentrant {
        unchecked {

            CoreState memory state = _coreState;
            ProveInput memory input = LibCodec.decodeProveInput(_data);

            // -------------------------------------------------------------------------------
            // 1. Validate batch bounds and calculate offset of the first unfinalized proposal
            // -------------------------------------------------------------------------------
            Commitment memory commitment = input.commitment;

            // `offset` is the index of the next-to-finalize proposal in the transitions array.
            (uint256 numProposals, uint256 lastProposalId, uint48 offset) =
                _validateCommitment(state, commitment);

            uint256 proposalAge = block.timestamp - commitment.transitions[offset].timestamp;
            bool isWhitelistEnabled = _checkProver(msg.sender);

            // ---------------------------------------------------------
            // 2. Verify parent block-hash continuity and last proposal hash
            // ---------------------------------------------------------
            // The parent block hash must match the stored lastFinalizedBlockHash.
            bytes32 expectedParentHash = offset == 0
                ? commitment.firstProposalParentBlockHash
                : commitment.transitions[offset - 1].blockHash;
            require(state.lastFinalizedBlockHash == expectedParentHash, ParentBlockHashMismatch());

            require(
                commitment.lastProposalHash == getProposalHash(lastProposalId),
                LastProposalHashMismatch()
            );

            // ---------------------------------------------------------
            // 3. Process bond instruction
            // ---------------------------------------------------------
            // Bond transfers only apply when whitelist is not enabled.
            if (!isWhitelistEnabled) {
                _processLivenessBond(commitment, offset);
            }

            // -----------------------------------------------------------------------------
            // 4. Sync checkpoint
            // -----------------------------------------------------------------------------
            _signalService.saveCheckpoint(
                ICheckpointStore.Checkpoint({
                    blockNumber: commitment.endBlockNumber,
                    stateRoot: commitment.endStateRoot,
                    blockHash: commitment.transitions[numProposals - 1].blockHash
                })
            );
            state.lastCheckpointTimestamp = uint48(block.timestamp);

            // ---------------------------------------------------------
            // 5. Update core state and emit event
            // ---------------------------------------------------------
            state.lastFinalizedProposalId = uint48(lastProposalId);
            state.lastFinalizedTimestamp = uint48(block.timestamp);
            state.lastFinalizedBlockHash = commitment.transitions[numProposals - 1].blockHash;

            _coreState = state;

            emit Proved(
                commitment.firstProposalId,
                commitment.firstProposalId + offset,
                uint48(lastProposalId),
                commitment.actualProver
            );

            // ---------------------------------------------------------
            // 6. Verify the proof
            // ---------------------------------------------------------
            // For multi-proposal batches (more than 1 unfinalized proposal), pass 0 to verifier.
            // Single-proposal proofs pass actual age for age-based verification logic.
            _proofVerifier.verifyProof(
                numProposals - offset == 1 ? proposalAge : 0,
                LibHashOptimized.hashCommitment(commitment),
                _proof
            );
        }
    }

    /// @inheritdoc IBondManager
    function deposit(uint64 _amount) external nonReentrant {
        _bondStorage.deposit(_bondToken, msg.sender, msg.sender, _amount, true);
    }

    /// @inheritdoc IBondManager
    function depositTo(address _recipient, uint64 _amount) external nonReentrant {
        _bondStorage.deposit(_bondToken, msg.sender, _recipient, _amount, false);
    }

    /// @inheritdoc IBondManager
    function withdraw(address _to, uint64 _amount) external nonReentrant {
        _bondStorage.withdraw(_bondToken, msg.sender, _to, _amount, _minBond, _withdrawalDelay);
    }

    /// @inheritdoc IBondManager
    function requestWithdrawal() external nonReentrant {
        _bondStorage.requestWithdrawal(msg.sender, _withdrawalDelay);
    }

    /// @inheritdoc IBondManager
    function cancelWithdrawal() external nonReentrant {
        _bondStorage.cancelWithdrawal(msg.sender);
    }

    /// @inheritdoc IForcedInclusionStore
    /// @dev This function will revert if called before the first non-activation proposal is
    /// submitted to make sure blocks have been produced already and the derivation can use the
    /// parent's block timestamp.
    function saveForcedInclusion(LibBlobs.BlobReference memory _blobReference) external payable {
        bytes32 proposalHash = _proposalHashes[1];
        require(proposalHash != bytes32(0), IncorrectProposalCount());

        uint256 refund = _forcedInclusionStorage.saveForcedInclusion(
            _forcedInclusionFeeInGwei, _forcedInclusionFeeDoubleThreshold, _blobReference
        );

        // Refund excess payment to the sender
        if (refund > 0) {
            msg.sender.sendEtherAndVerify(refund);
        }
    }

    /// @inheritdoc ICodec
    function encodeProposeInput(IInbox.ProposeInput calldata _input)
        external
        pure
        returns (bytes memory encoded_)
    {
        return LibCodec.encodeProposeInput(_input);
    }

    /// @inheritdoc ICodec
    function decodeProposeInput(bytes calldata _data)
        external
        pure
        returns (IInbox.ProposeInput memory input_)
    {
        return LibCodec.decodeProposeInput(_data);
    }

    /// @inheritdoc ICodec
    function encodeProveInput(IInbox.ProveInput calldata _input)
        external
        pure
        returns (bytes memory encoded_)
    {
        return LibCodec.encodeProveInput(_input);
    }

    /// @inheritdoc ICodec
    function decodeProveInput(bytes calldata _data)
        external
        pure
        returns (IInbox.ProveInput memory input_)
    {
        return LibCodec.decodeProveInput(_data);
    }

    /// @inheritdoc ICodec
    function hashProposal(IInbox.Proposal calldata _proposal) external pure returns (bytes32) {
        return LibHashOptimized.hashProposal(_proposal);
    }

    /// @inheritdoc ICodec
    function hashCommitment(IInbox.Commitment calldata _commitment)
        external
        pure
        returns (bytes32)
    {
        return LibHashOptimized.hashCommitment(_commitment);
    }

    // ---------------------------------------------------------------
    // External and Public View Functions
    // ---------------------------------------------------------------
    /// @inheritdoc IBondManager
    function getBond(address _address) external view returns (Bond memory bond_) {
        return _bondStorage.getBond(_address);
    }

    /// @inheritdoc IForcedInclusionStore
    function getCurrentForcedInclusionFee() external view returns (uint64 feeInGwei_) {
        return _forcedInclusionStorage.getCurrentForcedInclusionFee(
            _forcedInclusionFeeInGwei, _forcedInclusionFeeDoubleThreshold
        );
    }

    /// @inheritdoc IForcedInclusionStore
    function getForcedInclusions(
        uint48 _start,
        uint48 _maxCount
    )
        external
        view
        returns (IForcedInclusionStore.ForcedInclusion[] memory inclusions_)
    {
        return _forcedInclusionStorage.getForcedInclusions(_start, _maxCount);
    }

    /// @inheritdoc IForcedInclusionStore
    function getForcedInclusionState() external view returns (uint48 head_, uint48 tail_) {
        return _forcedInclusionStorage.getForcedInclusionState();
    }

    /// @inheritdoc IInbox
    function getConfig() external view returns (Config memory config_) {
        config_ = Config({
            proofVerifier: address(_proofVerifier),
            proposerChecker: address(_proposerChecker),
            proverWhitelist: address(_proverWhitelist),
            signalService: address(_signalService),
            bondToken: address(_bondToken),
            minBond: _minBond,
            livenessBond: _livenessBond,
            withdrawalDelay: _withdrawalDelay,
            provingWindow: _provingWindow,
            permissionlessProvingDelay: _permissionlessProvingDelay,
            maxProofSubmissionDelay: _maxProofSubmissionDelay,
            ringBufferSize: _ringBufferSize,
            basefeeSharingPctg: _basefeeSharingPctg,
            forcedInclusionDelay: _forcedInclusionDelay,
            forcedInclusionFeeInGwei: _forcedInclusionFeeInGwei,
            forcedInclusionFeeDoubleThreshold: _forcedInclusionFeeDoubleThreshold,
            permissionlessInclusionMultiplier: _permissionlessInclusionMultiplier
        });
    }

    /// @inheritdoc IInbox
    function getCoreState() external view returns (CoreState memory) {
        return _coreState;
    }

    /// @inheritdoc IInbox
    /// @dev Note that due to the ring buffer nature of the `_proposalHashes` mapping proposals
    /// may have been overwritten by a new one. You should verify that the hash matches the
    /// expected proposal.
    function getProposalHash(uint256 _proposalId) public view returns (bytes32) {
        return _proposalHashes[_proposalId % _ringBufferSize];
    }

    // ---------------------------------------------------------------
    // Private State-Changing Functions
    // ---------------------------------------------------------------

    /// @dev Builds proposal and derivation data.
    /// This function also checks:
    /// - If `msg.sender` can propose.
    /// - If `msg.sender` has sufficient bond.
    /// @param _input The propose input data.
    /// @param _lookahead Encoded data forwarded to the proposer checker (i.e. lookahead payloads).
    /// @param _nextProposalId The proposal ID to assign.
    /// @param _lastProposalBlockId The last block number where a proposal was made.
    /// @param _lastFinalizedProposalId The ID of the last finalized proposal.
    /// @return proposal_ The proposal with final endOfSubmissionWindowTimestamp set.
    function _buildProposal(
        ProposeInput memory _input,
        bytes calldata _lookahead,
        uint48 _nextProposalId,
        uint48 _lastProposalBlockId,
        uint48 _lastFinalizedProposalId
    )
        private
        returns (Proposal memory proposal_)
    {
        unchecked {
            // Enforce one propose call per Ethereum block to prevent spam attacks that could
            // deplete the ring buffer
            require(block.number > _lastProposalBlockId, CannotProposeInCurrentBlock());
            require(
                _ringBufferSize > _nextProposalId - _lastFinalizedProposalId, NotEnoughCapacity()
            );

            ConsumptionResult memory result =
                _consumeForcedInclusions(msg.sender, _input.numForcedInclusions);

            result.sources[result.sources.length - 1] =
                DerivationSource(false, LibBlobs.validateBlobReference(_input.blobReference));

            // Permissionless proposing is temporarily disabled.
            uint48 endOfSubmissionWindowTimestamp =
                _proposerChecker.checkProposer(msg.sender, _lookahead);
            if (_minBond > 0) {
                // Only if there is a minimum bond set, execute this check
                require(_bondStorage.hasSufficientBond(msg.sender, _minBond), InsufficientBond());
            }

            // Use previous block as the origin for the proposal to be able to call `blockhash`
            uint256 parentBlockNumber = block.number - 1;
            proposal_ = Proposal({
                id: _nextProposalId,
                timestamp: uint48(block.timestamp),
                endOfSubmissionWindowTimestamp: endOfSubmissionWindowTimestamp,
                proposer: msg.sender,
                parentProposalHash: getProposalHash(_nextProposalId - 1),
                originBlockNumber: uint48(parentBlockNumber),
                originBlockHash: blockhash(parentBlockNumber),
                basefeeSharingPctg: _basefeeSharingPctg,
                sources: result.sources
            });
        }
    }

    /// @dev Stores a proposal hash in the ring buffer
    /// Overwrites any existing hash at the calculated buffer slot
    function _setProposalHash(uint48 _proposalId, bytes32 _proposalHash) private {
        _proposalHashes[_proposalId % _ringBufferSize] = _proposalHash;
    }

    /// @dev Consumes forced inclusions from the queue and returns result with extra slot for normal
    /// source
    /// @param _feeRecipient Address to receive accumulated fees
    /// @param _numForcedInclusionsRequested Maximum number of forced inclusions to consume
    /// @return result_ ConsumptionResult with sources array (size: processed + 1, last slot empty)
    /// and whether permissionless proposals are allowed
    function _consumeForcedInclusions(
        address _feeRecipient,
        uint256 _numForcedInclusionsRequested
    )
        private
        returns (ConsumptionResult memory result_)
    {
        unchecked {
            LibForcedInclusion.Storage storage $ = _forcedInclusionStorage;

            // Load storage once
            (uint48 head, uint48 tail) = ($.head, $.tail);

            uint256 available = tail - head;
            uint256 dueToProcess;
            uint256 maxToInspect = available.min(MAX_FORCED_INCLUSIONS_PER_PROPOSAL);
            for (uint256 i; i < maxToInspect; ++i) {
                IForcedInclusionStore.ForcedInclusion storage inclusion = $.queue[head + i];
                uint256 timestamp = inclusion.blobSlice.timestamp;
                if (timestamp == 0 || block.timestamp < timestamp + uint256(_forcedInclusionDelay))
                {
                    break;
                }
                ++dueToProcess;
            }
            require(
                _numForcedInclusionsRequested >= dueToProcess, UnprocessedForcedInclusionIsDue()
            );

            uint256 toProcess = _numForcedInclusionsRequested.min(available)
                .min(MAX_FORCED_INCLUSIONS_PER_PROPOSAL);

            result_.sources = new DerivationSource[](toProcess + 1);

            (, head) = _dequeueAndProcessForcedInclusions(
                $, _feeRecipient, result_.sources, head, toProcess
            );
        }
    }

    /// @dev Dequeues and processes forced inclusions from the queue without checking if they exist
    /// @param $ Storage reference
    /// @param _feeRecipient Address to receive fees
    /// @param _sources Array to populate with derivation sources
    /// @param _head Current queue head position
    /// @param _toProcess Number of inclusions to process
    /// @return oldestTimestamp_ Oldest timestamp from processed inclusions.
    /// `type(uint48).max` if no inclusions were processed
    /// @return head_ Updated head position
    function _dequeueAndProcessForcedInclusions(
        LibForcedInclusion.Storage storage $,
        address _feeRecipient,
        DerivationSource[] memory _sources,
        uint48 _head,
        uint256 _toProcess
    )
        private
        returns (uint48 oldestTimestamp_, uint48 head_)
    {
        unchecked {
            if (_toProcess == 0) {
                return (type(uint48).max, _head);
            }

            // Process inclusions and accumulate fees
            uint256 totalFees;
            for (uint256 i; i < _toProcess; ++i) {
                IForcedInclusionStore.ForcedInclusion storage inclusion = $.queue[_head + i];
                _sources[i] = IInbox.DerivationSource(true, inclusion.blobSlice);
                totalFees += inclusion.feeInGwei;
            }

            // Transfer accumulated fees
            _feeRecipient.sendEtherAndVerify(totalFees * 1 gwei);

            // Oldest timestamp is the timestamp of the first inclusion
            oldestTimestamp_ = uint48(_sources[0].blobSlice.timestamp);

            // Update queue position
            head_ = _head + uint48(_toProcess);

            // Write to storage once
            $.head = head_;
        }
    }

    /// @dev Calculates and processes liveness bond settlement if applicable.
    /// @dev Settlement rules:
    ///      - On-time (within provingWindow + sequential grace): No bond changes.
    ///      - Late: Liveness bond slash with 50% credited to the actual prover and 50% burned.
    /// @param _commitment The commitment data.
    /// @param _offset The offset to the first unfinalized proposal.
    function _processLivenessBond(Commitment memory _commitment, uint48 _offset) private {
        unchecked {
            uint256 livenessWindowDeadline = (_commitment.transitions[_offset].timestamp
                    + _provingWindow)
            .max(_coreState.lastFinalizedTimestamp + _maxProofSubmissionDelay);

            // On-time proof - no bond transfer needed.
            if (block.timestamp <= livenessWindowDeadline) {
                return;
            }

            _bondStorage.settleLivenessBond(
                _commitment.transitions[_offset].proposer, _commitment.actualProver, _livenessBond
            );
        }
    }

    /// @dev Emits the Proposed event
    function _emitProposedEvent(Proposal memory _proposal) private {
        emit Proposed(
            _proposal.id,
            _proposal.proposer,
            _proposal.parentProposalHash,
            _proposal.endOfSubmissionWindowTimestamp,
            _proposal.basefeeSharingPctg,
            _proposal.sources
        );
    }

    // ---------------------------------------------------------------
    // Private View/Pure Functions
    // ---------------------------------------------------------------

    /// @dev Validates propose function inputs.
    /// @param _input The ProposeInput to validate
    function _validateProposeInput(ProposeInput memory _input) private view {
        require(_input.deadline == 0 || block.timestamp <= _input.deadline, DeadlineExceeded());
    }

    /// @dev Checks if the caller is an authorized prover.
    /// @param _addr The address of the caller to check.
    /// @return whitelistEnabled_ True if whitelist is enabled (proverCount > 0), false otherwise.
    function _checkProver(address _addr) private view returns (bool whitelistEnabled_) {
        if (address(_proverWhitelist) == address(0)) return false;

        (bool isWhitelisted, uint256 proverCount) = _proverWhitelist.isProverWhitelisted(_addr);
        if (proverCount == 0) return false;

        require(isWhitelisted, ProverNotWhitelisted());
        return true;
    }

    /// @dev Validates the batch bounds in the Commitment and calculates the offset
    ///      to the first unfinalized proposal.
    /// @param _state The core state.
    /// @param _commitment The commitment data.
    /// @return numProposals_ The number of proposals in the batch.
    /// @return lastProposalId_ The ID of the last proposal in the batch.
    /// @return offset_ The offset to the first unfinalized proposal.
    function _validateCommitment(
        CoreState memory _state,
        Commitment memory _commitment
    )
        private
        pure
        returns (uint256 numProposals_, uint256 lastProposalId_, uint48 offset_)
    {
        unchecked {
            uint256 firstUnfinalizedId = _state.lastFinalizedProposalId + 1;

            numProposals_ = _commitment.transitions.length;
            require(numProposals_ > 0, EmptyBatch());
            require(_commitment.firstProposalId <= firstUnfinalizedId, FirstProposalIdTooLarge());

            lastProposalId_ = _commitment.firstProposalId + numProposals_ - 1;
            require(lastProposalId_ < _state.nextProposalId, LastProposalIdTooLarge());
            require(lastProposalId_ >= firstUnfinalizedId, LastProposalAlreadyFinalized());

            // Calculate offset to first unfinalized proposal.
            // Some proposals in _commitment.transitions[] may already be finalized.
            // The offset points to the first proposal that will be finalized.
            offset_ = uint48(firstUnfinalizedId - _commitment.firstProposalId);
        }
    }

    // ---------------------------------------------------------------
    // Errors
    // ---------------------------------------------------------------
    error ActivationRequired();
    error CannotProposeInCurrentBlock();
    error DeadlineExceeded();
    error EmptyBatch();
    error FirstProposalIdTooLarge();
    error IncorrectProposalCount();
    error InsufficientBond();
    error InvalidRecoveryState();
    error LastProposalAlreadyFinalized();
    error LastProposalHashMismatch();
    error LastProposalIdTooLarge();
    error NotEnoughCapacity();
    error ParentBlockHashMismatch();
    error ProverNotWhitelisted();
    error UnprocessedForcedInclusionIsDue();
}
