惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
Engineering at Meta
Engineering at Meta
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
博客园 - Franky
The GitHub Blog
The GitHub Blog
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
B
Blog RSS Feed
云风的 BLOG
云风的 BLOG
小众软件
小众软件
罗磊的独立博客
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
美团技术团队
H
Hackread – Cybersecurity News, Data Breaches, AI and More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
C
Check Point Blog
WordPress大学
WordPress大学
博客园 - 【当耐特】
博客园 - 司徒正美
D
Docker

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Mathematically Prohibiting 'Cheating' in On-Chain RPS: A ...
Haruki Kondo · 2026-05-04 · via DEV Community

Introduction

Implementing Rock-Paper-Scissors (RPS) on-chain is surprisingly tricky.

The moment you choose "Rock" and send a transaction, your opponent can read your move from the public ledger. The game is over before it even starts.

I tried implementing a commit-reveal pattern manually, but managing salts, preventing front-running, and ensuring fair judging logic... it quickly became a rabbit hole.

That's when I turned to Midnight. I built a full-stack RPS dApp where "cheating" (looking at the opponent's move before playing) is mathematically impossible!

In this article, I'll share the ZK circuit design using Midnight's smart contract language, Compact, and the hurdles I faced during development.

Note
What ZK protects: The ZK proof ensures "confidentiality of the move during the commit phase" (fairness). After the reveal, both moves are recorded on the public ledger.

Here is what the dApp looks like:

RPS dApp Hero

What You'll Learn

  • Designing the "Hidden" vs. "Visible" split in Midnight.
  • Mastering the commit-reveal pattern with the Compact language.
  • Frontend UX strategies for handling ZK proof generation time (the 10-second hurdle).
  • Robust provider configuration with the Midnight JS SDK.

Repository

You can find the full source code here:

midnight-rps-sample-app

Midnight RPS sample dApp

Overview

midnight-rps-sample-app is a sample Rock-Paper-Scissors dApp project built on Midnight, a privacy-focused blockchain.

Key Features

  • Fair Gameplay via Zero-Knowledge Proofs (ZK Proofs)
    The app utilizes a "commit/reveal" scheme powered by Compact smart contracts. Players commit to their move (Rock, Paper, or Scissors) by submitting a hashed value. Once all players have committed, the moves are revealed. This ensures a tamper-proof gaming experience where "sniping" or reacting to an opponent's move is impossible.

  • Powered by the Midnight Blockchain
    Contracts are deployed on the Midnight PreProd testnet, with all transactions recorded on-chain.

  • Full-Stack Architecture

    Package Role
    pkgs/contract Smart contracts written in the Compact language
    pkgs/cli CLI tools for contract deployment and interaction
    pkgs/app Frontend UI built with React + Vite
  • Lace Wallet Integration
    Connects with Lace Wallet via the @midnight-ntwrk/dapp-connector-api for secure signing and transaction processing.

Game Flow

  1. Commit Phase — Each player commits their move…

Quick Start

Here are the steps to get the app running locally.

To be honest, it takes about 15-20 minutes including ZK circuit compilation and the initial Docker image pull. Get your coffee ready!

Environment

Tested with the following:

Docker version 27.4.0
compact 0.2.0
bun 1.3.13
node 23.3.0

Enter fullscreen mode Exit fullscreen mode

Install Lace Wallet to your browser

If you have not yet installed Lace Wallet, you must go to below page & need to install Lace Wallet

Lace | The light wallet platform to explore Web3

Discover the new light wallet platform from Input Output Global. Manage digital assets, and access NFTs, DApps, and DeFi services. Start your Web3 journey.

favicon lace.io

Next, you need to create wallet account of Midnight

Please switch to PreProd Network

0. Clone the Repository

git clone https://github.com/mashharuki/midnight-rps-sample-app.git

Enter fullscreen mode Exit fullscreen mode

1. Install Dependencies

bun install

Enter fullscreen mode Exit fullscreen mode

2. Compile and Build Contracts

# Generate ZK circuit assets (This takes the most time)
bun contract compact  

# Build CLI and Frontend
bun cli build
bun app build

Enter fullscreen mode Exit fullscreen mode

The bun contract compact step is where the Compact compiler generates the ZK proving and verification keys.

3. Start the Proof Server (Initial Pull: ~3 mins)

We need to run the server that handles ZK proof generation via Docker.

Without this server, you won't be able to deploy contracts or send commit/reveal transactions.

Version 8.0.3 is verified with Compact 0.2.0.
Ensure your SDK and Proof Server versions match, or proof generation will fail.

docker run -d -p 127.0.0.1:6300:6300 midnightntwrk/proof-server:8.0.3 midnight-proof-server

Enter fullscreen mode Exit fullscreen mode

4. Deploy Contract to PreProd Network

If you don't have testnet NIGHT Token, you can get some token from below site.

https://faucet.preprod.midnight.network/

bun cli preprod-pts

Enter fullscreen mode Exit fullscreen mode

If you deploy successfull, contract address shows like below.

[19:24:25.997] INFO (40223): Deploying RPS contract...
  ⠇ Deploying RPS contract[19:24:51.416] INFO (40223): Deployed RPS contract at: 23149945fed06aa010cc3e48e9f5df91625567300fae4e09371bb788d07a6bd8
  ✓ Deploying RPS contract
  Contract deployed at: 23149945fed06aa010cc3e48e9f5df91625567300fae4e09371bb788d07a6bd8

Enter fullscreen mode Exit fullscreen mode


The Essence of Midnight Architecture: "Localizing" Information

Developing with Midnight requires a different mindset compared to Solidity.

As mentioned in my previous articles, Midnight has two types of states:

  • Public State: Visible to everyone (on-chain ledger).
  • Private State: Visible only to you (local storage).

The Compact ZK circuits act as the bridge between these two.

App Architecture


Code Deep Dive: Mathematically Preventing Cheating

1. Security Core: Domain Separation

When deriving a public key from a secret key, Midnight recommends using domain separation:

pure circuit derive_pk(sk: Bytes<32>): Bytes<32> {
  // Domain separation with a fixed string "rps:pk:v1"
  return persistentHash<Vector<2, Bytes<32>>>([pad(32, "rps:pk:v1"), sk]);
}

Enter fullscreen mode Exit fullscreen mode

If you use the secret key as-is, using the same key in another app would result in the same public key, risking data leakage. By adding a prefix like "app name + version", we ensure that the public keys derived from the same secret key are unique to each app.

2. The Commit-Reveal Circuits

Commit Phase (Declaring a move while keeping it hidden)

export circuit commit(): [] {
  assert(!game_over,                 "Game is already over");
  assert(state == GameState.waiting, "Not in waiting state");

  const sk         = local_secret_key(); // witness: private input
  const pk         = derive_pk(sk);
  const my_move    = get_my_move();
  const my_salt    = get_my_salt();
  const commitment = make_commit(my_move, my_salt);
  store_move_and_salt(my_move, my_salt); // ⭐ Save move and salt to private state

  if (!p1_joined) {
    p1_key    = disclose(pk);
    p1_commit = disclose(commitment);
    p1_joined = true;
  } else {
    assert(!p2_joined, "Both players already committed");
    p2_key    = disclose(pk);
    p2_commit = disclose(commitment);
    p2_joined = true;
    state     = GameState.committed; // Transition to 'committed' when both joined
  }
}

Enter fullscreen mode Exit fullscreen mode

Pay close attention to store_move_and_salt.

This is a witness function that saves the selected move and salt to the browser's IndexedDB (private state). To prove that the move revealed later is the same one committed earlier, this step is absolutely necessary.

The use of disclose() is also critical. It allows developers to explicitly control which results of private computations are written to the public ledger.

Game State Transitions

The coordination between commit and reveal phases is managed by these states:

State Transitions

3. Reveal Phase: Where ZK Proofs Shine

If the commit is the "declaration," the reveal is the "proof."

This is where Midnight's ZK stack really pays off.

export circuit reveal(): [] {
  assert(!game_over,                   "Game is already over");
  assert(state == GameState.committed, "Not in committed state");

  const sk      = local_secret_key();
  const pk      = derive_pk(sk);
  const my_move = get_my_move();   // Restore from private state
  const my_salt = get_my_salt();   // Restore from private state
  const computed = make_commit(my_move, my_salt);

  const is_p1 = disclose(p1_key == pk);
  const is_p2 = disclose(p2_key == pk);
  assert(is_p1 || is_p2, "Caller is not a registered player");

  if (is_p1) {
    assert(!p1_revealed, "Player 1 already revealed");
    assert(disclose(computed == p1_commit), "Commitment mismatch for P1");
    p1_move     = disclose(my_move); // Move is written to ledger only now
    p1_revealed = true;
  }
  if (is_p2) {
    assert(!p2_revealed, "Player 2 already revealed");
    assert(disclose(computed == p2_commit), "Commitment mismatch for P2");
    p2_move     = disclose(my_move);
    p2_revealed = true;
  }

  if (disclose(p1_revealed && p2_revealed)) {
    result    = who_wins(p1_move, p2_move); // Judge winner
    game_over = true;
    state     = GameState.finished;
  }
}

Enter fullscreen mode Exit fullscreen mode

The line assert(disclose(computed == p1_commit), "Commitment mismatch for P1") is the heart of the logic:

  • my_move and my_salt exist only in your local private state.
  • make_commit(my_move, my_salt) is re-calculated, and the ZK circuit verifies it matches the p1_commit recorded on-chain during the commit phase.
  • You mathematically prove that "this move is the one I committed to" without revealing the move itself until the proof is valid.

Once the reveal is successful, p1_move and p2_move are finally written on-chain, and who_wins() determines the outcome.

During the commit phase, your move is hidden by ZK and remains unknown to the opponent until it is revealed. This is the mechanism that "mathematically prohibits cheating."


Overcoming the ZK dApp UX Hurdle

The biggest challenge during implementation was handling the "ZK proof generation time."

ZK Proof Loading

Filling the 10-Second Silence

In Midnight, before sending a transaction, you must generate a proof on your machine (or the Proof Server). This takes about 5-10 seconds.

UX improvements are essential to prevent users from thinking the app has frozen:

  • Optimistic UI Updates: Switch the UI state to "Generating Proof..." as soon as the process starts to communicate clearly what is happening.
  • Persistent Providers: Use levelPrivateStateProvider to ensure that even if the user reloads the browser during proof generation, the generated data and chosen moves are not lost.

Waiting for Opponent

// Example levelPrivateStateProvider configuration
privateStateProvider: levelPrivateStateProvider({ 
  accountId, 
  namespace: "rpsPrivateState", // Separate namespace for each game
  privateStoragePasswordProvider: () => storagePassword // Secure storage
}),

Enter fullscreen mode Exit fullscreen mode


Sequence: From Commit to Game End

Let's recap the full flow:


Remaining Challenges

There's still a lot to learn by building with Midnight and Compact. Here are some issues I'm still working on:

1. The Abandonment Problem (Griefing Attack)

If Player 1 commits and Player 2 never joins, the contract stays locked. I need to implement a timeout/cancel mechanism in a future update.

2. Version Management of Build Assets

ZK circuit assets (proving keys) generated by bun contract compact can become invalid with even minor updates to the Compact version or compiler.

I actually ran into this during development. If you're building a similar app, double-check your library and Proof Server versions!

Solving these two will open up even more possibilities for ZK dApps on Midnight.


Conclusion

Implementing this "fair" RPS app taught me three key lessons:

  1. Verify Versions: Proof generation can fail due to mismatched Proof Server versions.
  2. Don't Forget store_move_and_salt: Saving private state is mandatory for the reveal phase.
  3. UX is Priority #1: Designing a UI that handles the unique "waiting time" of ZK is critical.

Next, I'm planning to tackle multi-round support and timeout handling!

If you're interested in Midnight's privacy tech, check out the repository and my other technical blogs!

Thanks for reading!

Follow me on X!


References