Skip to main content
When you retrieve an attestation from Circle’s Attestation Service, you can optionally verify the attestation signature before using it to mint USDC on the destination blockchain. This page explains how the verification process works and when you might want to use it.

How verification works

The verification process uses cryptographic signature recovery to confirm that Circle’s Attestation Service signed the message. It involves the following steps:
1

Retrieve the public key

Fetch Circle’s current public key from the GET /v2/publicKeys endpoint.
2

Hash the message

Create a keccak256 hash of the message bytes.
3

Parse the attestation

Split the 65-byte attestation into its r, s, and v components (ECDSA signature format).
4

Recover the signer

Use the signature and message hash to recover the public key that signed the message.
5

Compare addresses

Convert both the recovered public key and Circle’s public key to Ethereum addresses and compare them.
If the addresses match, the attestation was signed by Circle’s Attestation Service and is valid.

When to verify attestations

Attestation verification is optional because the CCTP contracts on the destination blockchain perform their own verification when you call receiveMessage. However, you might want to verify attestations before submitting the mint transaction if:
  • Your application requires an additional layer of security: Verifying before minting provides defense-in-depth by catching invalid attestations at the application layer.
  • You want to detect invalid attestations before paying gas fees: If an attestation is invalid, the mint transaction fails and you lose the gas fees. Pre-verification lets you catch this before submitting the transaction.
  • You’re building a relayer service that batches multiple attestations: Relayers can verify each attestation in a batch before submitting, preventing a single invalid attestation from affecting the entire batch.

Verification code example

The following example shows how to verify an attestation signature using the ethers library:
JavaScript
import { ethers } from "ethers";

async function getPublicKey() {
  const response = await fetch(
    "https://iris-api-sandbox.circle.com/v2/publicKeys",
  );
  const data = await response.json();

  // Find the public key for CCTP V2
  const v2Key = data.publicKeys.find((key) => key.cctpVersion === "2");

  if (!v2Key) {
    throw new Error("CCTP V2 public key not found");
  }

  return v2Key.publicKey;
}

function verifyAttestation(message, attestation, publicKey) {
  try {
    // Hash the message using keccak256
    const messageHash = ethers.keccak256(message);

    // Split the attestation into r, s, and v components
    // Attestation format: 65 bytes (r: 32 bytes, s: 32 bytes, v: 1 byte)
    const attestationBytes = ethers.getBytes(attestation);

    if (attestationBytes.length !== 65) {
      throw new Error("Invalid attestation length");
    }

    const r = ethers.hexlify(attestationBytes.slice(0, 32));
    const s = ethers.hexlify(attestationBytes.slice(32, 64));
    const v = attestationBytes[64];

    // Reconstruct the signature
    const signature = { r, s, v };

    // Recover the public key from the signature
    const recoveredAddress = ethers.recoverAddress(messageHash, signature);

    // Convert the provided public key to an address
    const expectedAddress = ethers.computeAddress(publicKey);

    // Compare addresses
    if (recoveredAddress.toLowerCase() === expectedAddress.toLowerCase()) {
      console.log("Attestation signature is valid!");
      return true;
    } else {
      console.log("Attestation signature is invalid!");
      return false;
    }
  } catch (error) {
    console.error("Error verifying attestation:", error.message);
    return false;
  }
}

// Example usage
const publicKey = await getPublicKey();
const isValid = verifyAttestation(message, attestation, publicKey);