HiddenCanvas
HiddenCanvas is a privacy-first pixel canvas. Players paint a 5x5 grid, encrypt selected cell IDs with Zama FHE, and store ciphertexts on-chain. Owners can later fetch and decrypt their drawings locally to reveal the image.
- Overview
- Problems solved
- Key advantages
- Core features
- How it works
- Tech stack
- Repository structure
- Setup and prerequisites
- Contracts: local development
- Contracts: Sepolia deployment
- Frontend app
- ABI and address sync
- Tasks reference
- Testing
- Limitations and design choices
- Roadmap
- License
HiddenCanvas turns a tiny grid into a private drawing space. The chain stores only encrypted cell IDs, not the plaintext image. The front end handles encryption and decryption so the user controls when the drawing is revealed.
- Public chains expose raw drawing data by default; HiddenCanvas keeps the image confidential.
- Typical on-chain pixel art requires a lot of storage; HiddenCanvas stores compact encrypted handles.
- Users need proof of ownership and persistence; the contract keeps per-owner canvas data and timestamps.
- Privacy-preserving UX is hard to explain; this project offers an end-to-end FHE flow with tasks and UI.
- Private by design: plaintext cell IDs never touch the chain.
- User-controlled reveal: decryption happens locally on demand.
- Minimal on-chain footprint: only encrypted handles and small metadata are stored.
- Simple mental model: 5x5 grid, IDs 1-25, and a single owner per canvas.
- View methods are owner-address based and do not rely on msg.sender in view functions.
- Create or reset a canvas on-chain.
- Encrypt a list of cell IDs (1-25) using the Zama relayer.
- Store encrypted cell handles on-chain with timestamps.
- Fetch encrypted handles and metadata for any owner.
- Decrypt locally to render the final image in the UI.
- A player creates a canvas with
createCanvas. - The UI maps selected grid cells to IDs 1-25.
- The frontend uses the Zama relayer to encrypt those IDs.
- The contract stores the encrypted handles via
saveDrawing. - Anyone can read the encrypted handles with
getCanvasorgetCell. - The owner decrypts the handles locally and renders the grid.
Privacy model notes:
- The contract only sees encrypted values and cannot validate ID ranges or uniqueness.
- The owner address is allowed to decrypt by the FHE permissions applied on-chain.
- Decryption happens client-side; no plaintext drawing is written to the chain.
- Smart contracts: Solidity + Hardhat + hardhat-deploy
- FHE: Zama FHEVM libraries and relayer
- Frontend: React + Vite
- Web3: ethers (writes), viem (reads), RainbowKit (wallet UX)
- Docs:
docs/zama_llm.mdanddocs/zama_doc_relayer.md
contracts/ Solidity contracts
deploy/ Hardhat deploy script
deployments/ Network deployment artifacts (ABI source of truth)
tasks/ Hardhat tasks for CLI flows
test/ Hardhat tests
app/ React frontend
docs/ Zama integration docs
- Node.js and npm installed
- A funded wallet for Sepolia deployments
- Zama relayer access configured per
docs/zama_doc_relayer.md - No frontend environment variables are used or required
Install root dependencies:
npm install
Compile and test first:
npm run compile
npm test
Deploy locally:
npm run deploy:localhost
Example tasks (local):
npx hardhat task:create-canvas --network hardhat
npx hardhat task:save-drawing --cells 1,5,12 --network hardhat
npx hardhat task:decrypt-canvas --network hardhat
Sepolia deploys must use a private key, not a mnemonic. Create a .env in the repo root:
PRIVATE_KEY= # Sepolia deployer (required)
INFURA_API_KEY=
ETHERSCAN_API_KEY=
Then run:
npm run deploy:sepolia
Deployment artifacts are written to deployments/ and include
deployments/sepolia/HiddenCanvas.json, which is the ABI source of truth for the frontend.
The frontend targets Sepolia and does not use localhost networks, local storage, or environment variables.
Install and run:
cd app
npm install
npm run dev
Build:
npm run build
The UI lets users create a canvas, select cells, encrypt via the relayer, save on-chain, fetch encrypted cells, and decrypt locally to render the grid.
The frontend must always use the ABI generated by the deployed contract.
- Deploy to Sepolia.
- Copy the ABI from
deployments/sepolia/HiddenCanvas.json. - Paste it into
app/src/config/contracts.ts. - Update
CONTRACT_ADDRESSinapp/src/config/contracts.ts.
Do not import JSON in the frontend. Keep the ABI inline in TypeScript as shown.
task:canvas-addressprints the deployed contract address.task:create-canvascreates or resets a canvas.task:save-drawingencrypts a CSV list of IDs and saves it.task:decrypt-canvasfetches and decrypts stored cells.
Run a task:
npx hardhat task:canvas-address --network sepolia
Run the full test suite:
npm test
Tests cover the on-chain canvas lifecycle and the encrypted data flow.
- The grid is fixed at 5x5 with IDs 1-25.
- Cell IDs are encrypted, so the contract cannot validate ranges or uniqueness.
- Only the list of painted cell IDs is stored, not colors or brush metadata.
- Decryption requires the owner wallet and Zama relayer access.
- The frontend intentionally avoids local storage and environment variables.
- Add color or weight metadata with encrypted values.
- Support multiple layers or frames per canvas.
- Add shareable reveal links that still keep ciphertext on-chain.
- Add pagination and history for multiple saved drawings.
- Improve UX for relayer configuration and failure recovery.
BSD 3-Clause Clear License. See LICENSE.