// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

/**
 * @title AereValidatorManifest — public commitment registry for verification nodes
 * @notice The 3 → 7 → 21 validator transition requires that anyone, anywhere,
 *         can verify which binary a validator is running and which operator
 *         is running it. This contract is the on-chain manifest of those
 *         commitments. It does NOT enroll validators (consensus enrollment is
 *         off-chain via Foundation Ledger + QBFT extraData); it makes the
 *         operator's claim machine-verifiable.
 *
 *         For each validator a node operator registers:
 *           - validatorAddress      QBFT signer (the address Besu uses to sign blocks)
 *           - operatorLabel         human-readable name (e.g. "P2P Validator", "Figment")
 *           - operatorRegion        e.g. "EU-Central / DE / example-dc-1"
 *           - binaryDigest          SHA256 of the Besu image they're running
 *           - manifestUri           where the binary digest's signed manifest lives
 *                                   (typically the published .well-known/besu-image-digest)
 *           - declaredAt            unix seconds at which the operator made the claim
 *
 *         A new commitment FOR THE SAME validatorAddress overwrites the old one,
 *         keyed by an incrementing serial — history is queryable via events.
 *         There is NO admin override; only the validator's own QBFT signer key
 *         can update its commitment. That ties the binary commitment to consensus
 *         participation: if the signer is compromised, so is the manifest, and
 *         everyone observes simultaneously.
 *
 *         GOTCHA: this contract does not, and cannot, verify that the validator
 *         IS actually running the claimed binary. That verification is by
 *         third-party probing (block signature analysis, P2P handshake
 *         fingerprint) + the publicly reproducible build (`verify-besu-image.sh`).
 *         The contract gives a single canonical place to ATTEST so audits don't
 *         drift across mediums.
 *
 *         WHY no admin: the manifest belongs to the operator, not the Foundation.
 *         The Foundation publishes its own commitment same as every other
 *         operator. "The chain nobody owns" is incompatible with a Foundation
 *         that can rewrite operator manifests.
 */
contract AereValidatorManifest {

    struct Commitment {
        bytes32 binaryDigest;     // SHA256 (or other) of the Besu container image
        string  manifestUri;      // ipfs:// or https:// link to the signed manifest
        string  operatorLabel;
        string  operatorRegion;
        uint64  declaredAt;
        uint256 serial;           // monotonic per validator
    }

    /// @notice validator signer → most recent commitment.
    mapping(address => Commitment) public latest;

    /// @notice validator signer → next serial. Starts at 1.
    mapping(address => uint256) public nextSerial;

    /* ================================ events ================================ */

    event CommitmentDeclared(
        address indexed validator,
        uint256 indexed serial,
        bytes32 indexed binaryDigest,
        string  manifestUri,
        string  operatorLabel,
        string  operatorRegion,
        uint64  declaredAt
    );

    /* ================================ errors ================================ */

    error EmptyManifestUri();
    error EmptyOperatorLabel();
    error ZeroDigest();

    /* =============================== writes =============================== */

    /// @notice The validator (msg.sender == signer key) declares which binary
    ///         it is running + who is running it.
    function declare(
        bytes32 binaryDigest,
        string calldata manifestUri,
        string calldata operatorLabel,
        string calldata operatorRegion
    ) external {
        if (binaryDigest == bytes32(0)) revert ZeroDigest();
        if (bytes(manifestUri).length == 0) revert EmptyManifestUri();
        if (bytes(operatorLabel).length == 0) revert EmptyOperatorLabel();

        uint256 s = ++nextSerial[msg.sender];
        latest[msg.sender] = Commitment({
            binaryDigest:   binaryDigest,
            manifestUri:    manifestUri,
            operatorLabel:  operatorLabel,
            operatorRegion: operatorRegion,
            declaredAt:     uint64(block.timestamp),
            serial:         s
        });

        emit CommitmentDeclared(
            msg.sender, s, binaryDigest, manifestUri, operatorLabel, operatorRegion, uint64(block.timestamp)
        );
    }

    /* ================================ views ================================ */

    /// @notice True when the validator has declared at least one commitment.
    function isDeclared(address validator) external view returns (bool) {
        return latest[validator].declaredAt != 0;
    }

    /// @notice Pretty getter for indexers.
    function commitmentOf(address validator) external view returns (
        bytes32 binaryDigest,
        string memory manifestUri,
        string memory operatorLabel,
        string memory operatorRegion,
        uint64 declaredAt,
        uint256 serial
    ) {
        Commitment storage c = latest[validator];
        return (c.binaryDigest, c.manifestUri, c.operatorLabel, c.operatorRegion, c.declaredAt, c.serial);
    }
}
