Compiler-validated: Contract compiles (5 circuits) and 6/6 tests pass against Compact 0.29.0.
External data integration via a trusted oracle. The oracle provides data off-chain (e.g., price feeds, weather data, event outcomes), and the circuit validates format, freshness, and bounds before storing it on the public ledger. Demonstrates the witness-as-oracle pattern where the oracle's secret key authenticates updates, and domain-specific validation constraints ensure data quality within the ZK circuit.
pragma language_version >= 0.20;
import CompactStandardLibrary;
export ledger oracle: Bytes<32>;
export ledger prices: Map<Bytes<32>, Uint<64>>;
export ledger timestamps: Map<Bytes<32>, Uint<64>>;
export ledger updateCount: Counter;
export ledger stalePriceThreshold: Uint<64>;
witness localSecretKey(): Bytes<32>;
witness getCurrentTime(): Uint<64>;
export pure circuit publicKey(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "oracle:pk:"), sk]);
}
constructor() {
oracle = disclose(publicKey(localSecretKey()));
stalePriceThreshold = 3600;
}
export circuit postPrice(
asset: Bytes<32>,
price: Uint<64>,
timestamp: Uint<64>
): [] {
assert(publicKey(localSecretKey()) == oracle, "Only oracle can post prices");
assert(price > 0 as Uint<64>, "Price must be positive");
if (timestamps.member(disclose(asset))) {
const lastTimestamp = timestamps.lookup(disclose(asset));
assert(timestamp > lastTimestamp, "Timestamp must be newer than last update");
}
prices.insert(disclose(asset), disclose(price));
timestamps.insert(disclose(asset), disclose(timestamp));
updateCount.increment(1);
}
export circuit getPrice(asset: Bytes<32>): Uint<64> {
assert(prices.member(disclose(asset)), "Asset not found");
return prices.lookup(disclose(asset));
}
export circuit assertPriceFresh(asset: Bytes<32>): [] {
assert(prices.member(disclose(asset)), "Asset not found");
assert(timestamps.member(disclose(asset)), "No timestamp for asset");
const lastUpdate = timestamps.lookup(disclose(asset));
const now = getCurrentTime();
assert(disclose(now - lastUpdate < stalePriceThreshold), "Price is stale");
}
export circuit transferOracle(newOraclePk: Bytes<32>): [] {
assert(publicKey(localSecretKey()) == oracle, "Only current oracle");
oracle = disclose(newOraclePk);
}
export circuit setStalenessThreshold(newThreshold: Uint<64>): [] {
assert(publicKey(localSecretKey()) == oracle, "Only oracle");
assert(newThreshold > 0 as Uint<64>, "Threshold must be positive");
stalePriceThreshold = disclose(newThreshold);
}
The oracle's secret key authenticates updates through the standard authentication
pattern. The circuit verifies the caller is the registered oracle before accepting
data. No sequence counter needed -- the oracle key is permanent unless explicitly
rotated via transferOracle.
The circuit enforces constraints that cannot be faked: price > 0, monotonically increasing timestamps, staleness checks. These are ZK-proven guarantees.
The assertPriceFresh circuit lets consumers verify that a price was updated
within the staleness threshold. Note that getCurrentTime() is a witness and
therefore not cryptographically enforced -- the prover controls the time value.
transferOracle stores a new oracle public key directly. Since keys are
deterministic (no sequence), the new oracle just needs to derive their key from
their secret and the new oracle can start posting immediately.
stalePriceThreshold = 3600 assigns a compile-time constant to an export ledger
field. This does NOT need disclose() -- the compiler knows the value is not
witness-derived.
import { WitnessContext } from '@midnight-ntwrk/compact-runtime';
import type { Ledger } from '../src/managed/oracle/contract/index.js';
export interface OraclePrivateState {
readonly secretKey: Uint8Array;
}
export const witnesses = {
localSecretKey: (
{ privateState }: WitnessContext<Ledger, OraclePrivateState>,
): [OraclePrivateState, Uint8Array] => {
return [privateState, privateState.secretKey];
},
getCurrentTime: (
{ privateState }: WitnessContext<Ledger, OraclePrivateState>,
): [OraclePrivateState, bigint] => {
return [privateState, BigInt(Math.floor(Date.now() / 1000))];
},
};Compiler-validated simulator tests (6/6 passing). The getCurrentTime witness
is injected with controlled timestamps to test freshness logic deterministically.
Oracle key rotation test verifies old key loses access while new key gains it.
import { describe, it, expect, beforeEach } from "vitest";
import { Contract, pureCircuits } from "../src/managed/oracle/contract/index.js";
import {
createConstructorContext,
createCircuitContext,
sampleContractAddress,
} from "@midnight-ntwrk/compact-runtime";
import { setNetworkId } from "@midnight-ntwrk/midnight-js-network-id";
setNetworkId("undeployed");
const oracleKey = new Uint8Array(32); oracleKey[0] = 0x01;
const newOracleKey = new Uint8Array(32); newOracleKey[0] = 0x02;
const nonOracleKey = new Uint8Array(32); nonOracleKey[0] = 0x03;
const assetBTC = new Uint8Array(32);
new TextEncoder().encodeInto("BTC", assetBTC);
function makeWitnesses(sk, time = BigInt(Date.now())) {
return {
localSecretKey: ({ privateState }) => [privateState, sk],
getCurrentTime: ({ privateState }) => [privateState, time],
};
}
describe("Oracle Feed", () => {
let oracleContract, ctx;
beforeEach(() => {
oracleContract = new Contract(makeWitnesses(oracleKey));
const addr = sampleContractAddress();
const initial = oracleContract.initialState(createConstructorContext({}, addr));
ctx = createCircuitContext(
addr,
initial.currentZswapLocalState,
initial.currentContractState,
initial.currentPrivateState,
);
});
it("should post price and read it back", () => {
const r1 = oracleContract.impureCircuits.postPrice(ctx, assetBTC, 50000n, 1000n);
const r2 = oracleContract.impureCircuits.getPrice(r1.context, assetBTC);
expect(r2.result).toBe(50000n);
});
it("should reject non-oracle posting", () => {
const nonOracle = new Contract(makeWitnesses(nonOracleKey));
expect(() => {
nonOracle.impureCircuits.postPrice(ctx, assetBTC, 50000n, 1000n);
}).toThrow("Only oracle can post prices");
});
it("should reject zero price", () => {
expect(() => {
oracleContract.impureCircuits.postPrice(ctx, assetBTC, 0n, 1000n);
}).toThrow("Price must be positive");
});
it("should enforce timestamp monotonicity", () => {
const r1 = oracleContract.impureCircuits.postPrice(ctx, assetBTC, 50000n, 1000n);
expect(() => {
oracleContract.impureCircuits.postPrice(r1.context, assetBTC, 51000n, 500n);
}).toThrow("Timestamp must be newer than last update");
});
it("should allow oracle key rotation", () => {
const newOraclePk = pureCircuits.publicKey(newOracleKey);
const r1 = oracleContract.impureCircuits.transferOracle(ctx, newOraclePk);
expect(() => {
oracleContract.impureCircuits.postPrice(r1.context, assetBTC, 50000n, 1000n);
}).toThrow("Only oracle can post prices");
const newOracle = new Contract(makeWitnesses(newOracleKey));
const r2 = newOracle.impureCircuits.postPrice(r1.context, assetBTC, 50000n, 1000n);
expect(r2.context).toBeDefined();
});
it("should detect stale prices", () => {
const r1 = oracleContract.impureCircuits.postPrice(ctx, assetBTC, 50000n, 100n);
const staleFinder = new Contract(makeWitnesses(nonOracleKey, 100n + 3600n + 1n));
expect(() => {
staleFinder.impureCircuits.assertPriceFresh(r1.context, assetBTC);
}).toThrow("Price is stale");
});
});- Compiler-validated: 5 circuits compiled, 6/6 tests pass against Compact 0.29.0.
- No sequence counter: Oracle key is deterministic from secret key alone.
transferOraclereplaces the key directly. No key invalidation issues. - Time witness trust:
getCurrentTimeis not cryptographically enforced. The oracle provides both the timestamp and the price, so it's trusted to the same degree. The monotonicity check provides minimal safeguard. - Staleness subtraction:
now - lastUpdate < stalePriceThresholdcan underflow ifnow < lastUpdate. In practice, the monotonicity check on timestamps and the trusted time witness prevent this. - Consumer pattern: Other Midnight contracts cannot call this contract's circuits directly. Consumers read the oracle's public ledger via the indexer.
- For LOKx: this pattern is relevant for condition verification -- an oracle attests that a real-world event occurred, triggering a RELEASE.