A Pyth/Wormhole price oracle for CKB. Hermes provides signed Pyth price update bytes, Wormhole guardian-set data defines the trust root, and on-chain CKB scripts verify and store the latest accepted price state in oracle cells that downstream protocols can read as cell deps.
| Testnet | Live — see Live Deployments |
| Mainnet | Inert (API/config surfaces exist; no deployment yet) |
| SDK | lean-oracle-sdk on npm |
| License | MIT |
| Audit | Not audited — see Security |
The repository contains the on-chain scripts, a TypeScript SDK, and a deployment toolbox for publishing and operating the oracle.
- CKB-native cell model. Each price feed is a single, addressable oracle cell consumers reference as a cell dep. No registry contract, no router — the cell is the feed.
- Permissionless updates. Anyone can push a fresh Hermes update; the
owned_type_bind_lockpreserves type continuity and gives the owner an escape path without gating who can publish prices. - Minimal trust additions. The oracle inherits Pyth/Wormhole's guardian-set quorum security and adds nothing beyond it. Hermes is an untrusted transport.
- Lean script footprint. Three small CKB scripts (oracle, guardian-set,
bind-lock) with shared parsing in one
commoncrate. No precompiles, no hidden dependencies. - Honest scope. The oracle verifies authenticity and monotonicity. Freshness limits and risk policy are explicitly the consumer's responsibility — no false guarantees baked in.
┌──────────────────────────┐
│ Pythnet (off-chain) │ signs price updates
└──────────────┬───────────┘
│ guardian signatures
▼
┌──────────────────────────┐
│ Hermes (untrusted CDN) │ serves accumulator update blobs
└──────────────┬───────────┘
│ fetched by anyone
▼
┌────────────────────────────────────────┐
│ Update tx submitter (any CKB wallet) │
│ - drafts tx via lean-oracle-sdk │
│ - rebalances fees │
│ - signs + broadcasts │
└──────────────┬─────────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ CKB chain │
│ ┌──────────────────────────────────┐ │
│ │ oracle_script (type script) │ │ verifies:
│ │ + owned_type_bind_lock (lock) │ │ - guardian sig quorum
│ │ + guardian_set cell (cell dep) │ │ - emitter chain/address
│ │ │ │ - feed id match
│ │ ⇒ Oracle Cell (price state) │ │ - publish_time monotonic
│ └──────────────────────────────────┘ │
└──────────────┬─────────────────────────┘
│ cell dep
▼
┌──────────────────────────┐
│ Consumer dApp / script │ reads price, applies own
│ (your protocol) │ staleness/risk rules
└──────────────────────────┘
A successful update proves that:
- the update bytes are structurally valid
- the update is signed by the active guardian set (current-set-only policy)
- the update comes from the configured emitter chain and emitter address
- the message contains the expected Pyth price feed id
- the new
publish_timeis strictly newer than the existing oracle state - the output oracle cell exactly matches the authenticated price message
The oracle verifies authenticity and monotonicity. Freshness limits, deviation bounds, and other application-specific risk rules are the consumer's responsibility.
Install the SDK and read the latest testnet price for a Pyth feed:
npm install lean-oracle-sdk @ckb-ccc/coreimport { LeanOracleTestnetClient } from "lean-oracle-sdk";
const feedId =
"0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; // BTC/USD
const oracle = new LeanOracleTestnetClient();
const state = await oracle.getOracleCellState({ feedId });
if (!state) throw new Error(`No oracle cell for ${feedId}`);
// Apply your own staleness policy — the oracle guarantees monotonicity, not freshness.
const ageSeconds = Math.floor(Date.now() / 1000) - Number(state.data.publishTimeUnix);
if (ageSeconds > 60) throw new Error(`Price stale: ${ageSeconds}s old`);
console.log({
price: state.data.price, // bigint, scaled by 10^expo
expo: state.data.expo,
publishTimeUnix: state.data.publishTimeUnix,
outPoint: state.outPoint, // use as a CKB cell dep from your script
});On-chain consumers reference the oracle cell by outPoint as a CellDep,
decode its data using the layout in crates/lean_oracle/contracts/common/, and
apply their own staleness/deviation rules before acting on the price.
getOracleCellState returning undefined means no live oracle cell exists for
that Pyth feed id under the default public lock. Any feed Pyth signs (ETH/USD,
SOL/USD, equities, FX, …) can be brought up by deploying your own oracle cell
under a lock script you control — the on-chain scripts are permissionless,
so anyone can host a cell for any valid feed:
import { ccc } from "@ckb-ccc/core";
import { leanOracleTestnetPreset } from "lean-oracle-sdk/presets";
import { initiateOracleDeployTx } from "lean-oracle-sdk/tx";
import { rebalanceFuel } from "lean-oracle-sdk/fuel";
const cccClient = new ccc.ClientPublicTestnet();
const signer = new ccc.SignerCkbPrivateKey(cccClient, process.env.CKB_PRIVATE_KEY!);
const myLock = (await signer.getRecommendedAddressObj()).script;
const tx = await initiateOracleDeployTx({
network: leanOracleTestnetPreset,
cccClient,
feedId: "0x...ETH/USD feed id...",
oracleLockScript: myLock, // your lock governs subsequent updates
capacity: 20_000_000_000n, // shannons; size per cell layout + headroom
});
const balanced = await rebalanceFuel(tx, { cccClient, lockScript: myLock, fuelLimit: 32 });
if (balanced.status !== "ok") throw new Error("insufficient capacity");
await signer.sendTransaction(balanced.mutated);The cell you deploy is yours: the lock you supply governs who can submit
subsequent updates and who can burn the cell. If you want public, permissionless
updates instead, deploy under the owned_type_bind_lock preset
(live deployments) — it keeps type continuity while
letting anyone push fresh prices, with an owner escape path.
See packages/sdk/README.md for the full SDK API,
feed-id lookup, update-transaction drafting, and devnet integration.
Guardian set index 6, quorum 13 (Wormhole mainnet guardians).
| Component | Version | Code Hash | Deploy Tx |
|---|---|---|---|
oracle_type |
v2 (latest) | 0x10c9bcc3af00fc3728cb95d5e14ec882716af5f531a010852526ce784f6958ec |
0x45f033f0944b50be1e5b80f733c321648ddcfdbe0c183477cf0b77bd0f8312b5 |
oracle_type |
v1 (legacy) | 0x2277560d62a11a92084654b67848ea893fcf3c1880e20a3ce9c0c19d0ee27dc3 |
0xf39d3cb5eccab560bdab65529f4e6f86c2dc8c966a4d49a2fd17bb277e75bba2 |
guardian_set_type |
v1 | 0x57bddf3d57ea45c88ab68d0de706bbaecd68895fd6062b099626deb157100119 |
0x78f83c3967c566c50c783d45c9165af94d23018c5254228b3eb418aa0c5ac37f |
owned_type_bind_lock |
v1 | 0x5554bc20c9f3dbb8d1d7a6591b1b2ceeb0bbee822804635ee168911a440a111c |
0x982a5d5555ebc855a97d9e71a8ac9de9cefc25a62a44ccfc2b6605758c01ba9f |
All values, plus oracle/guardian-set cell outpoints and the full version
history, are checked in under
deployment/artifacts/testnet.*.json. The SDK's
leanOracleTestnetPreset consumes these directly — most consumers do not need
the hashes by hand.
Not deployed. LeanOracleMainnetClient is exported for API continuity but its
deployment values are intentionally inert until a real mainnet deployment is
published.
Lean Oracle keeps the main pieces of the Pythnet/Wormhole model, but represents them in CKB-native state:
- Hermes — off-chain transport for Pyth accumulator update blobs. Hermes is not trusted; it only delivers bytes that the scripts verify.
- Guardian set — the Wormhole guardian addresses and quorum used to verify update signatures. The active set lives in a guardian-set cell.
- Emitter chain — the Wormhole source chain id expected for Pythnet price messages.
- Emitter address — the expected Pyth emitter address for the configured source.
- Feed id — the 32-byte Pyth price feed id (e.g. BTC/USD) that identifies a specific oracle cell.
- Accumulator update — the signed update payload fetched from Hermes and supplied as witness data during oracle updates.
This implementation uses a current-set-only guardian policy. Only the active canonical guardian set is accepted. After Wormhole guardian rotation, callers must fetch a fresh Hermes update signed by the new active set.
What you trust when consuming a Lean Oracle price:
- Wormhole guardian quorum (13-of-19 on the configured set). A guardian-majority compromise can produce a fraudulent update Lean Oracle will accept.
- Pythnet emitter at the configured chain id + emitter address — the source of the signed price messages.
- CKB consensus for the chain Lean Oracle is deployed on.
What you do not trust:
- Hermes — pure transport; tampered or stale bytes are rejected by signature verification.
- The update submitter — anyone can push an update; the script enforces
feed-id match, emitter match, signature quorum, and
publish_timemonotonicity. - Update frequency — the oracle does not enforce a maximum age. If you need a freshness bound, enforce it in your consumer (see the snippet above).
Known boundaries:
- Guardian rotation: the current-set-only policy means that immediately after Wormhole rotates guardians, an update signed by the previous set is rejected. The first update under the new set must rotate the guardian-set cell.
- No price arbitration: the oracle stores exactly what Pyth signed. If Pyth publishes an aberrant value, Lean Oracle will accept it. Consumers should apply deviation/sanity checks if their use case warrants.
- No audit yet. This codebase has not been independently audited. Treat the testnet deployment as experimental. Do not use Lean Oracle for mainnet-equivalent value at risk until an audit lands.
crates/lean_oracle/ Rust workspace for CKB contracts and contract tests
contracts/common/ Shared parsing, hashing, layouts, and verifier logic
contracts/oracle_script/ Oracle cell type script
contracts/guardian_set_script/
Guardian-set cell type script
contracts/owned_type_bind_lock/
Public-update lock with owner escape path
tests/ Host-side and ckb-testtool integration tests
packages/sdk/ TypeScript SDK published as lean-oracle-sdk
deployment/ TypeScript deployment CLI and network config
config/ Checked-in network deployment intent
artifacts/ Generated deployment outputs (per network)
src/, tests/ Deployment actions, validators, and tests
docs/superpowers/specs/ Design specs (see below)
reports/ Weekly progress reports
- Deployment pipeline design — how the deployment CLI promotes script versions and writes artifacts.
- SDK devnet integration tests design — how the opt-in devnet suite is structured.
make contracts-build # build optimized RISC-V CKB binaries
make contracts-test # host-side test suite + ckb-testtool integrationcontracts-test runs against x86_64-unknown-linux-gnu so host test
dependencies do not get compiled for the no-std contract target.
cd packages/sdk
npm install
npm test # codecs, discovery, builders, witnesses, exports
npm run release:check # test + tarball-pack subpath checkSee packages/sdk/README.md for devnet integration
tests, custom CCC client wiring, and code-version pinning.
cd deployment
npm install
npm run build
npm test
node --enable-source-maps ./dist/index.js <action> \
--network <testnet|mainnet|devnet>Typical bootstrap order for a network:
deploy:guardian-set-type → promote:guardian-set-type → deploy:guardian-set
deploy:oracle-type → promote:oracle-type → deploy:oracle
See deployment/README.md for required environment
variables, dry-run behavior, artifact formats, and local devnet overrides.
The SDK is isolated from the deployment package: for a local devnet, build
your own LeanOracleNetworkConfig from local deployment metadata and pass it
plus a hand-built CCC client to LeanOracleClient. Do not use the testnet
preset for devnet cells. Generated local devnet artifacts should not be
committed.
Issues and pull requests welcome at https://github.com/calledAdo/lean-oracle/issues.
The repo uses three checks that should pass before opening a PR:
make contracts-test
( cd packages/sdk && npm run release:check )
( cd deployment && npm test )Lean Oracle is released under the MIT License — see LICENSE.
This covers the on-chain Rust contracts, the TypeScript SDK, and the
deployment toolbox.