{
  "language": "Solidity",
  "sources": {
    "contracts/SuperWalletTrustRegistry.sol": {
      "content": "// SPDX-License-Identifier: MIT\npragma solidity 0.8.24;\n\n/**\n * @title SuperWalletTrustRegistry\n * @notice Public, append-mostly record that lets anyone check a Super Wallet credential without\n *         trusting the Super Wallet servers: who the issuer is (and which key it signs with), that a\n *         credential was anchored and has not changed since (its canonical digest), whether it has\n *         been revoked, which zero-knowledge commitments are bound to it, and which verifier contract\n *         checks each kind of proof.\n * @dev    Nothing personal is ever stored here. The only values are hashes (issuer ids, key\n *         fingerprints, credential ids, credential digests, verification-key hashes), ZK commitments\n *         (field elements that reveal nothing without their salt), timestamps, small reason codes and\n *         public DIDs / URIs of issuers and circuits.\n *\n *         Roles:\n *         - owner (governance): registers and suspends issuers, manages each issuer's operators,\n *           registers circuits. Two-step ownership transfer so a mistyped address cannot take control.\n *         - operator of an issuer: an address allowed to anchor, revoke and commit for that issuer\n *           (for example the issuer's backend wallet). An operator acts only for issuers it was added to.\n *\n *         Identifiers (conventions the off-chain tooling follows; the contract treats them as opaque):\n *         - issuerId     = keccak256(utf8(issuer DID))\n *         - credentialId = keccak256(utf8(vc.id))\n *         - digest       = sha256(JCS(vc without its proof))  (RFC 8785 JSON Canonicalization Scheme)\n *         - circuitId    = keccak256(utf8(circuit name)), e.g. keccak256(\"tier-threshold-v1\")\n */\ncontract SuperWalletTrustRegistry {\n    // ---------------------------------------------------------------------------------------------\n    // Types\n    // ---------------------------------------------------------------------------------------------\n\n    /// @notice Issuer status values returned by {issuer}.\n    uint8 public constant ISSUER_NONE = 0;\n    uint8 public constant ISSUER_ACTIVE = 1;\n    uint8 public constant ISSUER_SUSPENDED = 2;\n\n    /// @notice Largest number of credentials {anchorBatch} accepts in one call.\n    uint256 public constant MAX_BATCH = 100;\n\n    struct Issuer {\n        bytes32 keyFingerprint;\n        uint8 status;\n        uint64 since;\n        string did;\n    }\n\n    struct Credential {\n        bytes32 digest;\n        bytes32 issuerId;\n        uint64 anchoredAt;\n        uint64 revokedAt;\n        uint8 reason;\n    }\n\n    struct Circuit {\n        address verifier;\n        uint16 contributions;\n        bytes32 vkHash;\n        string uri;\n    }\n\n    // ---------------------------------------------------------------------------------------------\n    // Storage\n    // ---------------------------------------------------------------------------------------------\n\n    /// @notice Current governance address.\n    address public owner;\n\n    /// @notice Address that may accept ownership; zero when no transfer is pending.\n    address public pendingOwner;\n\n    mapping(bytes32 => Issuer) private _issuers;\n    mapping(bytes32 => mapping(address => bool)) private _operators;\n    mapping(bytes32 => Credential) private _credentials;\n    mapping(bytes32 => mapping(bytes32 => uint256)) private _commitments;\n    mapping(bytes32 => Circuit) private _circuits;\n\n    // ---------------------------------------------------------------------------------------------\n    // Events\n    // ---------------------------------------------------------------------------------------------\n\n    event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner);\n    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);\n    event IssuerRegistered(bytes32 indexed issuerId, bytes32 keyFingerprint, string did);\n    event IssuerSuspended(bytes32 indexed issuerId);\n    event OperatorAdded(bytes32 indexed issuerId, address indexed operator);\n    event OperatorRemoved(bytes32 indexed issuerId, address indexed operator);\n    event Anchored(bytes32 indexed credentialId, bytes32 indexed issuerId, bytes32 digest);\n    event Revoked(bytes32 indexed credentialId, bytes32 indexed issuerId, uint8 reason);\n    event Committed(bytes32 indexed credentialId, bytes32 indexed circuitId, uint256 commitment);\n    event CircuitRegistered(\n        bytes32 indexed circuitId,\n        address verifier,\n        bytes32 vkHash,\n        uint16 ceremonyContributions,\n        string uri\n    );\n\n    // ---------------------------------------------------------------------------------------------\n    // Errors\n    // ---------------------------------------------------------------------------------------------\n\n    error NotOwner();\n    error NotPendingOwner();\n    error ZeroValue();\n    error IssuerNotActive(bytes32 issuerId);\n    error IssuerUnknown(bytes32 issuerId);\n    error NotIssuerOperator(bytes32 issuerId, address caller);\n    error OperatorAlreadyAdded(bytes32 issuerId, address operator);\n    error OperatorNotFound(bytes32 issuerId, address operator);\n    error AlreadyAnchored(bytes32 credentialId);\n    error NotAnchored(bytes32 credentialId);\n    error AlreadyRevoked(bytes32 credentialId);\n    error AlreadyCommitted(bytes32 credentialId, bytes32 circuitId);\n    error CircuitUnknown(bytes32 circuitId);\n    error CircuitAlreadyRegistered(bytes32 circuitId);\n    error BatchLengthMismatch();\n    error BatchSize(uint256 size);\n\n    // ---------------------------------------------------------------------------------------------\n    // Modifiers\n    // ---------------------------------------------------------------------------------------------\n\n    modifier onlyGovernance() {\n        if (msg.sender != owner) revert NotOwner();\n        _;\n    }\n\n    modifier onlyActiveIssuerOperator(bytes32 issuerId) {\n        _requireActiveOperator(issuerId);\n        _;\n    }\n\n    // ---------------------------------------------------------------------------------------------\n    // Construction and governance\n    // ---------------------------------------------------------------------------------------------\n\n    /// @notice Deploys the registry with the deployer as governance.\n    constructor() {\n        owner = msg.sender;\n        emit OwnershipTransferred(address(0), msg.sender);\n    }\n\n    /**\n     * @notice Starts a two-step ownership transfer. The new owner must call {acceptOwnership}.\n     * @dev Passing the zero address cancels a pending transfer.\n     * @param newOwner Address that will be allowed to accept ownership.\n     */\n    function transferOwnership(address newOwner) external onlyGovernance {\n        pendingOwner = newOwner;\n        emit OwnershipTransferStarted(owner, newOwner);\n    }\n\n    /// @notice Completes a transfer started by {transferOwnership}; callable only by the pending owner.\n    function acceptOwnership() external {\n        if (msg.sender != pendingOwner) revert NotPendingOwner();\n        address previous = owner;\n        owner = msg.sender;\n        pendingOwner = address(0);\n        emit OwnershipTransferred(previous, msg.sender);\n    }\n\n    // ---------------------------------------------------------------------------------------------\n    // Issuer registry\n    // ---------------------------------------------------------------------------------------------\n\n    /**\n     * @notice Registers an issuer, or re-registers one to rotate its key or lift a suspension.\n     * @dev Every registration sets status ACTIVE and `since` to the current block time, so `since`\n     *      always answers \"since when does this key and status hold\".\n     * @param issuerId keccak256(utf8(issuer DID)).\n     * @param keyFingerprint Hash of the issuer's current assertion public key\n     *        (Super Wallet uses the RFC 7638 SHA-256 JWK thumbprint).\n     * @param did The issuer DID, stored so a reader can match it to the credential's `issuer`.\n     */\n    function registerIssuer(bytes32 issuerId, bytes32 keyFingerprint, string calldata did)\n        external\n        onlyGovernance\n    {\n        if (issuerId == bytes32(0) || keyFingerprint == bytes32(0) || bytes(did).length == 0) {\n            revert ZeroValue();\n        }\n        Issuer storage i = _issuers[issuerId];\n        i.keyFingerprint = keyFingerprint;\n        i.status = ISSUER_ACTIVE;\n        i.since = uint64(block.timestamp);\n        i.did = did;\n        emit IssuerRegistered(issuerId, keyFingerprint, did);\n    }\n\n    /**\n     * @notice Suspends an active issuer: its operators can no longer anchor or commit.\n     * @dev Revocation of credentials it already anchored stays possible, so a suspended issuer can\n     *      still withdraw what it issued.\n     * @param issuerId The issuer to suspend.\n     */\n    function suspendIssuer(bytes32 issuerId) external onlyGovernance {\n        Issuer storage i = _issuers[issuerId];\n        if (i.status != ISSUER_ACTIVE) revert IssuerNotActive(issuerId);\n        i.status = ISSUER_SUSPENDED;\n        i.since = uint64(block.timestamp);\n        emit IssuerSuspended(issuerId);\n    }\n\n    /**\n     * @notice Allows `operator` to anchor, revoke and commit on behalf of `issuerId`.\n     * @param issuerId A registered issuer (active or suspended).\n     * @param operator The address to authorise, typically the issuer's backend wallet.\n     */\n    function addOperator(bytes32 issuerId, address operator) external onlyGovernance {\n        if (operator == address(0)) revert ZeroValue();\n        if (_issuers[issuerId].status == ISSUER_NONE) revert IssuerUnknown(issuerId);\n        if (_operators[issuerId][operator]) revert OperatorAlreadyAdded(issuerId, operator);\n        _operators[issuerId][operator] = true;\n        emit OperatorAdded(issuerId, operator);\n    }\n\n    /**\n     * @notice Withdraws an operator's authority for one issuer.\n     * @param issuerId The issuer the operator acted for.\n     * @param operator The address to remove.\n     */\n    function removeOperator(bytes32 issuerId, address operator) external onlyGovernance {\n        if (!_operators[issuerId][operator]) revert OperatorNotFound(issuerId, operator);\n        _operators[issuerId][operator] = false;\n        emit OperatorRemoved(issuerId, operator);\n    }\n\n    /**\n     * @notice Reads an issuer record.\n     * @param issuerId keccak256(utf8(issuer DID)).\n     * @return keyFingerprint Hash of the issuer's assertion public key.\n     * @return did The issuer DID.\n     * @return status 0 unknown, 1 active, 2 suspended.\n     * @return since Block time of the last registration or suspension.\n     */\n    function issuer(bytes32 issuerId)\n        external\n        view\n        returns (bytes32 keyFingerprint, string memory did, uint8 status, uint64 since)\n    {\n        Issuer storage i = _issuers[issuerId];\n        return (i.keyFingerprint, i.did, i.status, i.since);\n    }\n\n    /**\n     * @notice Whether `operator` may act for `issuerId`.\n     * @param issuerId The issuer.\n     * @param operator The address to check.\n     * @return True when the operator was added and not removed.\n     */\n    function isOperator(bytes32 issuerId, address operator) external view returns (bool) {\n        return _operators[issuerId][operator];\n    }\n\n    // ---------------------------------------------------------------------------------------------\n    // Credential anchors and revocation\n    // ---------------------------------------------------------------------------------------------\n\n    /**\n     * @notice Anchors one credential's canonical digest under an active issuer.\n     * @dev Reverts when the id was already anchored: an anchor is written once and never replaced.\n     * @param credentialId keccak256(utf8(vc.id)).\n     * @param digest sha256(JCS(vc without proof)).\n     * @param issuerId The issuer the caller operates for.\n     */\n    function anchor(bytes32 credentialId, bytes32 digest, bytes32 issuerId)\n        external\n        onlyActiveIssuerOperator(issuerId)\n    {\n        _anchor(credentialId, digest, issuerId);\n    }\n\n    /**\n     * @notice Anchors up to {MAX_BATCH} credentials in one transaction.\n     * @dev All-or-nothing: if any id is zero or already anchored the whole batch reverts.\n     * @param ids Credential ids.\n     * @param digests Digests, same length and order as `ids`.\n     * @param issuerId The issuer the caller operates for.\n     */\n    function anchorBatch(bytes32[] calldata ids, bytes32[] calldata digests, bytes32 issuerId)\n        external\n        onlyActiveIssuerOperator(issuerId)\n    {\n        uint256 n = ids.length;\n        if (n != digests.length) revert BatchLengthMismatch();\n        if (n == 0 || n > MAX_BATCH) revert BatchSize(n);\n        for (uint256 k = 0; k < n; ) {\n            _anchor(ids[k], digests[k], issuerId);\n            unchecked {\n                ++k;\n            }\n        }\n    }\n\n    /**\n     * @notice Records that an anchored credential is revoked.\n     * @dev Only an operator of the issuer that anchored the credential may revoke it, even while that\n     *      issuer is suspended. A revocation is final.\n     * @param credentialId The credential to revoke.\n     * @param reason Reason code (see the README for the values Super Wallet uses).\n     */\n    function revoke(bytes32 credentialId, uint8 reason) external {\n        Credential storage c = _credentials[credentialId];\n        if (c.anchoredAt == 0) revert NotAnchored(credentialId);\n        if (!_operators[c.issuerId][msg.sender]) revert NotIssuerOperator(c.issuerId, msg.sender);\n        if (c.revokedAt != 0) revert AlreadyRevoked(credentialId);\n        c.revokedAt = uint64(block.timestamp);\n        c.reason = reason;\n        emit Revoked(credentialId, c.issuerId, reason);\n    }\n\n    /**\n     * @notice Reads a credential anchor.\n     * @param credentialId keccak256(utf8(vc.id)).\n     * @return digest The anchored digest, zero when never anchored.\n     * @return issuerId The issuer that anchored it.\n     * @return anchoredAt Block time of the anchor, zero when never anchored.\n     * @return revokedAt Block time of the revocation, zero when not revoked.\n     * @return reason Revocation reason code, zero when not revoked.\n     */\n    function credential(bytes32 credentialId)\n        external\n        view\n        returns (bytes32 digest, bytes32 issuerId, uint64 anchoredAt, uint64 revokedAt, uint8 reason)\n    {\n        Credential storage c = _credentials[credentialId];\n        return (c.digest, c.issuerId, c.anchoredAt, c.revokedAt, c.reason);\n    }\n\n    // ---------------------------------------------------------------------------------------------\n    // Zero-knowledge commitments\n    // ---------------------------------------------------------------------------------------------\n\n    /**\n     * @notice Binds a ZK commitment to an anchored, unrevoked credential for one circuit.\n     * @dev Only the anchoring issuer's operator, only while that issuer is active, only for a\n     *      registered circuit, and only once per (credential, circuit).\n     * @param credentialId The credential the commitment belongs to.\n     * @param circuitId The circuit whose proofs open this commitment.\n     * @param commitmentValue The commitment (for example Poseidon(value, salt)); must be non-zero.\n     */\n    function commit(bytes32 credentialId, bytes32 circuitId, uint256 commitmentValue) external {\n        Credential storage c = _credentials[credentialId];\n        if (c.anchoredAt == 0) revert NotAnchored(credentialId);\n        _requireActiveOperator(c.issuerId);\n        if (c.revokedAt != 0) revert AlreadyRevoked(credentialId);\n        if (_circuits[circuitId].verifier == address(0)) revert CircuitUnknown(circuitId);\n        if (commitmentValue == 0) revert ZeroValue();\n        if (_commitments[credentialId][circuitId] != 0) revert AlreadyCommitted(credentialId, circuitId);\n        _commitments[credentialId][circuitId] = commitmentValue;\n        emit Committed(credentialId, circuitId, commitmentValue);\n    }\n\n    /**\n     * @notice Reads the commitment bound to a credential for a circuit.\n     * @dev A commitment of a revoked credential is still returned; check {credential} for revocation.\n     * @param credentialId The credential.\n     * @param circuitId The circuit.\n     * @return The commitment, zero when none was bound.\n     */\n    function commitment(bytes32 credentialId, bytes32 circuitId) external view returns (uint256) {\n        return _commitments[credentialId][circuitId];\n    }\n\n    // ---------------------------------------------------------------------------------------------\n    // Circuit registry\n    // ---------------------------------------------------------------------------------------------\n\n    /**\n     * @notice Registers a circuit: the contract that verifies its proofs and the key it was built with.\n     * @dev A circuit id is registered once; a new key means a new circuit id, so commitments made for\n     *      one key are never silently reinterpreted under another.\n     * @param circuitId keccak256(utf8(circuit name)).\n     * @param verifier The verifier contract address.\n     * @param vkHash Hash of the verification key the verifier was generated from.\n     * @param ceremonyContributions Number of independent contributions to the key ceremony.\n     * @param uri Where the circuit source, key and ceremony transcript are published.\n     */\n    function registerCircuit(\n        bytes32 circuitId,\n        address verifier,\n        bytes32 vkHash,\n        uint16 ceremonyContributions,\n        string calldata uri\n    ) external onlyGovernance {\n        if (circuitId == bytes32(0) || verifier == address(0) || vkHash == bytes32(0)) revert ZeroValue();\n        if (_circuits[circuitId].verifier != address(0)) revert CircuitAlreadyRegistered(circuitId);\n        _circuits[circuitId] = Circuit(verifier, ceremonyContributions, vkHash, uri);\n        emit CircuitRegistered(circuitId, verifier, vkHash, ceremonyContributions, uri);\n    }\n\n    /**\n     * @notice Reads a circuit record.\n     * @param circuitId keccak256(utf8(circuit name)).\n     * @return verifier The verifier contract, zero when unknown.\n     * @return vkHash Hash of the verification key.\n     * @return contributions Number of key-ceremony contributions.\n     * @return uri Where the circuit artefacts are published.\n     */\n    function circuit(bytes32 circuitId)\n        external\n        view\n        returns (address verifier, bytes32 vkHash, uint16 contributions, string memory uri)\n    {\n        Circuit storage c = _circuits[circuitId];\n        return (c.verifier, c.vkHash, c.contributions, c.uri);\n    }\n\n    // ---------------------------------------------------------------------------------------------\n    // Internals\n    // ---------------------------------------------------------------------------------------------\n\n    function _requireActiveOperator(bytes32 issuerId) private view {\n        if (_issuers[issuerId].status != ISSUER_ACTIVE) revert IssuerNotActive(issuerId);\n        if (!_operators[issuerId][msg.sender]) revert NotIssuerOperator(issuerId, msg.sender);\n    }\n\n    function _anchor(bytes32 credentialId, bytes32 digest, bytes32 issuerId) private {\n        if (credentialId == bytes32(0) || digest == bytes32(0)) revert ZeroValue();\n        Credential storage c = _credentials[credentialId];\n        if (c.anchoredAt != 0) revert AlreadyAnchored(credentialId);\n        c.digest = digest;\n        c.issuerId = issuerId;\n        c.anchoredAt = uint64(block.timestamp);\n        emit Anchored(credentialId, issuerId, digest);\n    }\n}\n"
    }
  },
  "settings": {
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "evmVersion": "paris",
    "outputSelection": {
      "*": {
        "*": [
          "abi",
          "evm.bytecode",
          "evm.deployedBytecode",
          "evm.methodIdentifiers",
          "metadata"
        ],
        "": [
          "ast"
        ]
      }
    }
  }
}
