The easiest way for me to understand the Dual State Model is I had to stop thinking about what data do I send to the chain and start thinking in terms of what fact does the chain need to verify. In a dual-state system, your application is split into two layers Private State and Public State. Think of the Private State as just that private- where sensitive data lives. This would be things like medical records, identity attributes, legal documents, internal business logic inputs. Because this is private state it is not published on-chain.
The Public State is public where the verifiable outcomes live. This would be like commitments, nullifiers, proof verifications, state transition markers, and authorization results. The public layer does not need the underlying private data. It only needs proof that the rules were satisfied.
Think of it this way:
Private state = your full working file
Public state = notarized proof that the file satisfies required conditions
Circuit = the legal standard for what counts as valid
Verifier = the public notary
Nullifier = the stamp that prevents duplicate use
A conventional app says:
input data → backend checks rules → database stores result
A dual-state app says:
private data → local proving logic checks rules → proof sent on-chain → chain verifies result`
So instead of trusting the backend to say “yes, this is valid,” the network verifies cryptographic proof that it is valid.
Let’s put this to the test on a basic example where you are the patient wants to prove the following:
- they are over 18
- they are a member
- they have not already claimed a benefit
without revealing:
- exact birthdate
- identity record
- membership file contents
Here what the architect looks like:
┌──────────────────────────┐
│ Private Client │
│--------------------------│
│ birthdate │
│ membership credential │
│ private witness data │
│ proof generation │
└─────────────┬────────────┘
│
│ generates proof
▼
┌──────────────────────────┐
│ Public Chain │
│--------------------------│
│ verification key │
│ commitment registry │
│ nullifier set │
│ proof verification │
└──────────────────────────┘
Public inputs are the values the chain is allowed to see and verify against.
Example public inputs:
current_date = 2026-04-17)
membership_commitment = H(member_id, membership_secret)
nullifier = H(member_id, "benefit-claim"
Why these are public:
current_date is needed for age comparison
membership_commitment links proof to an existing commitment
nullifier prevents double use without revealing identity
Define the Circuit
A circuit is the rule system the prover must satisfy.
Recall this circuit must prove:
the user is at least 18
the commitment matches a real membership secret
the nullifier is correctly derived
the claim has not been used already
Circuit logic in plain English plaintext
Given private witness:
birthdate
member_id
membership_secret`
Given public inputs:
current_date
membership_commitment
nullifier
Prove that:
age(birthdate, current_date) >= 18
H(member_id, membership_secret) == membership_commitment
H(member_id, "benefit-claim") == nullifier
What Developers Need to Design
When building with a dual-state architecture, developers usually need to define 4 things:
Private witness model: What data remains secret? These are the user attributes, financial data, legal records, and internal app state items.
Public verification model: What must be visible to verify correctness? Commitments, proofs, nullifiers, policy identifiers, and timestamps are some examples.
Circuit rules: What exactly must be proven? This would include value is in range, signature matches, state transition is valid, user belongs to a set, record satisfies compliance constraints.
On-chain verifier logic: What does the contract/network do after proof verification? Some examples are mint asset, approve action, update commitment, mark nullifier spent, and reject replay.
Practical Developer Mapping
Here is the easiest way to translate existing engineering instincts.
Traditional backend thinking
client sends raw data
server checks permissions
server checks rules
server writes result
Dual-State Thinking
client keeps raw data
client/prover computes proof
chain verifies proof
chain records only verifiable outcome
Common Data Structures
Commitment
A cryptographic representation of private data.
commitment = hash(private_data, randomness)
Purpose:
- bind proof to a specific hidden value
- allow later verification without revealing the value
Nullifier
A one-time-use marker derived from private identity material.
nullifier = hash(identity_secret, action_scope)
Purpose:
- prove correctness without exposing witness data
Full Flow Example
Here’s the end-to-end pattern in one block:
# Step 1: user has private state
private_state = { "birthdate": "2000-04-01",
"member_id": 847392,
"membership_secret": 928374928374
}
Step 2: derive public bindings
membership_commitment = hash(
private_state["member_id"],
private_state["membership_secret"]
)
nullifier = hash(
private_state["member_id"],
"benefit-claim"
)
public_inputs = {
"current_date": "2026-04-17",
"membership_commitment": membership_commitment,
"nullifier": nullifier
}
# Step 3: generate proof locally
proof = generate_zk_proof(
circuit="benefit_claim_circuit",
witness=private_state,
public_inputs=public_inputs
)
# Step 4: submit only proof + public inputs
transaction = {
"proof": proof,
"public_inputs": public_inputs
}
# Step 5: chain verifies
result = verify_and_execute(transaction)
if result:
print("Claim approved")
else:
print("Claim denied")
Here’s some things to consider when writing or thinking about the Circuit Design
What should remain private? Do not expose data just because it is available.
What fact actually needs verification? The proof target should be as narrow as possible.
What prevents replay? Usually a nullifier or spent-note mechanism.
What binds the proof to state? Usually commitments.
What is the public outcome? Approval, update, mint, transition, authorization, settlement.
Some Developer pitfalls to avoid:
Proving too much: Do not cram every business rule into one giant circuit unless necessary. Much better to keep circuits modular and prove only what matters for that transition.
Exposing too many public inputs If a value does not need to be public, do not publish it.
Forgetting replay protection A valid proof without nullifier logic can often be reused.
Confusing encryption with proof Encryption hides data. ZK proves facts about data. Those are not the same thing.
Treating on-chain state like a normal database The public side should store verification artifacts, not your whole private app model.
In conclusion the Dual State Model lets developers keep sensitive application state private while publishing cryptographic proof that state transitions are valid.
END
Visit Midnight’s Developer’s Hub! It connects you with everything required for engineering privacy with Midnight, from your first line of code to production. Use the Academy to get started, connect with Aliit Fellows for deep ecosystem knowledge and access technical documentation and open-source repos to deploy smart contracts. https://midnight.network/developer-hub
Top comments (0)