Skip to content

Create a Schema Hook

In this step, you extend the verifier contract generated by the ZK library and implement the ISPHook interface. The contract generated in the previous step looks something like this:

pragma solidity >=0.7.0 <0.9.0;

contract Groth16Verifier {
    ...

    function verifyProof(uint[2] calldata _pA, uint[2][2] calldata _pB, uint[2] calldata _pC, uint[32] calldata _pubSignals) public view returns (bool) {
        ...
     }
 }

Info

In the generated verifier, change all instances of calldata to memory.

The generated verifier should now look like this:

pragma solidity >=0.7.0 <0.9.0;

contract Groth16Verifier {
    ...

    function verifyProof(uint[2] memory _pA, uint[2][2] memory _pB, uint[2] memory _pC, uint[32] memory _pubSignals) public view returns (bool) {
        ...
     }
 }

To set up your schema hook, create another file with the following contents:

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

import { Groth16Verifier } from "./Verifier.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { ISPHook } from "@ethsign/sign-protocol-evm/src/interfaces/ISPHook.sol";

contract SHA256PreimageVerifier is Groth16Verifier, Ownable {
    address public spInstance;

    constructor() Ownable(_msgSender()) { }

    function setSPInstance(address instance) external onlyOwner {
        spInstance = instance;
    }
}

// @dev This contract implements the actual schema hook.
contract ZKHook is ISPHook, SHA256PreimageVerifier {
    error Unsupported();
    error ZKVerificationFailed();

    function didReceiveAttestation(
        address, // attester
        uint64, // schemaId
        uint64, // attestationId
        bytes calldata extraData
    )
        external
        payable
    {
        if (_msgSender() != spInstance) revert Unsupported();
        (uint256[2] memory _pA, uint256[2][2] memory _pB, uint256[2] memory _pC, uint256[32] memory _pubSignals) =
            abi.decode(extraData, (uint256[2], uint256[2][2], uint256[2], uint256[32]));
        // If the SHA256 preimage proof verification fails, revert.
        if (!verifyProof(_pA, _pB, _pC, _pubSignals)) revert ZKVerificationFailed();
    }

    function didReceiveAttestation(
        address, // attester
        uint64, // schemaId
        uint64, // attestationId
        IERC20, // resolverFeeERC20Token
        uint256, // resolverFeeERC20Amount
        bytes calldata // extraData
    )
        external
        pure
    {
        revert Unsupported();
    }

    function didReceiveRevocation(
        address, // attester
        uint64, // schemaId
        uint64, // attestationId
        bytes calldata // extraData
    )
        external
        payable
    {
        revert Unsupported();
    }

    function didReceiveRevocation(
        address, // attester
        uint64, // schemaId
        uint64, // attestationId
        IERC20, // resolverFeeERC20Token
        uint256, // resolverFeeERC20Amount
        bytes calldata // extraData
    )
        external
        pure
    {
        revert Unsupported();
    }
}

The file imports the generated Groth16Verifier contract, OpenZeppelin packages, and the ISPHook contract.

SHA256PreimageVerifier sets the Sign Protocol instance's contract address and adds ownable functionality.

ZKHook contains the schema hook logic. Because ZK proofs can be large, pass the proof data in extraData. Sign Protocol's contract receives this field when you create or revoke an attestation and forwards it to schema hooks without storing it onchain. The hook decodes extraData and passes it to the verifier's verifyProof() function. The call reverts if verification fails; otherwise, the attestation is created.

Finishing Up

Deploy your ZK schema hook contract to your desired network. Then create a new schema on the same network and set its hook address to your deployed schema hook contract.