Skip to main content

Overview

The SimpleAttestationVerifier contract verifies attestations from off-chain verification services using on-chain trust anchors. It implements a simplified model with a single witness that signs standardized attestations after verifying zkTLS proofs. Key Features:
  • Single witness signature verification
  • EIP-712 digest validation
  • Threshold signature validation (minimum 1 signature required)
  • Pluggable architecture through IAttestationVerifier interface
  • Governance controls for updating witness
Contract Location: contracts/unifiedVerifier/SimpleAttestationVerifier.sol

Verification Flow

The complete off-chain to on-chain verification flow:

1. User Generates zkTLS Proof

The user runs the zkTLS protocol with an attestor to generate a zkTLS proof of their payment transaction.

2. Off-Chain Service Verifies and Signs

The off-chain verification service:
  • Verifies the zkTLS proof
  • Extracts and validates payment details
  • Generates a standardized attestation
  • Signs the attestation with the witness private key
  • Includes metadata about the verification

3. On-Chain Verification

The SimpleAttestationVerifier contract:
  • Checks the witness signature on the EIP-712 digest
  • Validates trust anchors to ensure off-chain verification integrity
  • Returns verification result to the calling payment verifier

Architecture

The contract implements the IAttestationVerifier interface:
interface IAttestationVerifier {
    function verify(
        bytes32 _digest,
        bytes[] calldata _sigs,
        bytes calldata _data
    ) external view returns (bool isValid);
}
Source: contracts/interfaces/IAttestationVerifier.sol:8-21

Attestation Structure

While the SimpleAttestationVerifier only validates signatures on a digest, the digest is typically an EIP-712 formatted hash of a structured attestation. In the zkp2p system, this is the PaymentAttestation:
struct PaymentAttestation {
    bytes32 intentHash;       // Binds the payment to the intent
    uint256 releaseAmount;    // Final token amount to release
    bytes32 dataHash;         // Hash of the payment details
    bytes[] signatures;       // Witness signatures
    bytes data;               // Encoded payment details
    bytes metadata;           // Additional metadata (not signed)
}
The digest passed to verify() is:
bytes32 digest = keccak256(
    abi.encodePacked(
        "\x19\x01",
        DOMAIN_SEPARATOR,
        structHash
    )
);
Where structHash is the hash of the attestation fields according to EIP-712.

Signature Verification

The verify function validates that the provided signatures meet the required threshold:
function verify(
    bytes32 _digest,
    bytes[] calldata _sigs,
    bytes calldata
) external view override returns (bool isValid) {
    address[] memory witnesses = new address[](1);
    witnesses[0] = witness;
    
    // Verify signatures meet threshold
    isValid = ThresholdSigVerifierUtils.verifyWitnessSignatures(
        _digest,
        _sigs,
        witnesses,
        MIN_WITNESS_SIGNATURES // = 1
    );

    return isValid;
}
Source: contracts/unifiedVerifier/SimpleAttestationVerifier.sol:56-74

Parameters

  • _digest: The EIP-712 formatted message digest to verify
  • _sigs: Array of signatures (must contain at least 1 valid signature)
  • _data: Verification data (unused in SimpleAttestationVerifier but part of interface)

Return Value

  • isValid: True if at least one signature is valid from the configured witness

Threshold Signature Verification

The contract uses the ThresholdSigVerifierUtils library to validate signatures:
function verifyWitnessSignatures(
    bytes32 _digest,
    bytes[] memory _signatures,
    address[] memory _witnesses,
    uint256 _reqThreshold
)
    internal
    view
    returns (bool success)
Source: contracts/lib/ThresholdSigVerifierUtils.sol:29-86

Verification Logic

  1. Validates that the required threshold is > 0
  2. Ensures enough signatures and witnesses are provided
  3. For each signature, checks if any witness created it using SignatureChecker.isValidSignatureNow
  4. Tracks unique signers to prevent counting the same witness twice
  5. Returns true once the threshold is met (early exit optimization)
  6. Reverts if the threshold is not met

Supported Signature Types

The library uses OpenZeppelin’s SignatureChecker, which supports:
  • EOA signatures (ECDSA)
  • EIP-1271 smart contract signatures
  • Both EIP-712 and EIP-191 formatted messages

Trust Model

Single Witness Model

The SimpleAttestationVerifier uses a single witness trust model:
uint256 public constant MIN_WITNESS_SIGNATURES = 1;
address public witness;
This model is suitable for:
  • Development and testing environments
  • Scenarios where the witness is a trusted entity
  • Systems where the witness operates secure infrastructure (e.g., TEE)

Security Considerations

Single Point of Failure: The witness private key is a single point of failure. If compromised, attackers could sign fraudulent attestations. Mitigation Strategies:
  • Use hardware security modules (HSM) to protect witness keys
  • Implement witness key rotation procedures
  • Monitor witness activity for anomalies
  • Consider upgrading to multi-witness threshold model for production

Governance Functions

Setting the Witness

The contract owner can update the witness address:
function setWitness(address _newWitness) external onlyOwner {
    require(_newWitness != address(0), "SimpleAttestationVerifier: Zero address");
    
    address oldWitness = witness;
    witness = _newWitness;
    
    emit WitnessUpdated(oldWitness, _newWitness);
}
Source: contracts/unifiedVerifier/SimpleAttestationVerifier.sol:82-89

Access Control

The contract inherits from OpenZeppelin’s Ownable, providing:
  • onlyOwner modifier for privileged functions
  • transferOwnership for changing contract ownership
  • renounceOwnership for removing ownership (use with caution)

Events

WitnessUpdated

event WitnessUpdated(address indexed oldWitness, address indexed newWitness);
Emitted when the witness address is updated. Source: contracts/unifiedVerifier/SimpleAttestationVerifier.sol:27

Example Usage

From Payment Verifier

// Inside UnifiedPaymentVerifier._verifyAttestation()

bytes32 structHash = keccak256(
    abi.encode(
        PAYMENT_ATTESTATION_TYPEHASH,
        attestation.intentHash,
        attestation.releaseAmount,
        attestation.dataHash
    )
);

bytes32 digest = keccak256(
    abi.encodePacked(
        "\x19\x01",
        DOMAIN_SEPARATOR,
        structHash
    )
);

// Verify data integrity
require(
    keccak256(attestation.data) == attestation.dataHash,
    "UPV: Data hash mismatch"
);

// Verify witness signature
bool isValid = attestationVerifier.verify(
    digest, 
    attestation.signatures,
    attestation.data
);
Source: contracts/unifiedVerifier/UnifiedPaymentVerifier.sol:183-214

Deploying the Contract

// Deploy with initial witness
SimpleAttestationVerifier verifier = new SimpleAttestationVerifier(witnessAddress);

// Or deploy and set witness later
SimpleAttestationVerifier verifier = new SimpleAttestationVerifier(address(0));
verifier.setWitness(witnessAddress);

Integration Points

The SimpleAttestationVerifier is used by:

UnifiedPaymentVerifier

Stored as an immutable reference in BaseUnifiedPaymentVerifier:
IAttestationVerifier public attestationVerifier;
Can be updated via governance:
function setAttestationVerifier(address _newVerifier) external onlyOwner
Source: contracts/unifiedVerifier/BaseUnifiedPaymentVerifier.sol:38,104-111

Upgrading to Multi-Witness

For production systems requiring higher security, consider implementing a multi-witness attestation verifier:
contract MultiWitnessAttestationVerifier is IAttestationVerifier {
    uint256 public constant MIN_WITNESS_SIGNATURES = 2; // Require 2 of 3
    address[] public witnesses; // Multiple witnesses
    
    function verify(
        bytes32 _digest,
        bytes[] calldata _sigs,
        bytes calldata
    ) external view override returns (bool isValid) {
        return ThresholdSigVerifierUtils.verifyWitnessSignatures(
            _digest,
            _sigs,
            witnesses,
            MIN_WITNESS_SIGNATURES
        );
    }
}
This provides Byzantine fault tolerance where the system remains secure as long as less than the threshold of witnesses are compromised.

Security Best Practices

Witness Key Management

  1. Key Generation: Use secure random number generation for witness keys
  2. Key Storage: Store witness keys in HSM or secure enclaves
  3. Key Rotation: Implement regular witness key rotation schedules
  4. Access Control: Limit access to witness signing infrastructure

Monitoring

  1. Signature Activity: Monitor witness signature frequency and patterns
  2. Failed Verifications: Alert on unusual numbers of failed verifications
  3. Witness Updates: Log and audit all witness address changes

Testing

  1. Signature Validation: Test with valid and invalid signatures
  2. Edge Cases: Test with zero signatures, multiple signatures, wrong witness
  3. Replay Protection: Verify attestations cannot be reused (handled by payment verifier)
  • UnifiedPaymentVerifier - Uses attestation verifier for payment validation
  • ThresholdSigVerifierUtils - Signature verification library
  • IAttestationVerifier - Interface specification

Build docs developers (and LLMs) love