Skip to main content

Threat Model

Trusted Components

ComponentTrust LevelReason
Compact ZK circuitsTrustedCompiled to formally verified Halo2 SNARKs
Midnight Network consensusTrustedDecentralized BFT consensus
User's local machineTrustedWitness functions execute locally; secrets stay local
Web Crypto APITrustedOS-level CSPRNG for secret generation

Untrusted Components

ComponentTrust LevelConcern
OrganizerSemi-trustedHolds off-chain commitment → identity mapping
Indexer GraphQLUntrusted for privacyReads public ledger only; no secrets transmitted
Other circle membersUntrustedCan see on-chain state but not individual identities
Proof server (Docker)Untrusted for dataReceives circuit inputs; secrets must not be transmitted in future hosted deployments

Attack Analysis

Passive On-Chain Observer

Goal: Identify which member made which contribution.

Available data: Merkle tree of commitments, set of nullifiers, pool balance updates.

Result: Fails. Nullifiers and commitments use different hash constructions. Linking them requires inverting a one-way Poseidon function — computationally infeasible.


Malicious Organizer

Goal: Deanonymize all members.

Available data: The organizer may know which commitments belong to which members (they may have facilitated the off-chain joining process and seen which wallet submitted which join transaction).

Result: Partial. The organizer can link a commitment to a real-world identity. They cannot determine which member produced which round nullifier (because nullifiers use persistentHash(secret || round), not the commitment, and the secret is never transmitted).

Mitigation: Use a trustless onboarding mechanism, or accept this as a known assumption (social trust in the organizer, as in traditional ROSCAs). A future upgrade can implement an optional organizer-encrypted member record.


Member Collusion Attack

Goal: Multiple members collude to identify who contributed or defaulted in a given round.

Available data: Their own secrets, on-chain nullifiers, on-chain commitments.

Result: Limited. Colluding members know their own (secret, nonce) pairs and can verify each other's commitments. But they cannot determine other members' secrets from public on-chain data alone — they would need to know the target's secret.

Mitigation: Standard membership limits (≤16 members) limit the attack surface. ZK proofs remain valid regardless of colluding members.


Timing Analysis

Goal: Link contribution timing to identity (e.g., "member X always contributes within 5 minutes of a round opening").

Risk level: Medium. If members contribute in highly predictable patterns (always first, always last), timing may leak partial information.

Mitigation: Encourage members to use randomized contribution timing within the round window. Future UI can add a "random delay" option.


Replay Attack

Goal: Submit a previously valid contribution proof again to double-claim.

Result: Fails. The nullifier (roundId) is added to spentIdentifiers on first use. Resubmitting the same proof would fail assert !spentIdentifiers.member(roundId) — the transaction is rejected.


Double-Contribution Attack

Goal: Contribute twice in the same round to drain the pool or disrupt the round.

Result: Fails. The nullifier is deterministic per (member, round) pair. The second attempt produces the same roundId which is already in spentIdentifiers. The assertion fails and the proof is rejected.


Non-Member Contribution Attack

Goal: Contribute without being a registered member to steal from the pool or disrupt a round.

Result: Fails. The contribution circuit requires a valid Merkle membership proof. Without knowing a valid (secret, nonce) pair that maps to a leaf in the tree, the proof cannot be generated. The prover cannot forge a valid ZK proof.


Wrong-Round Payout Claim

Goal: Claim the payout in a round that isn't yours (stealing someone else's payout).

Result: Fails. The claimPayout circuit verifies leafIndex(memberMerklePath()) == currentRound. The proof fails if the member's position doesn't match the current round. No other member's proof would validate for a different round.