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:
- Assert
circleStatus == OPEN(initial zero-value state) - Publish circle parameters via
disclose(amount),disclose(cap), etc. - Set
circleStatus = OPEN - 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:
- Assert
circleStatus == OPEN - Assert
memberCount < memberCap - Compute
commitment = persistentCommit(memberSecret(), memberNonce()) - Insert commitment into
memberTreeat indexmemberCount - Increment
memberCount disclose(commitment)— hash on-chain; secret stays local- If
memberCount == memberCap: setcircleStatus = ROUND_IN_PROGRESS, setroundDeadline
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:
- Assert
circleStatus == ROUND_IN_PROGRESS - Assert
blockTimeLte(roundDeadline)— within round deadline - Verify Merkle membership:
merkleTreePathRoot(path, commitment) == memberTree.root - Compute round nullifier:
roundId = persistentHash(memberSecret() || currentRound) - Assert
!spentIdentifiers.member(roundId)— no double contribution - Add
roundIdtospentIdentifiers - Execute Zswap
send(NIGHT_TOKEN, contributionAmount)— tokens to pool - Increment
contributionsThisRound - If
contributionsThisRound == memberCap: setcircleStatus = 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:
- Assert
circleStatus == PAYOUT_PENDING - Assert
!payoutClaimed - Verify Merkle membership (same as contribute)
- Verify
leafIndex(memberMerklePath()) == currentRound— correct payout round - Execute Zswap
receive(NIGHT_TOKEN, contributionAmount * memberCap)— full pool to recipient - Set
payoutClaimed = true - Increment
currentRound, resetcontributionsThisRound = 0, resetpayoutClaimed = false - Set new
roundDeadline = blockTime() + roundDuration - If
currentRound == roundCount: setcircleStatus = COMPLETED - 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:
- Assert
circleStatus == ROUND_IN_PROGRESS - Assert
blockTimeGte(roundDeadline)— deadline has passed - Assert
contributionsThisRound < memberCap— not all contributed - Verify
leafCommitmentexists inmemberTree(Merkle proof of the defaulter) - Compute
expectedRoundId = persistentHash(candidateSecret || currentRound) - Assert
!spentIdentifiers.member(expectedRoundId)— confirms they did NOT contribute disclose(leafCommitment)— defaulter's commitment hash on-chain- 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:
- Assert
circleStatus == COMPLETED - Verify Merkle membership
- 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.