Creating a Verifier Spec

A Verifier Spec defines the complete specification for a type of verification — "what this type of transaction needs to verify, how to construct signatures, how to verify them." Immutable after deployment; multiple Verifier instances share the same Spec to form a competitive market.

This document uses XQuoteVerifierSpec as a reference implementation.


Relationship Between Spec and Verifier Instance

XQuoteVerifierSpec (1)
  ├── XQuoteVerifier A (Instance 1, operated by Team A)
  ├── XQuoteVerifier B (Instance 2, operated by Team B)
  └── XQuoteVerifier C (Instance 3, auto-operated by an Agent)
  • Spec defines the specification — Parameter format, signature structure, check() verification logic
  • Instances provide the service — Each independently operates off-chain services, sets own prices, competes for business
  • Deal Contract references SpecgetRequiredSpecs() returns the Spec address required for each verification slot in the deal flow; Traders use this to search for matching Verifier instances

Step 1: Implement Metadata

contract XQuoteVerifierSpec is IVerifierSpec {
    function name() external pure override returns (string memory) {
        return "XQuoteVerifierSpec";
    }

    function version() external pure override returns (string memory) {
        return "1.0";
    }

    function description() external pure override returns (string memory) {
        return
            "Verifies X/Twitter quote tweets. "
            "check params: string tweet_id, string quoter_username. "
            "specParams (for verification): abi.encode(string tweet_id, string quoter_username, string quote_tweet_id).";
    }
}

Importance of description(): It's not just human-readable documentation — off-chain services need to decode on-chain parameters based on the specParams format declared in the description. It must be precise down to parameter types and order.

Note: The IVerifierSpec base interface only defines three methods: name(), version(), description(). check() is not in the base interface because each Spec's signature parameters differ — you need to define the check() signature yourself. Deal Contracts cast the Spec address to your specific type when calling it.


Step 2: Define the EIP-712 Signature Structure

bytes32 public constant VERIFY_TYPEHASH = keccak256(
    "Verify(string tweetId,string quoterUsername,uint256 fee,uint256 deadline)"
);

This typehash defines the content structure for the Verifier's quote signature. The Verifier signs according to this structure during request_sign, and the contract verifies the signature according to this structure during createDeal.

Design Considerations

The signature includes: business parameters (solidifying verification content), fee (solidifying the price), deadline (solidifying the validity period). It does not include dealIndex (the deal hasn't been created at signing time) or partyA/partyB (the Verifier only cares about verification content).

EIP-712 is used instead of plain signatures because structured signatures allow wallets to display semantic signing content rather than a hex string.


Step 3: Implement check()

function check(
    address verifierInstance,
    string calldata tweet_id,
    string calldata quoter_username,
    uint256 fee,
    uint256 deadline,
    bytes calldata sig
) external view returns (bool) {
    if (block.timestamp > deadline) revert SignatureExpired();

    bytes32 structHash = keccak256(abi.encode(
        VERIFY_TYPEHASH,
        keccak256(bytes(tweet_id)),
        keccak256(bytes(quoter_username)),
        fee,
        deadline
    ));

    bytes32 domainSeparator = IVerifier(verifierInstance).DOMAIN_SEPARATOR();
    address owner_ = IVerifier(verifierInstance).owner();

    bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
    address signer = _recoverSigner(digest, sig);
    return signer == owner_;
}

Key Design Points

  • Call timingcheck() is called by the Deal Contract during createDeal to verify the signature was indeed issued by the specified Verifier's owner
  • DOMAIN_SEPARATOR — Read from verifierInstance; each instance has an independent domain to prevent cross-instance signature replay
  • Owner verification — signer must equal IVerifier(verifierInstance).owner(); owner must be an EOA (transferOwnership rejects contract addresses)
  • EIP-2 low-s value_recoverSigner enforces low-s value to prevent signature malleability attacks

Step 4: Define the specParams Format

specParams is the parameter package passed to the Verifier's off-chain service during the verification phase. Note it's not identical to check()'s signature parameters:

check() signature parameters (at createDeal stage):

tweet_id, quoter_username

specParams (at verification stage):

abi.encode(tweet_id, quoter_username, quote_tweet_id)

The additional quote_tweet_id — this field doesn't exist at createDeal time (B hasn't executed the task yet); it's written when B calls claimDone. The verifier needs it to check the specific quote tweet B posted.

Constructing specParams in the Deal Contract

// XQuoteDealContract.getVerificationParams()
specParams = abi.encode(d.tweet_id, d.quoter_username, d.quote_tweet_id);

Key point: specParams is constructed and returned by the Deal Contract's getVerificationParams(). The Verifier's off-chain service reads and decodes it from on-chain. The entire chain passes through no off-chain intermediary.


Step 5 (Optional): Create a Sample Verifier Contract Instance and Corresponding Off-Chain Verification Service

The Spec author doesn't necessarily need to operate a Verifier themselves; anyone can deploy an instance based on an existing Spec (see Deployment & Operations). The instance contract is lightweight:

contract XQuoteVerifier is VerifierBase {
    address public immutable SPEC;

    constructor(address specAddress) VerifierBase("XQuoteVerifier", "1") {
        require(specAddress != address(0), "spec cannot be zero");
        SPEC = specAddress;
    }

    function description() external pure override(VerifierBase) returns (string memory) {
        return "X Quote Tweet Verification: Verifies that a user has quoted a specific tweet on X.";
    }

    function spec() external view override(VerifierBase) returns (address) {
        return SPEC;
    }
}

Full VerifierBase API in Developer Reference.

The focus for Verifiers is the off-chain verification service implementation; see Deployment & Operations.


Reference Implementation

  • Spec contract: contracts/verifier-specs/XQuoteVerifierSpec.sol
  • Verifier instance: examples/x-quote-verifier/XQuoteVerifier.sol