Identity Proofs (Cardano)

Here we explain how Fairway encodes KYC/AML eligibility on Cardano using a single UTXO with a Merkle root, Fairway signature, a verification timestamp, and a reference to a Midnight proof UTXO.

Background

Instead of minting identity tokens per user (e.g., CIP-113 or NFTs), Fairway maintains a canonical Compliance UTXO that contains:

  • merkle_root → commitment to all compliant addresses + claims.

  • sanctions_epoch → global counter for sanctions/risk updates.

  • fairway_sig → Fairway’s signature binding root + epoch + Midnight ref.

  • midnight_ref → pointer to the ZK-proof UTXO on Midnight (audit trail).

Each Merkle leaf represents a user and includes:

  • address → wallet identifier.

  • claims → e.g., KYC2, EU_RESIDENT.

  • kyc_verified_at → timestamp of last KYC check.

Eligibility is checked by proving Merkle membership and verifying both:

  • Fairway signature over the Compliance UTXO state.

  • Freshness conditions (timestamp vs. sanctions epoch).


Data Model

UTXO Datum

{
  "merkle_root": "0xabc123...",
  "sanctions_epoch": 452,
  "midnight_ref": "0xdeadbeef#0",
  "fairway_sig": "sig(fairway_key, hash(merkle_root || sanctions_epoch || midnight_ref))"
}

Merkle Leaf

{
  "address": "addr1...",
  "claims": ["KYC2", "EU_RESIDENT"],
  "kyc_verified_at": "2025-03-15T10:00:00Z"
}

Flow

sequenceDiagram
  participant Vault as Decentralized Storage (PII)
  participant Agent as Fairway Cloud Agent
  participant Midnight as Midnight Proof UTXO
  participant Cardano as Compliance UTXO
  participant User as Wallet
  participant dApp as Protocol Validator

  Vault->>Agent: Scoped KYC read
  Agent->>Midnight: Generate & post ZK proof (proof UTXO)
  Agent->>Cardano: Update Merkle root + sanctions_epoch + Fairway sig
  User->>dApp: Submit Merkle proof {address, claims, kyc_verified_at}
  dApp->>Cardano: Verify Merkle membership, Fairway sig, sanctions_epoch
  dApp->>User: Access granted if valid

Validator Pseudocode (Aiken)

validator checkEligibility {
  fn is_valid(ctx: ScriptContext, proof: MerkleProof, leaf: Leaf, max_age: Int) -> Bool {
    let utxo = get_utxo(ctx, FAIRWAY_UTXO_REF)

    // 1. Verify Merkle proof
    let ok_proof = verify_merkle(proof, utxo.datum.merkle_root)

    // 2. Verify Fairway signature
    let payload = hash(utxo.datum.merkle_root || utxo.datum.sanctions_epoch || utxo.datum.midnight_ref)
    let ok_sig = verify_signature(FAIRWAY_PUBKEY, payload, utxo.datum.fairway_sig)

    // 3. Check sanctions epoch freshness
    let ok_epoch = utxo.datum.sanctions_epoch == current_sanctions_epoch()

    // 4. Check KYC recency (dApp-defined)
    let age = ctx.tx.now - leaf.kyc_verified_at
    let ok_kyc = age <= max_age

    ok_proof && ok_sig && ok_epoch && ok_kyc
  }
}
  • max_age is defined by the protocol/institution (e.g., 180 days).

  • The validator never opens raw PII.

  • midnight_ref links to a verifiable ZK proof on Midnight for audits.


Benefits

  • Compact → one UTXO anchors all compliant users.

  • Flexible → institutions define their own max_age for KYC proofs.

  • Auditablemidnight_ref links back to ZK proof.

  • Compliant → sanctions freshness via epoch, KYC recency via timestamp.

  • Composable → eligibility checks slot directly into LP/staking validators.


Next Steps

Last updated

Was this helpful?