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

推荐订阅源

P
Proofpoint News Feed
V
Visual Studio Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Threatpost
TaoSecurity Blog
TaoSecurity Blog
Engineering at Meta
Engineering at Meta
T
Troy Hunt's Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
H
Heimdal Security Blog
Webroot Blog
Webroot Blog
A
About on SuperTechFans
S
Securelist
Recorded Future
Recorded Future
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
阮一峰的网络日志
阮一峰的网络日志
S
SegmentFault 最新的问题
P
Palo Alto Networks Blog
F
Fortinet All Blogs
Hacker News: Ask HN
Hacker News: Ask HN
WordPress大学
WordPress大学
W
WeLiveSecurity
N
Netflix TechBlog - Medium
博客园 - 叶小钗
宝玉的分享
宝玉的分享
大猫的无限游戏
大猫的无限游戏
G
GRAHAM CLULEY
Schneier on Security
Schneier on Security
博客园 - 聂微东
www.infosecurity-magazine.com
www.infosecurity-magazine.com
小众软件
小众软件
博客园 - 【当耐特】
有赞技术团队
有赞技术团队
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
A
Arctic Wolf
C
CXSECURITY Database RSS Feed - CXSecurity.com
Google DeepMind News
Google DeepMind News
Security Latest
Security Latest
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threat Research - Cisco Blogs
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Spread Privacy
Spread Privacy
罗磊的独立博客
The Hacker News
The Hacker News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
IT之家
IT之家
B
Blog
GbyAI
GbyAI
Hugging Face - Blog
Hugging Face - Blog
Google Online Security Blog
Google Online Security Blog
MongoDB | Blog
MongoDB | Blog

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 Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Security Checklist for Midnight dApps Before Deployment
Arihant Agar · 2026-05-11 · via DEV Community

A Midnight DApp can pass local unit tests and still leak data, accept replayed actions, or fail proof generation in the target environment. The risk comes from the parts that make Midnight powerful: selective disclosure, witness functions, private state, ZK proofs, and a toolchain that spans Compact, generated JavaScript, Midnight.js, the wallet SDK, the indexer, and the proof server.

This checklist gives you a repeatable predeployment review. It focuses on seven release gates:

  1. Audit disclose() calls for accidental secret leaks.
  2. Review ownPublicKey() usage as a known unsafe authorization pattern.
  3. Verify replay protection with nonces, counters, consumed state, or nullifiers.
  4. Review every exported ledger field.
  5. Validate witness implementations.
  6. Confirm version compatibility across the Midnight stack.
  7. Generate proofs and submit transactions on Testnet before release.

Use this checklist before every public deployment, after every dependency upgrade, and before publishing examples for other developers to copy.

This tutorial is for developers who build Midnight DApps with Compact smart contracts, generated JavaScript artifacts, Midnight.js packages, wallet integration code, and a proof server.

Prerequisites

You need:

  • A Midnight project with Compact source files.
  • Node.js 22.x or later.
  • Docker for the proof server.
  • jq for reading generated metadata.
  • Access to the target Midnight Testnet environment.
  • A lockfile generated by npm ci, pnpm install --frozen-lockfile, or the equivalent package manager command.

Required reading:

Understand the security boundary

A Compact smart contract spans three contexts:

  1. Public ledger state: On-chain state that observers, indexers, and DApps can read.
  2. ZK circuits: Compact logic that proves valid execution without revealing private inputs.
  3. Local witnesses: TypeScript or JavaScript callbacks that run on the user's machine and supply private values.

Most Midnight bugs appear at the boundary between those contexts. The compiler helps prevent accidental disclosure, but it cannot decide whether a disclosure is safe for your logic or make a witness implementation trustworthy.

Release rule

Treat privacy as a release blocker. A working UI is not enough. A safe Midnight release needs clear evidence that the Compact smart contract discloses only intended values, validates every witness-derived value, rejects replays, exposes only reviewed public state, runs compatible versions, and proves the full flow against the target Testnet environment.

Create a release issue with these sections before you start:

Gate 1: Audit disclose() calls

The disclose() wrapper is an explicit statement that a value derived from witness data, exported circuit arguments, or constructor arguments is safe to place in a public context. Public contexts include public ledger assignments, values returned from exported circuits, and values passed to another smart contract.

Midnight supports selective disclosure. Compact keeps witness-derived values private unless the code explicitly marks a value for public exposure. That protection helps only when every disclose() call receives a human review.

List every disclose() call:

grep -RIn "disclose[[:space:]]*(" contract/src src contract 2>/dev/null

Enter fullscreen mode Exit fullscreen mode

For every result, answer four questions:

  1. What exact value becomes eligible for public exposure at this line?
  2. Can this value link two user actions that should stay unlinkable?
  3. Does the value include a secret, salt, nonce, private key, seed, or raw witness value?
  4. Is disclose() placed as close as possible to the public write or return?

A safe disclosure usually exposes a derived public value, not a raw private input. Use persistent primitives for values stored in ledger state or compared in later transactions. Use domain separation for every hash or commitment purpose. Use fresh randomness for commitments, and never reuse a salt across commitments.

Build a disclosure table in the release issue:

| File | Line | Disclosed value | Public destination | Reason | Reviewer | Status |
|---|---:|---|---|---|---|---|
| contract/src/main.compact | 42 | owner commitment | exported ledger field | Lets future calls prove ownership without revealing the secret | Ana | approved |

Enter fullscreen mode Exit fullscreen mode

Block the release when a disclosure exposes a raw secret, early-discloses a value before branching, persists a transient digest, or uses a missing domain separator.

Gate 2: Remove unsafe ownPublicKey() authorization

Review every use of ownPublicKey() as a release blocker. ownPublicKey() is witness-backed. A frontend supplies witness values off-chain, so the smart contract must not use ownPublicKey() as the first proof that the caller has authority.

Run this search:

grep -RIn "ownPublicKey[[:space:]]*(" contract/src src contract 2>/dev/null

Enter fullscreen mode Exit fullscreen mode

Classify each match:

  • Blocker: ownPublicKey() gates admin actions, ownership checks, minting, withdrawals, upgrades, or any privileged circuit.
  • High risk: ownPublicKey() writes a public owner or account value that later gates access.
  • Low risk: documentation or tests that assert the forbidden pattern fails review.

Prefer a proof-of-secret pattern. Store a derived public authorization value and validate future actions by recomputing the same value inside circuit logic from witness-supplied private material. Rotate the public value with a counter or consumed state when linkability matters.

If a dependency implements ownership, inspect the generated Compact API and the source. Do not assume that a package name proves the authorization path is safe.

Gate 3: Verify replay protection

A valid proof can still be unsafe when the same private action works twice. Review every circuit that consumes private state, changes ownership, moves value, records a vote, claims a reward, accepts an invitation, or updates an authorization path.

Search for replay-related state:

grep -RIn "Counter\|nonce\|nullifier\|sequence\|round\|persistentHash\|persistentCommit" contract/src src contract 2>/dev/null

Enter fullscreen mode Exit fullscreen mode

For each state-changing private action, identify the replay defense:

  • A consumed nullifier for one-time private resources.
  • A monotonic counter or sequence value for ordered actions.
  • A spent marker or consumed state transition.
  • A domain-separated commitment with fresh randomness.
  • A ledger update that makes the previous proof invalid.

A nullifier that omits the action domain can collide across flows. A commitment that reuses randomness can link actions. A nonce that is generated but never checked does not protect anything.

Add a transaction-level replay test. The following self-contained test shows the assertion shape. Replace the NullifierSet stub with your real Midnight transaction call in the integration suite.

import assert from "node:assert/strict";
import test from "node:test";

class NullifierSet {
  #seen = new Set();

  consume(nullifier) {
    if (this.#seen.has(nullifier)) throw new Error("replay detected");
    this.#seen.add(nullifier);
    return { status: "APPLIED_TO_CHAIN" };
  }
}

test("rejects a replayed private action", () => {
  const guard = new NullifierSet();
  const nullifier = "note:claim:7c5be3";

  assert.deepEqual(guard.consume(nullifier), { status: "APPLIED_TO_CHAIN" });
  assert.throws(() => guard.consume(nullifier), /replay detected/);
});

Enter fullscreen mode Exit fullscreen mode

Run the standalone template with Node:

node --test replay-guard.test.mjs

Enter fullscreen mode Exit fullscreen mode

Keep replay tests at the transaction level. Circuit-only tests can miss finalization bugs, public state mistakes, and wallet integration problems.

Gate 4: Review exported ledger fields

An exported ledger field is public API. Observers, indexers, tests, and other tools can read it. Generated metadata can also describe public ledger layout, so treat exported fields like response fields in a public service.

List exported fields:

grep -RIn "^export[[:space:]]\+ledger" contract/src src contract 2>/dev/null

Enter fullscreen mode Exit fullscreen mode

Inspect generated metadata after compilation:

find . -name contract-info.json -print
find . -name contract-info.json -exec sh -c 'echo "### $1"; jq "." "$1"' sh {} \;

Enter fullscreen mode Exit fullscreen mode

For each exported ledger field, document:

  • Name and type.
  • Why external readers need it.
  • Whether it reveals timing, balances, roles, votes, relationships, or user behavior.
  • Whether a sealed field is more appropriate for constructor-only configuration.
  • Whether a non-exported ledger field is enough.

Use sealed fields for values that must be initialized during deployment and stay immutable. Use non-exported ledger fields for state that circuits need but external readers do not. Use counters for state that benefits from reduced transaction dependencies.

A safe release has no surprise exports. If the UI does not need a field, the indexer does not need it, and no external integration depends on it, remove export.

Gate 5: Validate witness implementations

Witness functions run off-chain in the DApp. They supply private data to Compact circuits, but a witness return value is not cryptographically trustworthy by itself. Circuit logic must validate every witness value before it affects authorization or public state.

Review witness declarations:

grep -RIn "^witness\|^[[:space:]]*witness" contract/src src contract 2>/dev/null

Enter fullscreen mode Exit fullscreen mode

Find matching TypeScript or JavaScript implementations:

grep -RIn "witnesses\|localSecretKey\|privateState\|witness" src test ui contract 2>/dev/null

Enter fullscreen mode Exit fullscreen mode

Check these items:

  1. The witness returns the expected type and length.
  2. The witness never returns an all-zero secret outside a test fixture.
  3. Random salts and nonces come from cryptographic randomness.
  4. Private state updates happen in the same transaction flow that consumes the witness.
  5. Circuit assertions validate the witness before public writes.
  6. Error messages do not reveal which private condition failed.

Use this helper for Node-based fixtures and tests:

import { randomBytes } from "node:crypto";

export function randomBytes32(label) {
  const value = randomBytes(32);
  if (value.length !== 32) throw new Error(`${label} must be 32 bytes`);
  if (value.every((byte) => byte === 0)) throw new Error(`${label} must not be all zeros`);
  return value;
}

Enter fullscreen mode Exit fullscreen mode

Run this environment check:

node --input-type=module -e 'import { randomBytes } from "node:crypto"; const v = randomBytes(32); if (v.length !== 32) process.exit(1); console.log(v.length)'

Enter fullscreen mode Exit fullscreen mode

Expected output:

32

Enter fullscreen mode Exit fullscreen mode

For sensitive authorization checks, use generic errors such as Not authorized. Avoid messages that reveal which private condition passed.

Gate 6: Confirm version compatibility

A Midnight build spans multiple layers: Compact devtools, the Compact toolchain, Compact runtime, Compact JS, Platform JS, Midnight.js, the wallet SDK facade, the DApp Connector API, the indexer, the proof server, the ledger, and the on-chain runtime. A mismatch can compile locally and fail during proof generation or deployment.

Check the official compatibility matrix on the day you deploy. At the time this tutorial was prepared, the latest tested public matrix lists Compact devtools 0.5.1, Compact toolchain 0.31.0, Compact runtime 0.16.0, Compact JS 2.5.0, Platform JS 2.2.4, Midnight.js 4.0.4, testkit-js 4.0.4, DApp Connector API 4.0.1, Ledger 8.0.3, and proof server 8.0.3. Treat these numbers as a snapshot, not permanent constants.

Lock exact package versions for deployment builds. Avoid ^ and ~ in release branches.

{
  "scripts": {
    "compact": "compact compile src/main.compact src/managed/main",
    "build": "tsc -p tsconfig.json",
    "test": "node --test test/*.test.mjs",
    "audit:midnight": "node scripts/midnight-predeploy-audit.mjs contract/src --no-fail"
  }
}

Enter fullscreen mode Exit fullscreen mode

Run these checks in CI:

npm ci
node --version
npm --version
compact compile --version
compact compile --language-version
compact compile --ledger-version
compact compile --runtime-version
npm ls --depth=0 2>/dev/null | grep "@midnight-ntwrk" || true
npm run compact
npm run build
npm test

Enter fullscreen mode Exit fullscreen mode

If compact compile reports a different language, ledger, or runtime version from the reviewed matrix, stop and run a migration review. Regenerate artifacts after every toolchain update. Generated code is a release artifact, not a cache to reuse forever.

Gate 7: Test proof generation on Testnet

A local unit test does not prove the full DApp flow. Before release, generate real ZK proofs and submit transactions against the target Testnet environment. The proof server receives private data from the DApp or wallet flow, so run it locally or on infrastructure you control over an encrypted channel.

Start the proof server through your project script or Docker workflow. Midnight documentation identifies midnightntwrk/proof-server:8.0.3 as the current proof server image in the latest guide, and the proof server listens on port 6300.

npm run proof-server

Enter fullscreen mode Exit fullscreen mode

Then run the application flow against Testnet:

npm run compact
npm run build
npm run test:integration

Enter fullscreen mode Exit fullscreen mode

Your integration suite must verify:

  • Smart contract deployment completes.
  • Each exported circuit used by the UI submits a transaction.
  • The receipt reaches APPLIED_TO_CHAIN.
  • Public ledger state changes match expected values.
  • Private witness values do not appear in transaction outputs, public state, logs, or UI telemetry.
  • Replayed actions fail.
  • Unauthorized witness values fail.
  • Version metadata in logs matches the reviewed matrix.

Privacy tests should search serialized transaction outputs and public state for fixture secrets. Keep the fixture secret unique so the test catches accidental leaks. The following self-contained template shows the assertion shape. Replace the stub with your real DApp transaction flow.

import assert from "node:assert/strict";
import test from "node:test";

function submitWithPrivateMarker(marker) {
  if (typeof marker !== "string" || marker.length === 0) throw new Error("invalid marker");
  return {
    public: {
      status: "APPLIED_TO_CHAIN",
      transactionHash: "0xabc123",
    },
  };
}

test("does not expose the witness marker in public outputs", () => {
  const marker = "midnight-secret-fixture-5bb1c09f";
  const receipt = submitWithPrivateMarker(marker);
  const publicPayload = JSON.stringify(receipt.public);

  assert.equal(receipt.public.status, "APPLIED_TO_CHAIN");
  assert.equal(publicPayload.includes(marker), false);
});

Enter fullscreen mode Exit fullscreen mode

Run the standalone template with Node:

node --test privacy-output.test.mjs

Enter fullscreen mode Exit fullscreen mode

Never paste real private keys, wallet mnemonics, salts, or production witness data into tests. Use generated fixtures and destroy them after the run.

Automation you can check in

Static review does not replace compiler checks or Testnet tests, but it catches obvious mistakes early. Add this dependency-free script at scripts/midnight-predeploy-audit.mjs:

#!/usr/bin/env node
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path";

const root = process.argv[2] ?? "contract/src";
const noFail = process.argv.includes("--no-fail");
const sourceExtensions = new Set([".compact", ".ts", ".tsx", ".js", ".mjs", ".cts", ".mts"]);

function extensionOf(path) {
  const dot = path.lastIndexOf(".");
  return dot === -1 ? "" : path.slice(dot);
}

function walk(dir) {
  return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
    const path = join(dir, entry.name);
    if (entry.isDirectory()) return walk(path);
    if (!entry.isFile()) return [];
    return sourceExtensions.has(extensionOf(entry.name)) ? [path] : [];
  });
}

function add(findings, file, lineNumber, severity, rule, detail) {
  findings.push({ file, lineNumber, severity, rule, detail });
}

function scanFile(path, base) {
  const text = readFileSync(path, "utf8");
  const rel = relative(base, path);
  const isCompact = path.endsWith(".compact");
  const findings = [];

  text.split(/\r?\n/).forEach((line, index) => {
    const n = index + 1;
    if (/\bownPublicKey\s*\(/.test(line)) {
      add(findings, rel, n, "error", "ownPublicKey usage", "Do not authorize callers with ownPublicKey().");
    }
    if (isCompact && /\bexport\s+ledger\b/.test(line)) {
      add(findings, rel, n, "info", "exported ledger", "Confirm this field can be public.");
    }
    if (isCompact && /\bwitness\b/.test(line)) {
      add(findings, rel, n, "info", "witness declaration", "Confirm circuit logic validates the witness.");
    }
    if (isCompact && /\bdisclose\s*\(/.test(line)) {
      const severity = /(secret|private|sk|nonce|seed)/i.test(line) ? "warn" : "info";
      add(findings, rel, n, severity, "disclose call", "Confirm the disclosed value is intentional.");
    }
    if (isCompact && /\btransient(Hash|Commit)\s*</.test(line)) {
      add(findings, rel, n, "warn", "transient digest", "Do not persist transient digest output.");
    }
    if (/\bMath\.random\s*\(/.test(line)) {
      add(findings, rel, n, "warn", "weak randomness", "Use cryptographic randomness.");
    }
    if (/new\s+Uint8Array\s*\(\s*32\s*\)/.test(line) && !/(random|getRandom|fixture|test)/i.test(line)) {
      add(findings, rel, n, "warn", "zero-like secret", "Check for all-zero secrets, nonces, or salts.");
    }
  });

  return findings;
}

function main() {
  try {
    if (!statSync(root).isDirectory()) throw new Error("not a directory");
  } catch {
    console.error(`Source path not found or not a directory: ${root}`);
    process.exit(2);
  }

  const files = walk(root).sort();
  const findings = files.flatMap((file) => scanFile(file, root));
  const counts = findings.reduce((acc, item) => {
    acc[item.severity] += 1;
    return acc;
  }, { error: 0, warn: 0, info: 0 });

  console.log("# Midnight predeployment audit report\n");
  console.log(`Scanned path: \`${root}\``);
  console.log(`Files scanned: ${files.length}`);
  console.log(`Errors: ${counts.error}`);
  console.log(`Warnings: ${counts.warn}`);
  console.log(`Info: ${counts.info}\n`);

  if (findings.length) {
    console.log("| Severity | File | Line | Rule | Detail |");
    console.log("|---|---:|---:|---|---|");
    for (const item of findings) {
      console.log(`| ${item.severity} | \`${item.file}\` | ${item.lineNumber} | ${item.rule} | ${item.detail} |`);
    }
  } else {
    console.log("No findings. Continue with manual review and Testnet proof generation.");
  }

  if (counts.error > 0 && !noFail) process.exit(1);
}

main();

Enter fullscreen mode Exit fullscreen mode

Run it in CI:

node scripts/midnight-predeploy-audit.mjs contract/src

Enter fullscreen mode Exit fullscreen mode

The script exits with status 1 when it finds ownPublicKey(). That is intentional. Treat the failure as a prompt for human review, not as the full audit.

Final release checklist

Attach this evidence to the release issue:

  • disclose() audit table with owner, reason, and reviewed line number.
  • ownPublicKey() search output showing no unsafe authorization path.
  • Replay test output for each private action.
  • Exported ledger field table.
  • Witness implementation review notes.
  • Compatibility matrix snapshot and exact package lockfile.
  • Testnet transaction hashes for deployment and critical flows.
  • Proof server location and transport decision.
  • Logs that show the proof server, indexer, SDK, and Compact versions used by the release candidate.

Troubleshooting

The compiler reports a witness disclosure error

Move disclose() closer to the public write or return. If the value is a raw secret, stop and derive a public value with a persistent hash or commitment instead.

The audit script finds ownPublicKey() in a dependency

Trace whether the dependency participates in authorization. If it does, replace the path or upgrade to a reviewed pattern. If it is only a test fixture, document the file and keep it out of production builds.

The replay test succeeds twice

The smart contract is not consuming the nonce, counter, nullifier, or state transition that should make the second action invalid. Fix the circuit before release.

The proof server fails after a dependency update

Compare Compact devtools, the Compact toolchain, Compact runtime, Compact JS, Midnight.js, the DApp Connector API, the proof server, the ledger, and the indexer against the current compatibility matrix. Regenerate artifacts and rerun Testnet proofs.

Next steps

Save this checklist as docs/predeploy-security-checklist.md. Run it before every public deployment, after every Compact compiler upgrade, and after every dependency upgrade that touches Midnight.js, wallet code, generated artifacts, the indexer, or the proof server.

A strong Midnight release does not depend on trust in the UI. It depends on Compact circuits that disclose only intended values, validate every witness, reject replays, expose only reviewed ledger fields, use compatible versions, and prove the full flow on Testnet.