Skip to content

BoPDA1607/Midnight_Projects

Multi-Post Bulletin Board DApp

This project is built on the Midnight Network.

Generic badge Generic badge

A Midnight smart contract example demonstrating a multi-post bulletin board with zero-knowledge proofs on testnet. Users can post multiple messages (up to MAX_POSTS), and only the respective message authors can remove their own posts.


Track A: Multi-Post Board - Design Write-up

Design Decisions

Why Map + postCounter Instead of Array?

The original bulletin board contract used a single message storage model. For the multi-post extension, we chose to use Map<Uint32, Post> combined with a postCounter instead of an Array for several important reasons:

  1. Efficient Deletion: A Map allows O(1) deletion of any post by its key (ID) without affecting other entries. With an Array, removing an element from the middle would require either:

    • Shifting all subsequent elements (expensive and breaks existing IDs)
    • Leaving "holes" in the array (wastes space and complicates iteration)
  2. Stable Post IDs: Each post gets a permanent, unique ID that never changes. This is crucial for the CLI user experience—users can reference posts by ID even after other posts are deleted.

  3. Sparse Storage: The Map naturally handles gaps when posts are deleted. We don't need to track which indices are "valid" vs "empty."

  4. Simple Counter Logic: The postCounter auto-increments for each new post, providing unique IDs without needing to search for the next available slot.

Why MAX_POSTS = 10?

The MAX_POSTS constant limits the number of concurrent posts for several reasons:

  1. Resource Constraints: Zero-knowledge circuits have computational limits. Unbounded data structures would make proof generation impractical.

  2. DoS Prevention: Without a limit, an attacker could flood the board with posts, potentially causing performance issues.

  3. Practical Use Case: A bulletin board typically doesn't need unlimited posts. Ten concurrent posts is sufficient for demonstration while keeping the circuit size manageable.

Compact Patterns Applied

Using disclose() for Public Data

The disclose() function in Compact explicitly marks data that should be written to the public ledger:

const newPost = Post {
  message: disclose(newMessage),
  owner: disclose(ownerPk)
};
posts.insert(disclose(postId), newPost);

Every piece of data visible on-chain is intentionally disclosed. This makes the privacy model explicit—developers must consciously choose what becomes public.

Witness Functions for Secret Keys

The localSecretKey() witness function retrieves the user's secret key from off-chain private state:

witness localSecretKey(): Bytes<32>;

This pattern ensures:

  • The secret key never appears in the transaction or on-chain
  • Only the derived public key (via publicKey()) is disclosed
  • Users can prove ownership without revealing their identity

Assert Statements for State Protection

assert statements guard against invalid state transitions:

assert(posts.size() < MAX_POSTS, "Maximum number of posts reached");
assert(posts.member(postId), "Post does not exist");
assert(existingPost.owner == callerPk, "Only the post owner can take down this post");

These assertions are enforced in the zero-knowledge proof—if any assertion fails, the proof cannot be generated, and the transaction is rejected. This provides cryptographic guarantees for access control.

Privacy Properties

What Is Public (On-Chain)

  1. Post Messages: The content of each post is fully public on the ledger. Anyone can read all active posts.

  2. Owner Public Keys: Each post stores a derived public key (Bytes<32>) that identifies the owner. This is a hash of [prefix, sequence, secretKey].

  3. Post Counter & IDs: The total number of posts ever created and each post's unique ID are public.

What Is Private (Off-Chain)

  1. Secret Keys: The actual secret key (localSecretKey) never leaves the user's device. It's only accessed through the witness function during proof generation.

  2. Ownership Linkage: While public keys are visible, linking them to real-world identities is computationally infeasible without the secret key. Each public key is derived using a secure hash, providing pseudonymous identity.

  3. Proof of Ownership: When a user takes down a post, the zero-knowledge proof demonstrates they possess the secret key corresponding to the owner's public key—without revealing the secret key itself.

Privacy Trade-offs

This design prioritizes transparency of content while maintaining identity privacy:

  • Anyone can see what's posted (public bulletin board semantics)
  • No one can impersonate another user without their secret key
  • Users can prove ownership without revealing their identity

For use cases requiring private messages, the contract could be modified to encrypt messages using asymmetric encryption, with only the intended recipient able to decrypt.


Project Structure

bulletin-board/
├── contract/               # Smart contract in Compact language
│   └── src/               # Contract source and utilities
├── api/                   # Methods, classes and types for CLI and UI
├── bboard-cli/            # Command-line interface
│   └── src/               # CLI implementation
└── bboard-ui/             # Web browser interface
    └── src/               # Web UI implementation

Prerequisites

1. Node.js Version Check

You need Node.js (tested with current LTS):

node --version

Expected output: v24.11.1 or higher.

If you get a lower version: Install Node.js LTS.

2. Docker Installation

The proof server runs in Docker and is required for both CLI and UI to generate zero-knowledge proofs:

docker --version

Expected output: Docker version X.X.X.

If Docker is not found: Install Docker Desktop. Make sure Docker Desktop is running.

3. Lace Wallet Extension (UI Only)

For the web interface, install the official Cardano Lace wallet extension on Chrome Store or the Edge Store (tested with version 1.36.0).

After installing, set up the Midnight wallet:

  1. Open the Lace wallet extension and go to Settings
  2. Enable the Beta Program to unlock Midnight network support
  3. Create a new wallet — Midnight will appear as a network option
  4. Go to Settings > Midnight and set Network to Preprod
  5. Set Proof server to Local (http://localhost:6300) — this must point to your local proof server started via Docker
  6. Click Save configuration
  7. Fund your wallet with tNIGHT tokens from the Preprod Faucet
  8. Go to Tokens in the wallet, click Generate tDUST, and confirm the transaction — tDUST tokens are required to pay transaction fees on preprod

Setup Instructions

Install Project Dependencies

# Install root dependencies
npm install

# Install API dependencies
cd api && npm install && cd ..

# Install contract dependencies and compile
cd contract && npm install

Compile the Smart Contract

The Compact compiler (v0.29.0) generates TypeScript bindings and zero-knowledge circuits from the smart contract source code:

npm run compact    # Compiles the Compact contract
npm run build      # Copies compiled files to dist/
cd ..

Expected output:

> compact
> compact compile src/bboard.compact ./src/managed/bboard

Compiling 2 circuits:
  circuit "post" (k=14, rows=10070)
  circuit "takeDown" (k=14, rows=10087)

> build
> rm -rf dist && tsc --project tsconfig.build.json && cp -Rf ./src/managed ./dist/managed && cp ./src/bboard.compact ./dist

Build the CLI Interface

cd bboard-cli
npm install
npm run build
cd ..

Build the UI Interface (Optional)

Only needed if you want to use the web interface:

cd bboard-ui
npm install
npm run build
cd ..

Option 1: CLI Interface

Run the CLI

# For preprod network
npm run preprod-remote

# For preview network
npm run preview-remote

Using the CLI

Create a Wallet

  1. Choose option 1 to build a fresh wallet
  2. The system will generate a wallet address and seed
  3. Save both the address and seed - you'll need them later

Expected output:

Your wallet seed is: [64-character hex string]
Using unshielded address: mn_addr_preprod1hdvtst70zfgd8wvh7l8ppp7mcrxnjn56wc5hlxpwflz3fxdykaesrw0ln4 waiting for funds...

Fund Your Wallet

Before deploying contracts, you need testnet tokens.

  1. Copy your wallet address from the output above
  2. Visit the faucet
  3. Paste your address and request funds
  4. Wait for the CLI to detect the funds (takes 2-3 minutes)

Expected output:

Your NIGHT wallet balance is: 1000000000

Deploy Your Contract

  1. Choose the contract deployment option
  2. Wait for deployment (takes ~30 seconds)
  3. Save the contract address for future use

Expected output:

Deployed bulletin board contract at address: [contract address]

Use the Bulletin Board

You can now:

  • Post a message to the bulletin board
  • View the current message
  • Remove your message (only if you posted it)
  • Exit when done

Each action creates a real transaction on Midnight Testnet using zero-knowledge proofs generated by the proof server.

Option 2: Web UI Interface

The web interface uses the same proof server and requires additional browser setup.

Start the Proof Server (if not already running)

If you haven't started the proof server for the CLI, start it now:

cd bboard-cli
docker compose -f proof-server-local.yml up -d

Verify it's running:

docker ps

Start the Web Interface

The UI can run against preprod or preview networks:

cd bboard-ui

# For preprod network
npm run build:start

# For preview network
npm run build:start:preview

The UI will be available at:

Browser Setup

  1. Open the UI URL in a browser with Lace wallet extension installed
  2. Set up Lace wallet if it's your first time
  3. Authorize the application when Lace wallet prompts
  4. Use the bulletin board web interface

Useful Links

Troubleshooting

Common Issue Solution
npm install fails Ensure you're using Node.js LTS version. If you get ERESOLVE errors, try npm install --legacy-peer-deps
Contract compilation fails Ensure you're in contract directory and run npm run compact
Network connection timeout CLI requires internet connection, restart if connection times out
Token funding takes too long Wait 1-2 minutes, funding is automatic in CLI
"Application not authorized" error Start proof server: docker compose -f proof-server-local.yml up -d
Lace wallet not detected Install Lace wallet browser extension and refresh page
Docker issues Ensure Docker Desktop is running, check docker --version
Port 6300 in use Run docker compose down then restart services
Dependencies won't install Use Node.js LTS version. For older npm versions, you may need --legacy-peer-deps
Contract deployment fails Verify wallet has sufficient balance and network connection

Notes

  • CLI and UI can run simultaneously and share the same proof server
  • Proof server (Docker) is required for both CLI and UI to generate zero-knowledge proofs
  • Contract must be compiled before building CLI or UI
  • Fund your wallet using the testnet faucet before deploying contracts

Repository Notes / Temporary Workarounds

This repository contains several workarounds required due to current limitations in upstream tooling and dependencies. Each item below documents a concrete deviation from the default or expected setup.

  • Modified testkit sources Some parts of midnight-testkit-js are vendored into this repository and modified to work correctly with the current setup.

  • Transaction fee configuration
    The default additionalFeeOverhead value (500_000_000_000_000_000n) from 'midnight-testkit-js' is required on the Undeployed network (lower values fail with BalanceCheckOverspend on the midnight-node side). On the Preview network, that high overhead prevents transaction creation because it requires a large amount of dust, so it is overridden and set to 1_000n. The root cause is not yet clear.

  • LevelDB private state provider
    The levelDbPrivateStateProvider, shipped with Node.js dependencies, does not work in browser environments. An in-memory private state provider is used instead; the implementation is copied from midnight-js.

  • Overall API Usage Some of the tooling used in midnight-testkit-js, midnight-js and midnight-wallet is not currently well suited for direct application use. Significant wiring and integration logic is required, parts of which are copied into this repository. More flexible and composable APIs would reduce the need for copying and modification, allowing consumers to extend functionality rather than patch or fork existing implementations.

About

Midnight DApp related

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors