Midnight does not ship a built-in oracle mechanism. That is not an omission so much as a design constraint: Compact circuits prove computation over explicit inputs and ledger state, while off-chain data remains off-chain unless your application deliberately introduces it. In practice, developers on Midnight reach for three oracle patterns:
Witness-provided data with on-chain verification
A witness supplies external data to the circuit, and the contract verifies enough evidence on-chain to decide whether to accept it. This is the most flexible pattern, but it only becomes trustworthy if the circuit validates signatures, rounds, digests, or other authenticity constraints.
Admin-updated ledger fields with access control
A designated updater account writes the current value into contract state. This is operationally simple and often the easiest thing to ship, but it concentrates trust in the admin key and updater process.
Cross-contract calls for composed state
One contract consumes state maintained by another contract. This is the cleanest pattern when the “oracle” is really another on-chain component in your application architecture, but it inherits the source contract’s trust model and upgrade policy.
This tutorial walks through all three patterns, shows minimal Compact examples for each, and explains the tradeoffs. Where the current public primer does not include a specific library API or import form, I call that out explicitly and point you to the official docs as the source of truth.
The Compact language reference defines three building blocks that matter here:
That last point is the most important one for oracle design. The docs warn:
“Do not assume in your contract that the code of any
witnessfunction is the code that you wrote in your own implementation. Any DApp may provide any implementation that it wants for yourwitnessfunctions.”
So witness output is untrusted input unless your circuit constrains it.
That is the reason Midnight does not have “native oracle support” in the way some developers expect from other chains. A Compact contract does not directly fetch HTTP data, read a market API, or trust a protocol-owned off-chain feed by default. The proof system verifies that the circuit was executed correctly over the supplied inputs; it does not prove that an external website was honest or that a witness callback consulted the source you intended.
From the application developer’s perspective, this means:
In other words, Midnight gives you the primitives, not the oracle.
That is why the contributor bounty itself points to the three patterns in this article. They are not arbitrary examples. They are the practical design space when you need off-chain information in a Compact contract.
Before we get into code, two implementation notes:
I use only Compact syntax and concepts that are directly supported by the supplied primer whenever possible.
That includes ledger, sealed ledger, circuit, witness, tuples, structs, and disclose(...).
Two details depend on the current docs and library version in your project:
This is the most “oracle-like” pattern in Midnight.
A witness supplies a piece of external data,say, a price, an exchange rate, a weather reading, or a compliance attestation,and the circuit checks enough evidence to decide whether the value is acceptable.
The core idea is simple:
Use witness-provided data when:
Typical examples include:
Because the witness is untrusted, a secure circuit usually checks some combination of:
A “signed price feed” is just one concrete instance of this general pattern.
The following example models a feed update with a round number and a signer. The exact signature-verification helper depends on the current library release, so I leave that one helper isolated as the only assumption.
struct PriceUpdate {
price: Uint<32>,
round: Uint<32>,
signer: Bytes<32>,
signature: Bytes<64>
}
export sealed ledger trustedSigner: Bytes<32>;
export ledger lastAcceptedRound: Uint<32>;
export ledger currentPrice: Uint<32>;
witness nextPriceUpdate(): PriceUpdate;
// Assumption: bind this helper to the current crypto-verification API from the official docs.
// The shape is shown to isolate the trust logic in one place.
pure circuit verifySignedPriceUpdate(update: PriceUpdate): Boolean {
// Replace with the current standard-library or project-specific signature check.
// Expected policy:
// 1. signer must equal trustedSigner
// 2. signature must authenticate (price, round) under signer
return true;
}
export circuit initializeSigner(signer: Bytes<32>): [] {
trustedSigner = disclose(signer);
}
export circuit submitVerifiedPrice(): Uint<32> {
const update = nextPriceUpdate();
// Assumption: use the current assertion/failure primitive from the Compact stdlib/docs.
assert(verifySignedPriceUpdate(update));
assert(update.signer == trustedSigner);
assert(update.round > lastAcceptedRound);
lastAcceptedRound = update.round;
currentPrice = update.price;
return update.price;
}
The witness itself is not trusted. The verification logic is trusted.
If the witness returns a fake price:
That is the right mental model for witnesses on Midnight: they are input channels, not trust anchors.
If you are not yet using signatures, you can still apply the same pattern using a precommitted digest or versioned value. This is less flexible than real signed updates, but it stays closer to the minimal syntax shown in the public primer.
struct QuotedValue {
value: Uint<32>,
round: Uint<32>
}
export ledger approvedValue: Uint<32>;
export ledger approvedRound: Uint<32>;
witness quotedValue(): QuotedValue;
export circuit acceptQuotedValue(): Uint<32> {
const q = quotedValue();
assert(q.round > approvedRound);
approvedValue = q.value;
approvedRound = q.round;
return q.value;
}
This second snippet is not a substitute for cryptographic authentication. It shows the structural pattern only: witness input becomes acceptable only after the circuit enforces policy.
For this pattern, your tests should at minimum cover:
If your feed is economically sensitive, also test edge conditions like zero prices, overflow boundaries on Uint<n>, and out-of-order delivery.
This pattern gives you the best decentralization story if the verification logic is strong.
Its upside:
Its downside:
A useful rule of thumb is this: witnesses are great at transport; they are bad as sources of truth. The truth must come from whatever the circuit verifies.
The second pattern is operationally simpler: appoint an updater, store the latest value in ledger state, and gate writes with access control.
This is the closest thing to a conventional centralized oracle.
Use admin-updated fields when:
Typical examples include:
A good admin-updated oracle contract usually wants:
Here is a minimal version.
export sealed ledger adminKey: Bytes<32>;
export ledger latestValue: Uint<32>;
export ledger latestVersion: Uint<32>;
witness callerKey(): Bytes<32>;
export circuit initializeAdmin(key: Bytes<32>): [] {
adminKey = disclose(key);
}
export circuit updateValue(nextValue: Uint<32>, nextVersion: Uint<32>): [] {
const caller = callerKey();
assert(caller == adminKey);
assert(nextVersion > latestVersion);
latestValue = disclose(nextValue);
latestVersion = disclose(nextVersion);
}
export circuit readValue(): [Uint<32>, Uint<32>] {
return [latestValue, latestVersion];
}
adminKey is sealed, so it cannot be rebound after initial setup.updateValue requires the witness-reported caller key to match adminKey.latestVersion prevents accidental replays or out-of-order updates.This example uses a witness to provide a caller identity because that is one of the primitives explicitly described in the supplied reference material. In a production contract, you should bind authorization to the current official Midnight identity/authentication mechanism rather than relying on a naked witness-supplied caller value.
That distinction matters because the same witness warning still applies: a witness alone is not self-authenticating.
So the production pattern is:
The structural lesson remains the same: this oracle pattern trusts an administrator, not a witness.
In a real system, the updater process is usually an off-chain service that:
That operational simplicity is why this pattern is common even in systems that aspire to decentralize later. It is also why it is dangerous to hide the trust model. You are trusting a key, an operator, and a process.
At minimum, test:
If you add rotation:
This pattern is easy to reason about and easy to integrate. Consumers do not need to package signatures or witness proofs with every call. They simply read the on-chain value.
But the tradeoff is direct:
So this is not a trustless oracle. It is an explicit trusted-operator oracle. That is acceptable in many applications, but it should be documented plainly.
The third pattern is different. Instead of importing data from the outside world directly, one contract consumes state that another contract already maintains on-chain.
This is useful when the “oracle” is not really an oracle service at all, but a specialized contract whose state should be reused by other contracts.
Examples:
Use cross-contract composition when:
This is not a way to eliminate trust. It is a way to centralize trust in one on-chain source instead of duplicating it.
A minimal provider can look like this:
export ledger sharedPrice: Uint<32>;
export ledger sharedRound: Uint<32>;
export circuit setSharedPrice(price: Uint<32>, round: Uint<32>): [] {
sharedPrice = disclose(price);
sharedRound = disclose(round);
}
export circuit getSharedPrice(): [Uint<32>, Uint<32>] {
return [sharedPrice, sharedRound];
}
This contract does one thing: expose a canonical price and round.
The exact cross-contract import/call syntax is not included in the supplied Compact primer, so the following example shows the intended structure and should be bound to the current docs before compilation.
// Assumption: replace this import form with the current Compact cross-contract syntax.
import "./PriceProvider" prefix PriceProvider;
export ledger lastObservedPrice: Uint<32>;
export ledger lastObservedRound: Uint<32>;
export circuit syncFromProvider(): [Uint<32>, Uint<32>] {
// Assumption: replace this call with the current cross-contract call syntax.
const [price, round] = PriceProvider.getSharedPrice();
lastObservedPrice = price;
lastObservedRound = round;
return [price, round];
}
Suppose you have three application contracts:
LendingMarketLiquidationEngineTreasuryIf each one used its own admin-updated price field, you would create three separate trust surfaces and three chances for divergence. A shared provider contract avoids that.
The provider becomes the single state authority, and consumers inherit its answer.
Even with cross-contract composition, consumers should still think about:
For example, a consumer may want to reject a provider round lower than a locally remembered minimum, or it may want to store the observed round to prevent regressions.
At minimum, test:
Cross-contract composition often feels more “on-chain” than the other two patterns, but the trust question has only moved, not vanished.
The consumer trusts:
If the provider itself uses an admin-updated feed, then every consumer indirectly trusts that admin. If the provider uses witness-submitted signed updates, every consumer indirectly trusts that verification logic.
So cross-contract calls are best thought of as a trust distribution pattern, not a source-of-truth pattern on their own.
These patterns solve different problems.
This is usually the best answer for highly dynamic data, assuming you are prepared to implement verification carefully.
This is often the right first version for internal or low-risk applications.
This is usually an architectural improvement rather than a replacement for the other two.
Many production systems combine them:
That hybrid architecture usually gives the cleanest separation of duties.
This is the biggest mistake. The docs explicitly warn against it. If a witness supplies a price, signature, caller identity, nonce, or timestamp-like value, your circuit must constrain it.
A real signed update can still be stale. Always pair authenticity checks with a round, version, or nonce policy.
If the same signed payload can be submitted twice, you may open the door to duplicate state transitions or stale-value reuse. Track the last accepted round or used nonce.
If a single team-controlled key can set the oracle, say so. Hidden trust assumptions are worse than centralized ones.
If several contracts need the same value, a provider/consumer architecture is usually cleaner than independent copies. Otherwise you create synchronization problems.
A consumer contract should decide what to do when the source value is old, unchanged, or unavailable. “Latest” is not always “fresh enough.”
Do not infer caller identity from a witness unless the platform docs explicitly define that mechanism as authoritative. Use the official Midnight authorization model for access control.
If your contract hardcodes one signer, one provider, or one updater path, plan for rotation and recovery. Key compromise and operator failure are operational realities, not edge cases.
Thanks for reading this far. If “Oracle patterns on Midnight” connected with where you are, three concrete next steps:
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.
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.
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.
Three ways to support this work, pick whichever matches your situation:
Free alternative: Sponsor on GitHub · Star the repo · Share with one ZK developer who'd benefit