Witnesses in Depth: Patterns, Types, and Real Use Cases

TL;DR

In Midnight, a witness is an off-chain callback that supplies private or externally computed data to a circuit. The circuit does not trust the witness implementation itself; it only proves statements about the witness output under constraints you write in Compact. That distinction is the core design rule:

This tutorial covers:

Because the public primer included with this bounty does not enumerate every Compact operator, cryptographic primitive, or JavaScript witness runtime API, the code below is split into two parts:

  1. Compact snippets that use only syntax verified by the supplied primer and official Compact references
  2. Standalone TypeScript witness functions that are executable and testable as off-chain logic, without assuming undocumented Midnight SDK APIs

Where production code would normally use official crypto primitives or wallet/runtime hooks, I call that out explicitly and link the official docs as the source of truth.


Context: the trust boundary that makes witnesses useful

The official Compact reference defines witnesses as declarations like:

witness W(x: Uint<16>): Bytes<32>;

and describes them as TypeScript/JavaScript callbacks provided by the dApp, executing outside the proof system, with their return values consumed by circuits (Compact language reference, as summarized in the bounty primer).

That gives Midnight developers a clean split of responsibilities:

This matters because many things a contract wants are not natively part of the chain’s public state:

The Compact docs also attach an unusually strong warning:

Do not assume in your contract that the code of any witness function is the code that you wrote in your own implementation. Any DApp may provide any implementation that it wants for your witness functions.

That warning is the single most important design constraint in this tutorial. A witness is not trusted because you wrote it. A witness is only useful when the circuit checks enough properties of the witness output to make malicious implementations irrelevant.

If you keep only one rule from this article, keep this one:

Treat witness output as untrusted input. Constrain it in the circuit.


What witnesses are: off-chain computation feeding into the ZK circuit

In Compact, contracts have three relevant building blocks (Midnight docs, Compact language reference):

  1. Ledger declarations: public on-chain state
  2. Circuits: proof-producing entry points and helpers
  3. Witnesses: off-chain callbacks supplying values to circuits

A minimal shape looks like this:

ledger expected: Uint<32>;

witness ReadPrivateValue(): Uint<32>;

export circuit checkPrivateValue(): Boolean {
  const x = ReadPrivateValue();
  return x == expected;
}

Conceptually:

That makes witnesses the right tool when the dApp needs information that is:

What witnesses are not

A witness is not:

The contributor primer bundled with this bounty explicitly notes that Midnight has no built-in oracle, and recommends patterns such as witness-provided signed data plus in-circuit verification, or admin-updated ledger fields with access control. That is exactly the kind of boundary witnesses are designed for.

Why this split exists

Zero-knowledge circuits are best at proving that a relationship holds. They are not the right place to perform every piece of application logic from scratch, especially logic that depends on:

Witnesses let the dApp do those tasks off-chain, then hand the result into the proof.


How witnesses differ from circuit logic

Witnesses and circuits differ in three ways: execution environment, trust model, and verification cost.

1. Execution environment

A witness runs in JavaScript or TypeScript supplied by the dApp. A circuit runs in Compact and becomes part of the proving system.

Witness:

Circuit:

2. Trust model

This is the big one.

A circuit is part of the verified computation. A witness implementation is not. The dApp may swap the witness implementation entirely. Therefore:

For example, if a witness claims to return a quotient and remainder, the circuit must verify:

If it does not, the witness could simply lie.

3. Verification cost

Circuits are the expensive, proof-constrained layer. Witnesses are the cheap, flexible layer. A good design pushes computation outward where possible, while keeping the critical correctness conditions inside the circuit.

That design principle leads directly to the common witness patterns Midnight developers actually use.


Common patterns: secret key verification, division with remainder, and external data ingestion

This section covers the three patterns named in the bounty.

Pattern 1: secret key verification

The idea is simple:

Because the supplied primer does not list Midnight’s cryptographic APIs, the safest verified example is a shape rather than a production crypto implementation.

Compact shape

export sealed ledger registeredKey: Bytes<32>;

witness PresentKeyMaterial(): Bytes<32>;

export circuit isAuthorized(): Boolean {
  const candidate = PresentKeyMaterial();
  return candidate == registeredKey;
}

This proves that the witness returned the same 32-byte value as registeredKey.

In a real application, you would normally not expose or compare raw secret material directly. Instead, you would use the official cryptographic primitives documented by Midnight to verify a signature, public-key derivation, or commitment opening inside the circuit. The structural point remains the same: the witness supplies private material, and the circuit checks an authorization relation against public state.

Standalone TypeScript witness logic

// secret-key-verification.ts
type Bytes32 = Uint8Array;

function equalBytes32(a: Bytes32, b: Bytes32): boolean {
  if (a.length !== 32 || b.length !== 32) return false;
  for (let i = 0; i < 32; i++) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

export function presentKeyMaterial(localKey: Bytes32): Bytes32 {
  if (localKey.length !== 32) {
    throw new Error("Expected 32-byte key material");
  }
  return localKey;
}

// simple self-test
const sample = new Uint8Array(32);
sample[0] = 7;
const returned = presentKeyMaterial(sample);
if (!equalBytes32(sample, returned)) {
  throw new Error("Witness function failed self-test");
}

This is deliberately simple: it demonstrates the off-chain part without assuming undocumented wallet APIs.

Pattern 2: division with remainder

Division is a classic witness use case because the witness can compute quotient and remainder off-chain, while the circuit proves they are valid.

Compact declaration

witness Divide(n: Uint<32>, d: Uint<32>): [Uint<32>, Uint<32>];

pure circuit divisionIsValid(n: Uint<32>, d: Uint<32>): Boolean {
  const [q, r] = Divide(n, d);
  return n == q * d + r;
}

This is not yet complete, because a correct division witness must also satisfy r < d and, in practice, d != 0. The supplied primer does not enumerate every comparison operator, so I am stating the full rule in prose:

The official Compact reference should be consulted for the exact operator syntax currently supported.

Standalone TypeScript witness logic

// division-with-remainder.ts
export function divideWitness(n: bigint, d: bigint): [bigint, bigint] {
  if (d === 0n) {
    throw new Error("Division by zero");
  }
  const q = n / d;
  const r = n % d;
  return [q, r];
}

export function divisionRelationHolds(
  n: bigint,
  d: bigint,
  q: bigint,
  r: bigint
): boolean {
  if (d === 0n) return false;
  if (r < 0n) return false;
  return n === q * d + r && r < d;
}

// self-test
const [q, r] = divideWitness(23n, 5n);
if (!divisionRelationHolds(23n, 5n, q, r)) {
  throw new Error("Division witness failed self-test");
}

This is the exact off-chain computation the Compact circuit should be constraining.

Pattern 3: external data ingestion

Midnight has no built-in oracle, so external data has to arrive through one of the patterns the primer lists: witness-provided signed data plus in-circuit verification, admin-updated ledger state, or composition through other contracts.

The witness role here is to fetch or prepare the external payload. The circuit role is to verify whatever makes that payload acceptable.

Compact shape

ledger maxAcceptedValue: Uint<32>;

witness ReadExternalValue(): Uint<32>;

export circuit externalValueWithinLimit(): Boolean {
  const x = ReadExternalValue();
  return x == maxAcceptedValue;
}

This example is intentionally minimal and only proves equality with a ledger value. In a real ingestion flow, you would typically constrain more than that:

Again, the exact signature-verification primitives should come from the official Midnight docs, not guessed names in a tutorial.

Standalone TypeScript witness logic

// external-data-ingestion.ts
export async function readExternalValue(
  fetcher: () => Promise<number>
): Promise<number> {
  const value = await fetcher();
  if (!Number.isInteger(value) || value < 0) {
    throw new Error("External value must be a non-negative integer");
  }
  return value;
}

// self-test
async function selfTest() {
  const value = await readExternalValue(async () => 42);
  if (value !== 42) {
    throw new Error("External data witness failed self-test");
  }
}
void selfTest();

The important design point is not the fetch itself. It is the fact that the fetched value is untrusted until the circuit constrains it.


Witness-verified division pattern

Now let’s make the division example precise, because this is one of the most useful witness patterns in ZK applications.

Why division is a witness pattern

Circuits are very good at checking algebraic relations. They do not need to recompute long-form division if the witness can provide candidate outputs. The witness proposes (q, r), and the circuit proves that:

That is the standard “compute off-chain, verify in-circuit” shape.

Compact structure

Using only syntax verified in the primer, the structure looks like this:

witness Divide(n: Uint<32>, d: Uint<32>): [Uint<32>, Uint<32>];

pure circuit divisionEquation(n: Uint<32>, d: Uint<32>): Boolean {
  const [q, r] = Divide(n, d);
  return n == q * d + r;
}

To make this production-safe, add the missing semantic constraints from the language reference for your exact operator set:

If your contract uses the result for later state transitions, those constraints must be part of the proof path before any ledger write depends on q or r.

Why the extra constraints matter

Suppose you only check:

Then many invalid pairs can satisfy the equation. Example for n = 23, d = 5:

That is why the remainder bound is essential.

Standalone TypeScript implementation

// witness-verified-division.ts
export type DivisionResult = {
  quotient: bigint;
  remainder: bigint;
};

export function witnessVerifiedDivision(
  numerator: bigint,
  denominator: bigint
): DivisionResult {
  if (denominator === 0n) {
    throw new Error("denominator must be nonzero");
  }

  const quotient = numerator / denominator;
  const remainder = numerator % denominator;

  return { quotient, remainder };
}

export function verifyDivisionResult(
  numerator: bigint,
  denominator: bigint,
  result: DivisionResult
): boolean {
  if (denominator === 0n) return false;
  if (result.remainder < 0n) return false;

  const equation =
    numerator === result.quotient * denominator + result.remainder;
  const boundedRemainder = result.remainder < denominator;

  return equation && boundedRemainder;
}

// self-tests
const result = witnessVerifiedDivision(23n, 5n);
if (
  result.quotient !== 4n ||
  result.remainder !== 3n ||
  !verifyDivisionResult(23n, 5n, result)
) {
  throw new Error("Witness-verified division failed self-test");
}

const fake: DivisionResult = { quotient: 3n, remainder: 8n };
if (verifyDivisionResult(23n, 5n, fake)) {
  throw new Error("Invalid division result incorrectly accepted");
}

Where this pattern is useful

Witness-verified division appears whenever you need:

The recurring design lesson is that the witness is allowed to do the convenient computation, but the circuit must define what counts as a valid answer.


Witness-based access control

Witness-based access control is the other pattern explicitly requested in the bounty, and it follows directly from the trust boundary above.

The core idea

A contract wants to allow a privileged action only when a private party can supply evidence of authorization. A witness delivers that evidence. The circuit checks it against public state.

The simplest Compact shape is:

export sealed ledger adminKey: Bytes<32>;
ledger protectedValue: Uint<32>;

witness PresentAdminCredential(): Bytes<32>;

export circuit canAdminWrite(): Boolean {
  const candidate = PresentAdminCredential();
  return candidate == adminKey;
}

This is the authorization relation in its smallest form.

In practice, a full access-control flow usually wants more than identity:

The bounty primer points to related Midnight topics such as replay-attack prevention and ownPublicKey() trust assumptions, which is exactly where production access-control design gets subtle.

Why witness-based access control is useful

It is a natural fit when:

This is especially relevant in Midnight-style private applications, where the user often proves a relationship to a secret rather than publishing the secret itself.

Standalone TypeScript implementation

// witness-based-access-control.ts
type Bytes32 = Uint8Array;

function eq32(a: Bytes32, b: Bytes32): boolean {
  if (a.length !== 32 || b.length !== 32) return false;
  for (let i = 0; i < 32; i++) {
    if (a[i] !== b[i]) return false;
  }
  return true;
}

export function presentAdminCredential(localCredential: Bytes32): Bytes32 {
  if (localCredential.length !== 32) {
    throw new Error("Expected 32-byte credential");
  }
  return localCredential;
}

export function isAuthorizedAdmin(
  localCredential: Bytes32,
  registeredAdminKey: Bytes32
): boolean {
  return eq32(
    presentAdminCredential(localCredential),
    registeredAdminKey
  );
}

// self-test
const admin = new Uint8Array(32);
admin[31] = 99;

const user = new Uint8Array(32);
user[31] = 11;

if (!isAuthorizedAdmin(admin, admin)) {
  throw new Error("Admin should have been authorized");
}
if (isAuthorizedAdmin(user, admin)) {
  throw new Error("Unauthorized user was incorrectly accepted");
}

Security note

This example uses raw equality because that is the strongest claim we can make without inventing undocumented cryptographic APIs. In a production Midnight contract, access control should generally be expressed through documented public-key, signature, or commitment-verification primitives inside the circuit.


Real use cases from Midnight dApps and dApp patterns

The bounty asks for “real use cases from existing Midnight dApps.” Public Midnight sources linked in the issue do not currently provide a single canonical catalog of open-source production dApps together with their witness implementations. So the most defensible approach is to discuss publicly documented Midnight application patterns that clearly require witnesses, and that appear throughout Midnight’s docs and contributor-hub topic set.

1. Shielded asset flows and token vaults

The contributor-hub topic pool explicitly includes building a shielded token vault. These flows almost always need witnesses because the user must provide private state or locally held data to prove a valid action without revealing everything publicly.

Typical witness roles:

Typical circuit roles:

2. Anonymous membership and allowlists

Another contributor-hub topic is anonymous membership proofs. This is a textbook witness use case:

This is the same architectural pattern as access control, but with privacy-preserving group membership instead of a single admin key.

3. Compliance attestation and selective disclosure

The contributor-hub also lists compliance attestation with selective disclosure. Here, witnesses are useful because credential material, attestations, or user-local disclosures often exist off-chain first.

Typical witness roles:

Typical circuit roles:

4. Oracle-like signed data ingestion

The primer explicitly states that Midnight has no built-in oracle and recommends witness-provided signed data plus on-chain verification. Any dApp that needs exchange rates, external events, or off-chain reference values will follow this witness pattern.

Typical witness roles:

Typical circuit roles:

5. Verified math in private applications

The contributor-hub topic list also includes verified math in ZK, including division and exchange-rate patterns. This is not a toy use case. It is the general shape for any dApp where:

That shows up in private accounting, fair exchange, fee calculations, and allocation logic.

The common thread in all of these “real use cases” is the same: Midnight applications repeatedly need private or externally sourced data, and witnesses are the mechanism that brings that data into the proof boundary.


Working examples recap

For quick reference, here is the pattern map:

Pattern Witness provides Circuit must prove
Secret key verification key-derived or credential-derived value it matches authorized public state
Division with remainder quotient and remainder reconstruction equation and bounded remainder
External data ingestion fetched or locally assembled payload signature/range/freshness/identity checks
Witness-based access control private authorization evidence caller is authorized for the action

The important point is that witnesses do not replace constraints. They supply inputs that constraints reason about.


Pitfalls and common errors

1. Trusting the witness implementation

This is the number-one mistake. The Compact docs explicitly warn against it. If the circuit does not check the property, the property is not part of the proof.

2. Verifying only part of a relation

For division, checking n == q * d + r without r < d is incomplete.

For access control, checking a credential match without replay protection may also be incomplete.

3. Treating witness-fed external data as an oracle

A witness can fetch data, but that does not make the data trustworthy. Trust comes from in-circuit verification of signatures, identities, and policy rules.

4. Using undocumented assumptions as security boundaries

If a security argument depends on a wallet API, runtime hook, or cryptographic helper, use the official Midnight docs as the source of truth. Do not rely on guessed APIs or informal behavior.

5. Forgetting the public/private split

If a value belongs to local private state, a witness is often the right ingestion mechanism. If the contract needs a durable, publicly verifiable reference, it likely belongs in ledger state or in a verified external-data pattern.


References


Bounty spec coverage


Where to go next

Thanks for reading this far. If “Witnesses in Midnight” connected with where you are, three concrete next steps:

Learn more in Midnight

The full Midnight ZK Cookbook index has 17 tutorials across Midnight, Aleo, Aztec, Noir, and risc0 plus 4 Chinese translations. Adjacent tutorials are listed by ecosystem on that page.

Find paid work in Midnight

Bounty Radar tracks open ZK bounties across Algora, GitHub labels, Drips Wave, Code4rena, and Bountycaster. Browse the Midnight sub-feed; JSON at /midnight.json. The free tier is poll-based; the $19/mo Hobbyist tier pushes one filter to your Telegram in real time.

Audit your own ZK pipeline

zk-pipeline-doctor is the free MIT-licensed CLI that scores any ZK project on tests, CI, docs, security, reproducibility, and language toolchain (supports Compact, Leo, Noir, Cairo, and 7 Rust zkVMs). Drop it into a GitHub Action with zk-doctor-action for diff-aware PR comments. The $15/mo Pro tier adds four cross-ecosystem deep detectors (circuit complexity, proving-system pitfalls, verifier soundness, multi-file consistency).


Drafted with AI assistance and reviewed by the author before publishing. See DISCLOSURE for the full process.

If this saved you time

Three ways to support this work, pick whichever matches your situation:

$15 · One-time
All 17 tutorials as one Markdown + companion code repos. Offline-readable.
Get the Bundle →
$19 / month
Real-time bounty alerts pushed to your Telegram, filtered by ecosystem.
Try Radar →
$99 · One-time
Pre-flight audit of your ZK repo. See sample.
Order Audit →

Free alternative: Sponsor on GitHub · Star the repo · Share with one ZK developer who'd benefit

Related projects