Skip to main content

Circuit Reference

Kosh compiles 6 circuits from rosca.compact. Each circuit is a ZK-provable state transition. Proofs are generated by the local proof server and verified by the Midnight node in ~6ms.

createCircle

Initializes the circle with immutable parameters. Called once by the organizer on deployment.

Inputs: amount: Uint<64>, cap: Uint<8>, rounds: Uint<8>, durationPerRound: Uint<64>

Logic:

  1. Assert circleStatus == OPEN (initial zero-value state)
  2. Publish circle parameters via disclose(amount), disclose(cap), etc.
  3. Set circleStatus = OPEN
  4. Initialize empty memberTree

Public output: Circle parameters on ledger. circleStatus = OPEN.

Privacy: None — all parameters are explicitly published.


joinCircle

A member joins the circle by committing their identity into the Merkle tree. Generates fresh secrets locally; only the commitment hash goes on-chain.

Witness inputs: memberSecret(), memberNonce()

Logic:

  1. Assert circleStatus == OPEN
  2. Assert memberCount < memberCap
  3. Compute commitment = persistentCommit(memberSecret(), memberNonce())
  4. Insert commitment into memberTree at index memberCount
  5. Increment memberCount
  6. disclose(commitment) — hash on-chain; secret stays local
  7. If memberCount == memberCap: set circleStatus = ROUND_IN_PROGRESS, set roundDeadline

Public output: Updated Merkle root, updated member count. Commitment hash on-chain.

Privacy guarantee: No information about who the member is leaks from the commitment.


contribute

A member contributes the fixed amount for the current round. Proves membership via Merkle path and prevents double-contribution via nullifier.

Witness inputs: memberSecret(), memberNonce(), memberMerklePath(), inCoin(), outCoin()

Logic:

  1. Assert circleStatus == ROUND_IN_PROGRESS
  2. Assert blockTimeLte(roundDeadline) — within round deadline
  3. Verify Merkle membership: merkleTreePathRoot(path, commitment) == memberTree.root
  4. Compute round nullifier: roundId = persistentHash(memberSecret() || currentRound)
  5. Assert !spentIdentifiers.member(roundId) — no double contribution
  6. Add roundId to spentIdentifiers
  7. Execute Zswap send(NIGHT_TOKEN, contributionAmount) — tokens to pool
  8. Increment contributionsThisRound
  9. If contributionsThisRound == memberCap: set circleStatus = PAYOUT_PENDING

Public output: New nullifier in spentIdentifiers, updated contribution count.

Privacy guarantee: The nullifier is unlinkable to the identity commitment. No information about which member contributed is published.


claimPayout

The designated recipient claims the full pool. Proves membership AND their specific leaf position matches the current round number.

Witness inputs: memberSecret(), memberNonce(), memberMerklePath(), payoutRecipient()

Logic:

  1. Assert circleStatus == PAYOUT_PENDING
  2. Assert !payoutClaimed
  3. Verify Merkle membership (same as contribute)
  4. Verify leafIndex(memberMerklePath()) == currentRound — correct payout round
  5. Execute Zswap receive(NIGHT_TOKEN, contributionAmount * memberCap) — full pool to recipient
  6. Set payoutClaimed = true
  7. Increment currentRound, reset contributionsThisRound = 0, reset payoutClaimed = false
  8. Set new roundDeadline = blockTime() + roundDuration
  9. If currentRound == roundCount: set circleStatus = COMPLETED
  10. Else: set circleStatus = ROUND_IN_PROGRESS

Public output: Pool balance reset, round counter incremented.

Privacy guarantee: The recipient's identity is not disclosed — only that someone at the correct leaf position claimed.


reportDefault

Reports a member who failed to contribute by the round deadline. Reveals only the defaulter's commitment hash.

Inputs: leafCommitment: Bytes<32> (public, provided by reporter)

Witness inputs: memberSecret(), memberMerklePath() (for the defaulter's leaf)

Logic:

  1. Assert circleStatus == ROUND_IN_PROGRESS
  2. Assert blockTimeGte(roundDeadline) — deadline has passed
  3. Assert contributionsThisRound < memberCap — not all contributed
  4. Verify leafCommitment exists in memberTree (Merkle proof of the defaulter)
  5. Compute expectedRoundId = persistentHash(candidateSecret || currentRound)
  6. Assert !spentIdentifiers.member(expectedRoundId) — confirms they did NOT contribute
  7. disclose(leafCommitment) — defaulter's commitment hash on-chain
  8. Set circleStatus = DEFAULT_DETECTED

Public output: Defaulter's commitment hash. Circle paused.

Privacy guarantee: Only the non-contributing member's commitment is revealed. All contributing members' identities remain private.


generateParticipationProof

Generates a portable proof of circle completion. Can be shared to prove participation without revealing identity or which circle.

Witness inputs: memberSecret(), memberNonce(), memberMerklePath()

Logic:

  1. Assert circleStatus == COMPLETED
  2. Verify Merkle membership
  3. Return persistentCommit(memberSecret(), currentRound) — a receipt bound to the member + completion

Returns: Bytes<32> receipt, stored on-chain.

Privacy guarantee: The receipt proves "I participated in a completed circle" without revealing which circle or who the member is.


Compilation Output

After compact compile +0.29.0 src/contracts/rosca.compact build/, the build/keys/ directory contains:

claimPayout.prover         claimPayout.verifier
contribute.prover contribute.verifier
createCircle.prover createCircle.verifier
generateParticipationProof.prover generateParticipationProof.verifier
joinCircle.prover joinCircle.verifier
reportDefault.prover reportDefault.verifier

Each .prover key is used by the proof server to generate proofs. Each .verifier key is used by the Midnight node to verify them.