Skip to main content

Zero-knowledge compliance verification

This tutorial walks you through DPO2U's end-to-end compliance verification pipeline — from generating an LGPD policy kit to recording a zero-knowledge proof on the the host chain. You will understand how each smart contract works, how privacy is preserved at every step, and how to verify an Attestation programmatically.

Prerequisites:

  • Familiarity with blockchain concepts (transactions, smart contracts, hashes)
  • Basic understanding of privacy regulations (LGPD or GDPR)
  • Node.js v18+ installed (for the verification examples)
  • [Compact toolchain](https://docs.dpo2u.com installed (optional, for compiling contracts)
tip

You don't need to know Compact or zk-SNARKs to follow this tutorial. Each concept is introduced when it first appears.

Why zero-knowledge proofs for compliance

Traditional compliance relies on trust: a consultant reviews documents, produces a PDF, and everyone agrees to treat it as "verification." The PDF can be edited. The review date can be falsified. Nobody checks because checking is expensive.

Public blockchains solve immutability but create a new problem: all state is transparent. How do you prove compliance with privacy laws (LGPD/GDPR) without exposing the very data those laws protect? This is the privacy paradox that zero-knowledge proofs resolve.

A zk-SNARK lets a Prover convince a Verifier that a statement is true without revealing any information beyond the truth of the statement itself. Think of it like the Ali Baba cave analogy: Alice proves she knows the secret password by consistently exiting from whichever side Bob requests — after 20 rounds, the probability of luck is 1 in 1,048,576, yet Bob never hears the password.

For a deeper exploration of this paradigm shift — from "trust us" to "verify mathematically" — read Trustless trust: how on-chain attestations replace reputation management.

The key insight: compliance becomes a verifiable property of a system, not a document stored in a shared drive. The proof is on-chain, the documents are on IPFS, and verification is a single API call.

How zk-SNARKs work: the three phases

Every zk-SNARK goes through three phases:

PhaseWhen it happensWhat it produces
SetupOnce, at contract deployProving key (pk) and verifying key (vk)
Proof generationEach time an Attestation is issuedA compact proof (a few KB) that the circuit was executed correctly
VerificationAny time, by anyoneA boolean: valid or invalid (milliseconds, on-chain)
Proving Key (pk) ──┐
Witness (private) ──┤──→ [ZK-SNARK Algorithm] ──→ Proof (π)
Circuit ───────────┘

Verifying Key (vk) ──┐
Proof (π) ────────────┤──→ [Verifier] ──→ VALID / INVALID
Public Output ────────┘

The witness is the private data — in DPO2U's case, the full compliance attestation JSON. The circuit is the smart contracts logic compiled into arithmetic constraints. The proof attests that the circuit was executed correctly with a valid witness, without revealing what the witness contains.

ZK-SNARKs vs ZK-STARKs

The the host chain uses zk-SNARKs because they produce smaller proofs and faster on-chain verification. ZK-STARKs offer quantum resistance but generate larger proofs, increasing verification costs. For compliance use cases where verification efficiency is critical, zk-SNARKs are the better fit.

The compliance verification flow

The entire pipeline — from a client's request to an on-chain Attestation — passes through five stages:

Here is what happens at each stage:

  1. Schema generation — A client AI calls generate_lgpd_kit via the MCP Server. The Expert Agent produces a policy.json conforming to the dpo2u/lgpd/v1 schema, plus human-readable reports (privacy notice, DPIA).

  2. Immutability via IPFS — The reports and schema are uploaded to IPFS via the Lighthouse SDK. Each artifact receives a CID (content-addressed hash). Modifying a single byte changes the CID, invalidating all downstream references.

  3. Zero-trust edge analysis — The Auditor Agent retrieves the schema and CIDs, validates all mandatory fields, and scores compliance on a 0–100 scale. The validation runs in a sandbox with no network access beyond IPFS retrieval and the the host chain RPC endpoint.

  4. On-chain settlement — The Auditor's score and CID are wrapped in a zk-SNARK proof by the local Proof Server (proof-server:7.0.0). The proof is submitted to ComplianceRegistry.compact on the the host chain.

  5. Verification — Any party calls check_compliance_status(company_id) and receives a verifiable result. No PDF. No shared drive. No trust required.

For the full protocol lifecycle with validation rules and failure behaviors, read From PDF to proof.

Understanding the ComplianceRegistry contract

ComplianceRegistry.compact is the central registry that holds all generated Attestations. Here is the contract using real Compact syntax:

// compliance-attestation.compact
pragma language_version 0.20;

// ── LEDGER STATE (public on-chain) ──────────────────────────────
// These values are visible to anyone on the blockchain.

// SHA-256 hash of the most recent compliance attestation.
// The hash is public (verifiable), but the attestation content is private.
export ledger complianceHash: Opaque<"string">;

// Public key of the contract owner (DPO or company).
export ledger owner: PublicKey;

// Timestamp of the last issued attestation.
export ledger lastAttestationTimestamp: Uint64;

// ── CIRCUITS (contract functions) ───────────────────────────────
// Parameters are PRIVATE by default — they never appear on the blockchain.

// Initialize the contract, setting the owner.
export circuit initialize(ownerKey: PublicKey): [] {
// disclose() moves a value from the private domain to the public ledger.
owner = disclose(ownerKey);
}

// Issue a new compliance attestation.
// attestationData remains PRIVATE; only its hash is stored publicly.
export circuit issueAttestation(
attestationData: Opaque<"string">, // Full JSON (PRIVATE)
attestationHash: Opaque<"string">, // SHA-256 hash (becomes PUBLIC)
timestamp: Uint64, // Issuance time (becomes PUBLIC)
signerKey: PrivateKey // Proves caller is the owner
): [] {
// Verify the caller is the contract owner.
// publicKey() derives the public key from the private key.
assert(checkSignature(publicKey(signerKey), owner));

// Store the hash publicly. The original attestationData
// NEVER touches the public ledger.
complianceHash = disclose(attestationHash);

// Record the issuance timestamp.
lastAttestationTimestamp = disclose(timestamp);
}

Key points to understand:

  • export ledger — Declares public on-chain state. Anyone can read these values. This is the only data that lives on the blockchain.

  • export circuit — Defines a circuit function. The the contract compiler turns each circuit into a zk-SNARK with a deterministic size. Circuit parameters are private by default.

  • disclose() — The bridge between private and public. Assigning a private value to a ledger variable without disclose() causes a compilation error. This makes data leaks impossible by design.

  • Opaque<"string"> — Represents an external value (a JavaScript string) that Compact can store and transmit without interpreting its internal structure. Useful for hashes, identifiers, and CIDs.

  • assert(checkSignature(...)) — Authorization check that runs inside the zk-SNARK circuit. The verifier confirms the check passed without seeing the signer's private key.

Privacy guarantee

Trying to write complianceHash = attestationHash without disclose() produces a compile-time error. This is not a convention — it is enforced by the compiler. Private data cannot leak to the ledger accidentally.

What is stored on-chain vs off-chain
On-chain (the ledger)Off-chain (IPFS)
Hashed company IDPrivacy notice (full text)
IPFS CID (pointer)DPIA document
Compliance scorepolicy.json with all fields
TimestampCompany profile data
Agent DIDDPO contact information

Compiling the contract

The the contract compiler transforms the contract into zk-SNARK circuits and generates the cryptographic keys:

compact compile contracts/compliance-attestation.compact contracts/managed/compliance-attestation

This produces the following artifacts:

DirectoryContentsPurpose
contract/index.js, index.d.tsTypeScript API to call the contract's circuits from your DApp
keys/*.pk, *.vkProving key (generates proofs) and verifying key (verifies proofs)
zkir/.zkir filesZero-Knowledge Intermediate Representation — the bridge between Compact and the cryptographic backend

How Compact differs from Solidity

If you come from an Ethereum / Solidity background, Compact requires a mental shift. It is not a Solidity variant — it is a zero-knowledge circuit language.

AspectSolidityCompact
Execution modelEVM bytecodezk-SNARK circuits
PrivacyPublic by defaultPrivate by default
LoopsArbitrary (for, while)No arbitrary loops (circuit size must be deterministic)
MemoryDynamic allocationFixed data structures
StringsNative supportOpaque<"string"> (field elements)
DisclosureN/Adisclose() for selective revelation
State declarationmapping, uint, addressexport ledger with typed fields

The critical difference is the disclose() primitive. In Compact, all data is private unless you explicitly choose to reveal it. This is the opposite of Solidity, where all state is public unless you engineer privacy on top.

// Compact: selective disclosure
export circuit verifyCompliance(company_id: Opaque<"string">): [] {
// The circuit reads the ledger state and the private input,
// but only discloses what the developer explicitly chooses.
// Here we reveal the score but NOT the agent DID or CID.
disclose(complianceHash);
disclose(lastAttestationTimestamp);
}

For a comparison of ZK-circuit smart-contract languages vs general-purpose ones, see the architecture docs.

The attestation data model

Every Attestation starts as a compliance assessment generated by the DPO2U Agent. This JSON document is private — it is never stored directly on the blockchain:

{
"version": "1.0",
"schemaType": "DPO2U_COMPLIANCE_ATTESTATION",
"issuer": {
"did": "did:dpo2u:agent:auditor-001",
"name": "DPO2U Auditor Agent",
"version": "0.9.1"
},
"subject": {
"companyId": "cnpj:12.345.678/0001-90",
"companyName": "Acme Corp Ltda",
"assessmentDate": "2026-03-04T12:00:00Z"
},
"compliance": {
"lgpd": {
"status": "compliant",
"score": 87,
"criticalFindings": 0,
"lastAssessment": "2026-03-04T10:00:00Z"
},
"gdpr": {
"status": "compliant",
"score": 82,
"criticalFindings": 0,
"lastAssessment": "2026-03-04T10:00:00Z"
}
},
"evidenceHashes": {
"dpia": "sha256:a1b2c3d4...",
"dataInventory": "sha256:e5f6g7h8...",
"privacyPolicy": "sha256:i9j0k1l2..."
}
}

The evidenceHashes field is key: instead of including full documents, you include only their SHA-256 hashes. An auditor can verify document integrity (if they are provided separately) without the attestation carrying sensitive data.

This attestation conforms to the dpo2u/lgpd/v1 schema. Four fields are mandatory and block the pipeline if missing:

FieldLGPD articlePurpose
retention_daysArt. 15How long data is kept
consent_mechanismArt. 7How consent is obtained
dpo_contactArt. 41Data Protection Officer contact
last_reviewArt. 6, VWhen the policy was last reviewed

The data boundary is strict:

Raw company data never touches the blockchain. Only the hash of the attestation, the IPFS CID (a pointer), and the timestamp are recorded on-chain.

Issuing an attestation

Here is the complete flow for issuing an Attestation programmatically. The script loads a compliance assessment, hashes it, and submits it to the smart contracts:

import { createHash } from 'crypto';
import { readFileSync } from 'fs';

// TypeScript API generated by `compact compile`
import { ComplianceAttestationContract } from
'../contracts/managed/compliance-attestation/contract/index.js';

// Generate a deterministic SHA-256 hash of the attestation
function generateAttestationHash(attestation: object): string {
const normalized = JSON.stringify(
attestation,
Object.keys(attestation).sort()
);
return createHash('sha256').update(normalized).digest('hex');
}

async function issueComplianceAttestation() {
// 1. Load the compliance assessment (generated by the DPO2U Agent)
const attestationData = JSON.parse(
readFileSync('./attestation-data.json', 'utf-8')
);

// 2. Hash the attestation — this hash goes on-chain
const attestationHash = generateAttestationHash(attestationData);

// 3. Connect to the host chain
const provider = await ComplianceAttestationContract.createProvider({
endpoint: 'https://rpc.dpo2u.com
networkId: 'preprod'
});
const wallet = provider.createWallet(process.env.DPO_PRIVATE_KEY!);

// 4. Call the issueAttestation circuit
// - attestationData is PRIVATE (never leaves the local machine)
// - attestationHash is PUBLIC (stored on the ledger)
const contract = new ComplianceAttestationContract(
process.env.CONTRACT_ADDRESS!,
wallet
);

const tx = await contract.issueAttestation(
JSON.stringify(attestationData), // private witness
attestationHash, // public output
BigInt(Date.now()), // timestamp
wallet.privateKey // authorization
);

const receipt = await tx.wait();

console.log('Attestation registered on-chain');
console.log('Transaction:', receipt.transactionHash);
console.log('Compliance hash:', attestationHash);
}

issueComplianceAttestation();
Proof generation

When you call contract.issueAttestation(), the SDK automatically generates the zk-SNARK proof using the proving key generated during compilation. This step may take a few seconds. The proof is included in the transaction and verified on-chain by the the host chain validators.

Verifying an attestation

Once an Attestation is on-chain, any party can verify it. Here are two approaches:

Using the MCP Server

The simplest method — a single API call:

curl -X POST https://mcp.dpo2u.com/tools/check_compliance_status \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"company_id": "cnpj:12.345.678/0001-90"}'

Response:

{
"compliant": true,
"score": 92,
"last_validated": "2026-03-13T14:30:00Z",
"proof_url": "zk-attestation://attestation/0x7a3f..."
}

For rate limits and error codes, see the MCP Server documentation.

Using TypeScript with the the SDK

For programmatic verification — including hash comparison against a local copy of the attestation:

import { createHash } from 'crypto';
import { ComplianceAttestationContract } from
'../contracts/managed/compliance-attestation/contract/index.js';

async function verifyAttestation(
contractAddress: string,
localAttestationData: object
) {
// Connect read-only — no private key needed for verification
const provider = await ComplianceAttestationContract.createReadOnlyProvider({
endpoint: 'https://rpc.dpo2u.com
});
const contract = new ComplianceAttestationContract(contractAddress, provider);

// Read the hash stored on-chain
const onChainHash = await contract.complianceHash();
const onChainTimestamp = await contract.lastAttestationTimestamp();

// Recompute the hash from the local attestation data
const localHash = createHash('sha256')
.update(JSON.stringify(
localAttestationData,
Object.keys(localAttestationData).sort()
))
.digest('hex');

// Compare: if they match, the attestation is authentic and untampered
const isValid = localHash === onChainHash;

console.log('On-chain hash:', onChainHash);
console.log('Local hash: ', localHash);
console.log('Timestamp: ', new Date(Number(onChainTimestamp)).toISOString());
console.log('Valid: ', isValid);

return isValid;
}
Testnet only

The the SDK examples target the testnet. Contract addresses and RPC endpoints will change when mainnet launches.

Agent authorization with AgentRegistry

Not everyone can create Attestations — only agents registered in AgentRegistry.compact have permission. This contract maintains a ledger of approved did:dpo2u:agent:* identities:

pragma language_version 0.20;

// Public ledger of authorized agent DIDs
export ledger agentCount: Uint64;

// Register a new agent — only governance can call this
export circuit registerAgent(
agentDid: Opaque<"string">,
agentName: Opaque<"string">,
permissions: Uint8,
governanceKey: PrivateKey
): [] {
// Verify caller has governance permission
assert(checkSignature(publicKey(governanceKey), governanceOwner));

// The agent DID and name become public on the ledger
// but the governance key remains private
agentCount = disclose(agentCount + 1);
}

// Check if an agent is authorized to issue attestations
export circuit isAuthorized(agentDid: Opaque<"string">): [] {
// Circuit verifies the agent exists and has WRITE permission
// The proof confirms authorization without revealing
// the full permission set or private key
disclose(agentDid);
}

The authorization model uses permission bits:

BitValueDescription
READ1Query on-chain data
WRITE2Create Attestations
TREASURY4Financial operations
DEPLOY8Deploy contracts
GOVERNANCE16Manage agent registry

When ComplianceRegistry calls isAuthorizedAgent(agent_did), it delegates to AgentRegistry.isAuthorized(). The zk-SNARK proves the agent has WRITE permission without revealing the agent's full permission set or private key.

DID format

Agent DIDs follow the pattern did:dpo2u:agent:<name>. Only agents listed in AgentRegistry can interact with ComplianceRegistry. For the Base Chain equivalent, see the AgentRegistry Solidity contract.

Selective disclosure: practical scenarios

One of the most powerful applications of zk-SNARKs for compliance is selective disclosure — proving exactly what needs to be proven, and nothing more.

Regulatory audit (ANPD)

The Brazilian data protection authority (ANPD) wants to verify that a company has a designated DPO and an up-to-date privacy policy. With zk-SNARKs, the company proves exactly that — without revealing the DPO's name, the policy content, or any other operational data.

B2B supplier contract

A company requires suppliers to demonstrate LGPD compliance before signing a data processing agreement. The supplier generates a proof that their compliance score exceeds 80 — without revealing the exact score, their vulnerabilities, or any other sensitive information.

M&A due diligence

During an acquisition, the buyer needs to verify the target company's compliance status. The target proves it has no critical pending violations — without exposing operational data processing details that are competitively sensitive.

LGPD/GDPR mapping

Each field in the dpo2u/lgpd/v1 schema maps to specific LGPD articles. The zero-knowledge verification ensures compliance without exposing the underlying data:

LGPD principleArticleSchema fieldZK verification
Finalidade (Purpose)Art. 6, Idata_categoriesCircuit checks categories are declared
Adequação (Adequacy)Art. 6, IIdata_categoriesCircuit validates categories match declared purpose
Necessidade (Necessity)Art. 6, IIIdata_categoriesCircuit checks no excessive categories
Livre acesso (Free access)Art. 6, IVdpo_contactCircuit verifies contact is non-null
Qualidade (Data quality)Art. 6, Vlast_reviewCircuit checks review within 365 days
Transparência (Transparency)Art. 6, VIprivacy_notice docCID exists on IPFS (verifiable)
Segurança (Security)Art. 6, VIIOn-chain AttestationThe proof itself is the security guarantee
Prevenção (Prevention)Art. 6, VIIIdpia docCID exists on IPFS (verifiable)
Responsabilização (Accountability)Art. 6, XImmutable CID + AttestationPermanent, tamper-proof record
Consentimento (Consent)Art. 7, Iconsent_mechanismCircuit checks mechanism is declared
GDPR equivalence

These principles map directly to GDPR Articles 5 and 6. The schema is designed for LGPD but the verification logic applies equally to GDPR compliance assessments.

For a developer-oriented guide to LGPD's 10 legal bases and when each applies, read LGPD legal bases: a developer guide. For the nuances of legitimate interest — the most dangerous legal basis — see Legitimate interest: the dangerous legal basis.

Attestation lifecycle

A compliance attestation is not a one-time event — it is a continuous process. DPO2U supports a lifecycle aligned with regulatory requirements:

EventRecommended frequencyDPO2U action
Compliance assessmentQuarterlyAgent runs new analysis, issues updated attestation
Policy updateAs neededNew attestation referencing updated policy CID
Security incidentImmediateIncident response attestation recorded on-chain
Regulatory auditOn demandRegulator verifies the history of on-chain hashes
Annual renewalYearlyFull attestation with updated score

Each new attestation overwrites the complianceHash on the ledger, but the previous values remain in the blockchain's transaction history — providing a complete, tamper-proof audit trail.

What's next