ZKP compliance for 42 CFR Part 2 — substance use treatment privacy

I’m building a check-in app for a drug recovery drop-in center using Midnight’s ZKP technology.

42 CFR Part 2 is a federal law that protects substance use treatment records — even stricter than HIPAA. It means you can’t disclose that someone is even a patient without their consent.

My question: Can a Zero Knowledge Proof prove that someone checked in without revealing their identity or treatment status to anyone?

Has anyone explored using Midnight for healthcare or sensitive data compliance use cases? Any guidance appreciated!

2 Likes

Welcome to the community, DR.Ecovery! This is a fascinating use case.

The short answer is yes. This is exactly the kind of ‘selective disclosure’ Midnight is designed for.

In a Midnight-based app, you can use a Zero Knowledge Proof to verify that a state transition occurred (e.g., ‘a valid patient checked in’) without revealing the underlying private data (the patient’s identity or status) on the public ledger. To an observer, the proof confirms the event is valid according to your rules, but the data remains shielded.

Healthcare compliance is a major area of interest for the ecosystem. I’ll share this thread for our devrel team to see if we have specific compliance frameworks or similar projects to share.

Looking forward to seeing how this develops!

3 Likes

Yeah, this is the kind of application Midnight was designed for. Smart contracts have two states: public and private. Private state is stored locally. A hash of the private state is generated and stored in the public state. To validate the private state without exposing it, your contract will publish a zero-knowledge proof once the transaction is submitted, and the network validates this proof against the hash in the public state.

2 Likes

Hi @DR.Ecovery! Following up on this after chatting with our team

To give you a concrete path forward for the 42 CFR Part 2 “status anonymity” requirement, we recommend a two-layered architecture:

1. Anonymous Check-ins (The Membership Proof Pattern)

To satisfy the rule that “even being a patient” is protected status, you can use a Membership Proof + Nullifier. This proves “I am someone on the clinic’s list” without revealing which leaf in the tree you are.

Example of how to implement this in Compact:

// 1. A private tree of all authorized patient commitments
ledger patientTree: HistoricMerkleTree<32, Bytes<32>>;

// 2. Prevent double-check-ins using a daily Nullifier
ledger usedNullifiers: Set<Bytes<32>>;

witness patientPath(): MerkleTreePath<32, Bytes<32>>;
witness patientSecret(): Bytes<32>;

export circuit checkIn(currentDate: Bytes<32>): [] {
    const path = patientPath();
    const sk = patientSecret();
    
    // Prove the user is in the patient tree without revealing their identity
    assert(patientTree.checkRoot(merkleTreePathRoot<32, Bytes<32>>(path)));
    assert(persistentHash<Bytes<32>>(sk) == path.leaf);

    // Use a nullifier (Secret + Date) to prevent identity tracking
    const nullifier = persistentHash<Vector<2, Bytes<32>>>([sk, currentDate]);
    assert(!usedNullifiers.member(nullifier), "Already checked in today");
    usedNullifiers.insert(nullifier);
}

2. Confidential Record Storage (Off-chain Pattern)

For the actual medical records (treatment notes), you shouldn’t store them on-chain. Instead, use a pattern of Client-side Encryption + IPFS.

The Storage Pattern:

struct DocumentRecord {
    contentHash: Bytes<32>,     // Hash of the encrypted file for integrity
    storageCid: Opaque<"string">, // The IPFS/Arweave CID
    ownerCommitment: Bytes<32>, // Commitment of the patient who owns it
    isActive: Boolean
}
export ledger records: Map<Bytes<32>, DocumentRecord>;

Workflow:

  1. Encrypt Locally: Use AES-256-GCM to encrypt the records on the provider’s device.
  2. IPFS: Upload the encrypted blob to IPFS.
  3. Audit Trail: Store only the CID and the hash on Midnight. This gives you a mathematical audit trail without ever exposing the patient’s data or status to the public ledger.

Reference Resources

We have a community-led reference project called midnight-doc-manager that demonstrates this exact encryption-to-IPFS flow. It’s an excellent starting point for how to handle large, sensitive files while keeping the verification on Midnight.

You can also find our official, maintained examples here:
midnightntwrk repositories · GitHub
Does this technical architecture align with your compliance requirements? We’d love to hear how your implementation goes!

1 Like

@nasihudeenJ — thank you, this is exactly what I needed.

I already have a working capstone on the example-bboard repo (branch capstone/andres-chavez, PR open) with 6 Compact circuits and 13 passing tests. The current implementation uses the sealed owner pattern. Your Merkle tree + nullifier architecture is a direct upgrade path and I’m planning to implement it in Phase 2.

A little context: I’m a self-taught developer who works at an overdose drop-in center in El Paso serving ~15,000 contacts per year. I built MANO to solve a real problem I see every day — staff re-asking participants the same sensitive demographic questions, no privacy guarantees, no way for participants to control their own data. I’m building this to pitch to leadership as a real implementation. The use case is genuine even if deployment isn’t confirmed yet.

A few specific questions as I plan the Phase 2 upgrade:

  1. Tree sizing — What depth should HistoricMerkleTree be for a clinic expecting ~500 enrolled participants? Is <32, Bytes<32>> right or overkill?

  2. Enrollment circuit — Your example shows checkIn but not enroll. Is the pattern simply patientTree.insert(persistentHash(sk))? Does staff call enroll, or does the participant’s witness provide the leaf?

  3. IPFS + Midnight together — For visit records, is it acceptable to store the Pinata CID in a separate database alongside the on-chain hash, or should the CID live on-chain inside the DocumentRecord struct?

Happy to share more about the use case if it’s useful to the ecosystem.

@DR.Ecovery thank you for giving more context

Tree sizing: <32, Bytes<32>> is overkill for ~500 enrolled. In Compact, MerkleTree<n, T> / HistoricMerkleTree<n, T> use depth n with capacity 2^n leaves (max depth 32).
For ~500, HistoricMerkleTree<10, Bytes<32>> (1024 leaves) is a comfortable fit; <9, …> (512) works if enrollment stays capped near 500.
Prefer HistoricMerkleTree over plain MerkleTree if you enroll often checkRoot accepts proofs against prior roots after inserts, which matters when the tree keeps growing. Smaller depth = smaller circuits and faster proving.

Enrollment: Yes pattern is essentially patientTree.insert(persistentHash(sk)) (or insertHash with the same commitment). Participant generates sk locally and only shares the commitment with the clinic; staff never needs the secret. Typical split:

  • enroll staff-gated circuit (your existing admin/sealed pattern): takes disclosed commitment, inserts into tree.
  • checkIn participant witness supplies sk + Merkle path; circuit verifies persistentHash(sk) matches the leaf and records the nullifier.

The participant (or your app) needs the Merkle path for their leaf after enrollment usually computed off-chain from tree state/indexer, then passed as a witness at check-in. Staff enrolls the commitment; participant proves membership at check-in.

IPFS + Midnight: Encrypted blob on Pinata/IPFS is fine. For audit integrity, anchor on-chain in DocumentRecord — at minimum contentHash + storageCid (CID alone isn’t PHI if the blob is encrypted). A separate DB/cache for Pinata URLs is OK for app UX, but treat the on-chain record as source of truth; the DB should mirror it, not replace it. If CID is only off-chain, you lose the tamper-evident link Midnight gives you.

Happy to hear more, use cases like this help the ecosystem. Share how Phase 2 goes, and congrats again on the capstone progress.

1 Like

@nasihudeenJ — this is exactly the clarity I needed, thank you.

The depth-10 tree making sense for our scale is helpful — I hadn’t thought about proof size being tied to tree depth. And the enrollment split makes the trust model much cleaner: staff handles the commitment, participant proves membership without ever exposing their secret. That separation is actually a great story for our participants too.

One thing I want to confirm before I start the Phase 2 upgrade: when the participant’s app computes the Merkle path off-chain from the indexer after enrollment, does that path need to be recomputed every time the tree grows (new enrollments), or is the path stable once the leaf is inserted?

I’ll share how Phase 2 goes. This use case — anonymous check-in for a federally-regulated substance use program — feels like a strong real-world example of what Midnight is built for. Appreciate the support.

Good question, Leaf index is fixed once enrolled, what changes as the tree grows is the root (and usually the path to the current root).

With HistoricMerkleTree, checkRoot can accept proofs against prior roots, so an enrollment-time path isn’t automatically useless after new enrollments that’s why we recommended Historic over plain MerkleTree.

In practice: store sk + leaf index (and commitment); before check-in, call pathForLeaf(index, leaf) on current contract state (indexer/app) and pass that as the witness.
That’s the pattern in our keeping-data-private docs. Re-fetching before each check-in is the simplest reliable approach even if historic roots could work in theory.
Looking forward to more updates on this